diff --git a/.claude/backend.md b/.claude/backend.md new file mode 100644 index 0000000..729ab2e --- /dev/null +++ b/.claude/backend.md @@ -0,0 +1,217 @@ +# 02 — 后端规则 + +> **适用**:所有后端 AI 开发任务。 + +--- + +## 分层不变式 + +``` +Controller → 参数校验 + 调 Service + 返回统一格式 + 禁止:写业务逻辑、直接调 Mapper、处理事务 + +Service → 业务逻辑 + 事务管理 + 禁止:处理 HTTP 请求/响应、直接操作 Request/Response + +Mapper → 数据访问(MyBatis-Plus BaseMapper) + 禁止:写业务逻辑、调用其他 Mapper + +Entity → 数据映射(@TableName 对应表名) + 禁止:写业务方法、放非持久化字段(放 DTO/VO) +``` + +### 包结构 + +``` +com.ghb.base/ +├── system/ # 系统模块(不改) +│ ├── entity/ SysUser, SysRole, SysMenu... +│ ├── controller/ +│ └── service/ +└── business/ # 业务模块 ← 你的代码放这里 + ├── entity/ # 继承 JeecgBoot 的 BaseEntity + ├── controller/ # 继承 JeecgController + ├── service/ # impl/ 继承 ServiceImpl + ├── mapper/ # 继承 BaseMapper + │ └── xml/ # MyBatis XML(复杂查询才需要) + └── vo/ # 请求/响应 DTO (*VO, *Request, *ImportRow) +``` + +--- + +## 命名规则 + +| 元素 | 规则 | 示例(正确 → 错误) | +|------|------|---------------------| +| **类名** | 大驼峰,名词 | `TaskController` `OrderService` `UserMapper` | +| **方法名** | 小驼峰,动词开头 | `getById()` `saveTask()` `listByStatus()` | +| **变量** | 小驼峰,有意义 | `taskList` `userId` → ❌ `list` `id` `data` | +| **包名** | 全小写,点分隔 | `com.xxx.server.controller` | +| **Entity** | 大驼峰,对应表名 | 表 `task_apply` → 类 `TaskApply` | +| **DTO** | 大驼峰,后缀 DTO/VO/Request | `TaskCreateRequest` `UserListVO` | +| **常量** | 全大写,下划线分隔 | `MAX_RETRY_COUNT` `DEFAULT_PAGE_SIZE` | +| **SQL 表名** | 小写,下划线分隔 | `task_apply` `user_profile` | +| **SQL 字段** | 小写,下划线分隔 | `create_time` `task_id` `del_flag` | +| **URL 路径** | 小写,短横线或斜杠 | `/app/task/list` → ❌ `/app/Task/GetList` | +| **配置文件** | kebab-case 或小写 | `application-prod.yml` | + +### 长度控制 + +类名、方法名、表名不要过长。超过 3 个单词或 30 个字符时,用通用缩写。 + +**前缀也要缩**:模块前缀用一个单词,别堆多个词。 + +| 完整 | 缩写 | JeecgBoot 实际案例 | +|------|------|-------------------| +| Department | Dept | `SysDepartRolePermission` → `SysDeptRolePerm` | +| Permission | Perm | 同上 | +| Announcement | Notice | `SysAnnouncementSend` → `SysNoticeSend` | +| Enterprise | Ent | `WechatEnterprise` → `WxEnt` | +| Message | Msg | `SysMessageTemplate` → `SysMsgTemplate` | + +```java +// ❌ JeecgBoot 原版 — 太长 +SysDepartRolePermissionServiceImpl // 35 字符 +SysAnnouncementSendServiceImpl // 30 字符 +ThirdAppWechatEnterpriseServiceImpl // 35 字符 + +// ✅ 缩写后 +SysDeptRolePermServiceImpl // 26 字符 +SysNoticeSendServiceImpl // 25 字符 +ThirdAppWxEntServiceImpl // 27 字符 +``` + +**原则**:优先用全称,超长再缩写。缩写必须一眼能认出含义,不自造别人看不懂的缩写。 + +--- + +## 注释规则 + +| 位置 | 要求 | 示例 | +|------|------|------| +| **类/接口** | 必须有 Javadoc,说明职责 | `/** 任务管理 Controller,处理任务的增删改查 */` | +| **公共方法** | 必须有 Javadoc,说明入参/返回/异常 | `/** @param taskId 任务ID @return 任务详情 @throws 无 */` | +| **复杂逻辑** | 行内注释说明"为什么这么做" | `// 先查缓存再查库,避免击穿` | +| **常量** | 必须有注释说明含义 | `/** 最大重试次数 */` | +| **TODO/FIXME** | 必须有负责人和日期 | `// TODO(yaoshuli 2026-06) 后续改为配置项` | +| **禁止** | ❌ 注释写"做了什么"(代码本身已说明) | `// 查询任务列表` ← 删掉 | +| **禁止** | ❌ 注释掉的旧代码 | 直接删,Git 有历史 | +| **禁止** | ❌ 无意义注释 | `// 定义一个变量` `// 返回结果` | +| **禁止** | ❌ Controller 方法加 Javadoc 说"这是xxx接口" | 写业务含义,不写技术废话 | + +### 注释示例 + +```java +/** 任务服务,负责任务的创建、分配、状态流转 */ +@Service +public class TaskServiceImpl implements TaskService { + + /** 单次批量操作上限 */ + private static final int BATCH_LIMIT = 100; + + /** + * 根据状态和截止时间查询待处理任务。 + * 先查 Redis 缓存,未命中再查 MySQL。 + * + * @param status 任务状态,不能为 null + * @param deadline 截止时间,只查此时间之前的任务 + * @return 任务列表,可能为空 + */ + @Override + public List listPending(String status, LocalDateTime deadline) { + // 缓存 key: pending:{status} + String cacheKey = "pending:" + status; + List cached = cacheService.get(cacheKey); + if (cached != null) return cached; + + // 查库(这里用复合索引 idx_status_deadline) + List tasks = taskMapper.selectByStatusBefore(status, deadline); + cacheService.set(cacheKey, tasks, 300); + return tasks; + } +} +``` + +## 新增功能标准步骤 + +``` +1. 建表 SQL → [项目数据库脚本目录] +2. Entity → 表映射 +3. Mapper → 继承 BaseMapper +4. Service 接口+实现 → 业务逻辑 +5. Controller → 接口暴露 +6. 编译验证 → 构建通过 +``` + +--- + +## CRUD 模板(填空式) + +```java +// ===== 分页列表 ===== +@GetMapping("/list") +public Result> list(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + Page p = new Page<>(page, size); + // [填空] 构建查询条件 + return Result.ok(service.page(p, wrapper)); +} + +// ===== 详情 ===== +@GetMapping("/{id}") +public Result detail(@PathVariable Long id) { + Xxx entity = service.getById(id); + if (entity == null) return Result.error("[资源]不存在"); + return Result.ok(entity); +} + +// ===== 新增 ===== +@PostMapping +public Result create(@RequestBody @Valid XxxDto dto) { + // [填空] DTO → Entity 转换 + service.save(entity); + return Result.ok(); +} + +// ===== 更新 ===== +@PutMapping("/{id}") +public Result update(@PathVariable Long id, @RequestBody @Valid XxxDto dto) { + Xxx entity = service.getById(id); + if (entity == null) return Result.error("[资源]不存在"); + // [填空] 字段更新 + service.updateById(entity); + return Result.ok(); +} +``` + +--- + +## 禁止清单 + +| 类别 | ❌ 禁止 | +|------|--------| +| **分层** | Controller 里写 if/else 业务判断 | +| **分层** | Controller 直接调 Mapper/Repository | +| **分层** | Entity 里写业务方法 | +| **数据** | SQL/查询条件字符串拼接 | +| **数据** | 查不到不判空,直接返回可能 NPE | +| **数据** | `selectOne` / `getOne` 不做唯一性约束 | +| **输入** | 入参不校验(该用 `@Valid` 的地方手撸 if) | +| **输出** | 返回裸对象、裸字符串、自造格式 | +| **值** | 代码里硬编码魔法数字、魔法字符串 | +| **事务** | 该加事务的地方漏加 | + +--- + +## 检查表 + +每完成一个后端任务,逐项自检: + +- [ ] 编译通过(`mvn compile` / `go build` / 对应构建命令) +- [ ] 所有返回值是统一格式,无裸返回 +- [ ] 分页接口格式正确(`list/total/pages/current/size`) +- [ ] Controller 方法体不超过 20 行(超过说明写了业务逻辑) +- [ ] 查不到/null 路径有明确的 `fail` 返回 +- [ ] 改了接口 → 已 grep 同步所有前端调用方 +- [ ] 新增/改字段 → 已更新数据库脚本 +- [ ] 无禁止清单中的违规项 diff --git a/.claude/contract.md b/.claude/contract.md new file mode 100644 index 0000000..a213462 --- /dev/null +++ b/.claude/contract.md @@ -0,0 +1,108 @@ +# 01 — 接口契约 + +> **这是前后端共享的唯一真相来源。** 修改本文件 = 修改协议,必须同步更新所有端。 + +--- + +## 契约生命周期(任务拆分时决定) + +契约由**任务拆分时先开工的那一端**负责起草。这是任务产出物之一,不是额外工作。 + +``` +任务拆分阶段: + ┌─ 任务A:后端-用户管理CRUD ──→ 产出:接口契约(后端起草) + │ + 后端代码 + │ + └─ 任务B:前端-用户管理页面 ──→ 拿到契约后对接 + +反过来: + ┌─ 任务A:前端-首页改版 ──→ 产出:接口契约(前端起草) + │ + 前端页面 + │ + └─ 任务B:后端-首页数据接口 ──→ 按契约实现 +``` + +**铁律**: +- 拆分任务时明确标注:**本任务包含接口契约起草** +- 契约随任务一起交付,不单独走审批 +- 后开工的一方拿到契约后,发现问题直接跟起草方沟通修改 +- 联调时对照本文件,不一致的以本文件为准 + +### 分仓库时契约放哪 + +``` +Monorepo(推荐): 分仓库: +项目根/ 后端仓库/ 前端仓库/ +└── rules/ ├── rules/ ├── rules/ + └── 01-接口契约.md │ └── 01-接口契约.md │ └── 01-接口契约.md + ← git submodule 引用同一份 → +``` + +--- + +## 返回格式(不变式) + +```json +// ✅ 唯一正确格式 +{ "code": 200, "message": "success", "data": { ... } } +{ "code": 500, "message": "错误原因", "data": null } + +// ❌ 永远不许出现 +return data; // 裸对象 +return "ok"; // 裸字符串 +{ "success": true, "result": {...} } // 自造格式 +``` + +--- + +## 分页格式(不变式) + +```json +{ "list": [...], "total": 100, "pages": 10, "current": 1, "size": 10 } +``` + +--- + +## 接口命名约定 + +| 前缀 | 用途 | 示例 | +|------|------|------| +| `/admin/**` | 管理后台 | `/admin/user/list` | +| `/app/**` | 移动端/H5/小程序 | `/app/task/list` | +| `/auth/**` | 登录注册(共享) | `/auth/login` | +| `/open/**` | 对外开放接口 | `/open/callback` | + +--- + +## 状态值映射表 + +**← 改这里 = 改协议,必须同步后端 + 所有前端。** + +| 含义 | 后端值 | 前端显示 | 前端 CSS 类 | +|------|--------|----------|------------| +| [状态1] | [值] | [文字] | [class] | +| [状态2] | [值] | [文字] | [class] | + +--- + +## 字段名桥接表 + +同一个概念在不同端字段名不同时,在此登记: + +| 概念 | 后端字段 | 前端A字段 | 前端B字段 | +|------|----------|-----------|-----------| +| | | | | + +--- + +## 契约同步铁律 + +``` +改后端接口 → grep 所有前端 → 逐个同步 → 两端都通过才算完成 + +步骤: +1. 改之前:grep -r "接口路径" 所有前端目录/ +2. 改后端 → 编译通过 +3. 逐个改前端 → 构建通过 +4. 更新本文件的映射表(如有新增状态值/字段差异) +``` diff --git a/.claude/database.md b/.claude/database.md new file mode 100644 index 0000000..e5aa79e --- /dev/null +++ b/.claude/database.md @@ -0,0 +1,175 @@ +# 04 — 数据库表设计规则 + +> **适用**:所有涉及建表、改表、索引设计的 AI 开发任务。 + +--- + +## 命名规则 + +| 元素 | 规则 | 示例(正确 → 错误) | +| -------- | ------------- | -------------------------------------------------------- | +| **表名** | 小写,下划线分隔,单数名词 | `task_apply` → ❌ `TaskApply` `task_applies` | +| **字段名** | 小写,下划线分隔 | `create_time` `task_id` → ❌ `createdAt` `taskId` | +| **主键** | 统一用 `id` | `id` → ❌ `task_id` `pk_id` | +| **外键** | `关联表名_id` | `task_id` `user_id` → ❌ `tid` `uid` | +| **布尔字段** | `del_flag` 或明确业务含义的 `is_` 前缀 | `del_flag` `is_active` → ❌ `deleted` `status` | +| **时间字段** | `_time` 后缀 | `create_time` `update_time` → ❌ `created_at` `updateTime` | +| **金额字段** | `_amount` 后缀 | `reward_amount` → ❌ `reward` `price` | +| **索引名** | `idx_表名_字段` | `idx_task_status` → ❌ `index1` `task_status_idx` | +| **唯一索引** | `uk_表名_字段` | `uk_user_phone` → ❌ `uq_phone` | +| **关联表** | 两表名用下划线连接 | `task_apply` `user_role` → ❌ `apply_task` | + +### 长度控制 + +表名超过 3 个单词或 30 个字符时缩写。前缀也要缩成一个单词。 + +| 完整 | 缩写 | JeecgBoot 实际案例 | +|------|------|-------------------| +| department | dept | `sys_depart_role_permission` → `sys_dept_role_perm` | +| permission | perm | 同上 | +| announcement | notice | `sys_announcement_send` → `sys_notice_send` | +| message | msg | `sys_message_template` → `sys_msg_template` | + +```sql +-- ❌ JeecgBoot 原版 — 太长 +sys_depart_role_permission -- 26 字符 +sys_permission_data_rule -- 24 字符 + +-- ✅ 缩写后 +sys_dept_role_perm -- 19 字符 +sys_perm_data_rule -- 20 字符 +``` + +--- + +## 字段类型规范 + +| 场景 | 类型 | 说明 | +|------|------|------| +| 主键 | `BIGINT` 自增 或 `VARCHAR(32)` | 推荐 BIGINT 自增;分布式用雪花ID | +| 短文本 | `VARCHAR(N)` | 姓名(50)、标题(200)、URL(500) | +| 长文本 | `TEXT` / `LONGTEXT` | 文章、JSON、描述 | +| 金额 | `DECIMAL(12,2)` | **禁止**用 FLOAT/DOUBLE | +| 状态/类型 | `VARCHAR(20)` 或 `TINYINT` | 枚举值,必须有注释说明 | +| 布尔 | `TINYINT(1)` | 0=否 1=是,注释必须写清含义 | +| 时间 | `DATETIME` | **禁止**用 TIMESTAMP(2038 问题) | +| 日期 | `DATE` | 生日、截止日期 | +| 数量 | `INT` 或 `BIGINT` | | + +--- + +## 每张表必须有的字段 + +```sql +CREATE TABLE xxx ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + -- [业务字段] + create_by VARCHAR(32) NULL DEFAULT NULL COMMENT '创建人', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(32) NULL DEFAULT NULL COMMENT '更新人', + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + del_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-正常 1-已删除', + INDEX idx_xxx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表说明'; +``` + +**强制要求**: +- `create_by` — 创建人,建议保留;系统自动填充时可为空 +- `create_time` — 创建时间,必须有 +- `update_by` — 更新人,建议保留;系统自动填充时可为空 +- `update_time` — 更新时间,必须有(`ON UPDATE CURRENT_TIMESTAMP` 自动更新) +- `del_flag` — 逻辑删除标记,除非明确不需要软删除 +- 每张表必须有 `COMMENT` +- 引擎统一 `InnoDB`,字符集 `utf8mb4` + +--- + +## 索引规则 + +| 规则 | 说明 | +|------|------| +| **主键** | 每张表必须有,推荐自增 BIGINT | +| **外键** | 必须加索引(MySQL 自动加,但显式声明更清晰) | +| **查询条件** | WHERE 条件字段必须加索引 | +| **排序字段** | ORDER BY 字段考虑加索引 | +| **联合索引** | 遵循最左前缀原则,区分度高的在前 | +| **唯一约束** | 业务唯一字段必须加唯一索引 | +| **禁止** | ❌ 不加索引的 WHERE / JOIN / ORDER BY | +| **禁止** | ❌ 在大字段(TEXT/BLOB)上建索引 | +| **禁止** | ❌ 过多索引(单表建议 ≤ 5 个) | + +### 索引命名 + +```sql +INDEX idx_表名_字段 -- 普通索引 +UNIQUE uk_表名_字段 -- 唯一索引 +INDEX idx_表名_字段1_字段2 -- 联合索引 +``` + +--- + +## 字段约束规则 + +| 规则 | 示例 | +|------|------| +| 主键 | `PRIMARY KEY` 或 `NOT NULL AUTO_INCREMENT` | +| 非空 | 业务必填字段 `NOT NULL` | +| 默认值 | 有默认值的字段 `DEFAULT xxx`,**禁止**依赖代码设默认值 | +| 唯一 | 业务唯一字段 `UNIQUE` | +| 外键 | 尽量用逻辑外键(代码维护),**不推荐**物理外键(`FOREIGN KEY`) | + +--- + +## 禁止清单 + +| ❌ 禁止 | 原因 | +|---------|------| +| 表名/字段名用大写或驼峰 | 跨平台兼容 | +| 金额用 FLOAT/DOUBLE | 精度丢失 | +| 时间用 TIMESTAMP | 2038 年溢出 | +| 用物理外键 | 分库分表/数据迁移困难 | +| 不加注释 | 无人知道字段含义 | +| 字符串代替布尔 | 空间浪费,索引效率低 | +| 大表无索引 | 性能灾难 | +| 字段用 NULL 代替默认值 | 查询需额外处理 `IS NULL` | +| 在代码里设默认值 | 数据一致性依赖应用层 | + +--- + +## 修改表规则 + +```sql +-- ✅ 正确:显式命名约束 +ALTER TABLE task ADD COLUMN priority TINYINT NOT NULL DEFAULT 0 COMMENT '优先级:0-普通 1-紧急'; +ALTER TABLE task ADD INDEX idx_task_priority (priority); + +-- ❌ 禁止:不写 COMMENT +ALTER TABLE task ADD COLUMN priority TINYINT; + +-- ❌ 禁止:不写默认值导致存量数据为 NULL +ALTER TABLE task ADD COLUMN priority TINYINT NOT NULL; +``` + +**修改表必须**: +- 新字段有 `COMMENT` +- 非空字段有 `DEFAULT` +- 考虑对存量数据的影响 +- 附带回滚 SQL + +--- + +## 检查表 + +每涉及建表/改表,逐项自检: + +- [ ] 表名、字段名全小写+下划线 +- [ ] 有 `create_by` `create_time` `update_by` `update_time` +- [ ] 软删除字段 `del_flag`(如需要) +- [ ] 每张表有 `COMMENT`,每个字段有 `COMMENT` +- [ ] 金额用 `DECIMAL`,时间用 `DATETIME` +- [ ] 主键 + 外键 + WHERE 条件字段有索引 +- [ ] 非空字段有 `NOT NULL DEFAULT` +- [ ] 无物理外键 +- [ ] 索引命名 `idx_表名_字段` / `uk_表名_字段` +- [ ] 附带回滚 SQL +- [ ] 引擎 InnoDB,字符集 utf8mb4 diff --git a/.claude/encoding.md b/.claude/encoding.md new file mode 100644 index 0000000..cdd62f7 --- /dev/null +++ b/.claude/encoding.md @@ -0,0 +1,44 @@ +# 编码规范(字符编码) + +> 本规则因一次事故而立:批量改包名时用了非 UTF-8 感知的脚本,把中文注释整成乱码(`鐗堟湰`=版本、`寰湇鍔`=微服务),且引入 BOM。以下为硬性约束。 + +## 不变式(违反即 Bug) + +1. **所有源码/配置/文本文件一律 UTF-8 编码**:`.java` `.xml` `.yml` `.yaml` `.properties` `.sql` `.md` `.vue` `.ts` `.js` `.json` 等。 +2. **禁止 BOM**:UTF-8 文件不得带 BOM(`EF BB BF`)。BOM 会让 XML/YAML 解析、shell 脚本、diff 出错。 +3. **禁止 GBK / GB2312 / GB18030 / Latin1 落盘**:任何环节(编辑器、脚本、终端重定向)都不得以非 UTF-8 写文件。 +4. **禁止用非 UTF-8 感知的工具批量改写文本**:如必须批量替换,工具必须显式按 UTF-8 读、UTF-8 写(Python 用 `io.open(p, encoding='utf-8')`;`sed`/`grep` 在 UTF-8 locale 下运行)。 +5. **换行**:统一 LF(`\n`),`.gitattributes` 固化,避免 CRLF 混入触发整文件 diff。 + +## 构建/工程配置(必须存在) + +- **Maven**:父 pom 设 + ```xml + + UTF-8 + UTF-8 + + ``` +- **`.editorconfig`**(后端仓库和前端仓库根目录各一个 `.editorconfig` 文件): + ``` + root = true + [*] + charset = utf-8 + end_of_line = lf + insert_final_newline = true + ``` +- **前端**:Vite/Node 默认 UTF-8,源文件保持 UTF-8 无 BOM 即可。 +- **Windows 终端**:PowerShell 默认输出可能是 GBK;**不要用 `>`/`>>` 重定向把含中文的内容写进源文件**(会按当前代码页编码)。要写文件用 UTF-8 感知的程序,不要靠 shell 重定向。 + +## 处理已损坏文件(mojibake 还原) + +经典双重编码(UTF-8 被当 GBK 再存 UTF-8)可逆转:`坏文本.encode('gbk').decode('utf-8')`。 +但若 mojibake 中已出现 `?`(0x3F),该字节已丢失、不可逆,只能按上下文重写注释。 + +## 检查表(提交前) + +- [ ] 新增/修改的文本文件是 UTF-8 无 BOM(`file *.xml` 或编辑器状态栏确认) +- [ ] 没有出现 `鐗堟湰`/`寰湇`/`锛` 这类乱码片段 +- [ ] 仓库根有 `.editorconfig`(charset=utf-8) +- [ ] 父 pom 有 `project.build.sourceEncoding=UTF-8` +- [ ] 批量文本替换用的是 UTF-8 感知工具,不是裸 `>` 重定向 diff --git a/.claude/frontend.md b/.claude/frontend.md new file mode 100644 index 0000000..05fdf58 --- /dev/null +++ b/.claude/frontend.md @@ -0,0 +1,199 @@ +# 03 — 前端规则 + +> **适用**:所有前端 AI 开发任务(Web / H5 / 小程序)。 + +--- + +## 样式铁律 + +**禁止在组件文件中硬编码**:颜色值(`#xxx` / `rgb()`)、字号(`px` / `rpx` / `rem`)、间距(`margin` / `padding` 数值)、圆角(`border-radius` 数值)。 + +所有样式必须来自: +1. **Design Token 文件** → 变量(`$color-primary`) +2. **全局 CSS 类** → 语义类(`.text-h1` `.card` `.gap-2`) +3. **UI 框架内置类** → Tailwind / Ant Design 等 + +```vue + + +``` + +--- + +## 禁止清单 + +| 类别 | ❌ 禁止 | +|------|--------| +| **样式** | 硬编码颜色/字号/间距/圆角 | +| **样式** | 在 scoped 中定义全局重置 | +| **样式** | scoped 超过 100 行 | +| **组件** | 自己写布局外壳(页头/侧栏/TabBar) | +| **组件** | 裸用 `` 代替图标组件 | +| **网络** | 直调 `fetch` / `axios` / `uni.request` | +| **网络** | 不处理错误态 | +| **状态** | 无加载态/空态 | + +--- + +## 检查表 + +每完成一个前端任务,逐项自检: + +- [ ] 使用项目统一布局组件 +- [ ] 无硬编码颜色/字号/间距/圆角 +- [ ] 图标全部走统一图标组件 +- [ ] API 调用走统一封装,无裸调 +- [ ] 页面/组件有加载态 + 空态 + 错误态 +- [ ] 关键操作有 loading 态(防重复点击) +- [ ] ` + + \ No newline at end of file diff --git a/test-base-core/src/main/resources/templates/email/bpm_cuiban_email.ftl b/test-base-core/src/main/resources/templates/email/bpm_cuiban_email.ftl new file mode 100644 index 0000000..0566477 --- /dev/null +++ b/test-base-core/src/main/resources/templates/email/bpm_cuiban_email.ftl @@ -0,0 +1,104 @@ + + + + + + +
+
+
【重要】流程办理的通知
+
+
+
+

+ 您好,您有一个新的流程任务亟待处理,任务内容如下:: +

+ + + + + + + + + + + + + + + + + + +
+ 流程名称 + + ${bpm_name}[立刻办理] +
+ 催办任务 + + ${bpm_task} +
+ 催办时间 + + ${datetime} +
+ 催办内容 + + ${remark} +
+
+ +
+
+ 温馨提醒 +
+
使用过程中如有任何问题,请联系系统管理员。
+
+
+
+

+ Copyright © 2023-2024 北京国炬信息技术有限公司. 保留所有权利。 +

+

+ 邮件由系统自动发送,请勿直接回复本邮件! +

+
+
+ + + + + \ No newline at end of file diff --git a/test-base-core/src/main/resources/templates/email/bpm_new_task_email.ftl b/test-base-core/src/main/resources/templates/email/bpm_new_task_email.ftl new file mode 100644 index 0000000..eadb60c --- /dev/null +++ b/test-base-core/src/main/resources/templates/email/bpm_new_task_email.ftl @@ -0,0 +1,101 @@ + + + + + + +
+
+ +
【重要】流程办理的通知
+
+
+
+

+ 您好, ${REALNAME},
您有一个新的流程任务需要处理,任务内容如下: +

+ + + + + + + + + + + + + + + + +
+ 业务标题 + + ${title} +
+ 流程名称 + + ${name} + [立刻办理] +
+ 任务节点 + + ${task} +
+
+ +
+
+ 温馨提醒 +
+
使用过程中如有任何问题,请联系系统管理员。
+
+
+
+

+ Copyright © 2023-2024 北京国炬信息技术有限公司. 保留所有权利。 +

+

+ 邮件由系统自动发送,请勿直接回复本邮件! +

+
+
+ + + + + \ No newline at end of file diff --git a/test-base-core/src/main/resources/templates/email/desform_new_data_email.ftl b/test-base-core/src/main/resources/templates/email/desform_new_data_email.ftl new file mode 100644 index 0000000..89a5070 --- /dev/null +++ b/test-base-core/src/main/resources/templates/email/desform_new_data_email.ftl @@ -0,0 +1,78 @@ + + + + + + +
+
+ +
+ 【重要】新数据提醒 +
+
+
+
+

+ 尊敬的 ${userName} 用户,您好: +

+ 你的表单 【${formName}】 + 在 ${createTime} 新增了1条数据。 + + ${dataMarkdown} + +

+ 如需查看更多请点击 + [查看所有数据] +

+
+ + +
+
+

+ Copyright © 2023-2024 北京敲敲云科技有限公司. 保留所有权利。 +

+

+ 邮件由系统自动发送,请勿直接回复本邮件! +

+
+
+ + + + + \ No newline at end of file diff --git a/test-base-core/src/test/java/com/ghb/base/test/sqlparse/TestIpUtil.java b/test-base-core/src/test/java/com/ghb/base/test/sqlparse/TestIpUtil.java new file mode 100644 index 0000000..afa7e72 --- /dev/null +++ b/test-base-core/src/test/java/com/ghb/base/test/sqlparse/TestIpUtil.java @@ -0,0 +1,36 @@ +package com.ghb.base.test.sqlparse; + +import com.ghb.base.common.util.IpUtils; +import com.ghb.base.common.util.oConvertUtils; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author: scott + * @date: 2024年04月29日 16:48 + */ +public class TestIpUtil { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put("key1", new String[]{"value1", "value2", "value3"}); + map.put("key4", null); + map.put("key2", new String[]{"value4", "value5"}); + map.put("key3", new String[]{"value6"}); + System.out.println(oConvertUtils.mapToString(map)); + } + + @Test + public void test() { + String ip = "2408:8207:1851:10e0:50bd:1a50:60c8:b030, 115.231.101.180"; + String[] ipAddresses = ip.split(","); + for (String ipAddress : ipAddresses) { + System.out.println(ipAddress); + ipAddress = ipAddress.trim(); + if (IpUtils.isValidIpAddress(ipAddress)) { + System.out.println("ipAddress= " + ipAddress); + } + } + } +} diff --git a/test-module-business/pom.xml b/test-module-business/pom.xml new file mode 100644 index 0000000..7574682 --- /dev/null +++ b/test-module-business/pom.xml @@ -0,0 +1,24 @@ + + + + com.ghb + test-base-parent + 3.9.2 + + 4.0.0 + test-module-business + test-module-business + 业务模块 — 你的代码放这里 + + + com.ghb + test-base-core + + + com.ghb + test-system-local-api + + + diff --git a/test-module-business/src/main/java/com/ghb/base/business/controller/BusinessTestController.java b/test-module-business/src/main/java/com/ghb/base/business/controller/BusinessTestController.java new file mode 100644 index 0000000..233f6e2 --- /dev/null +++ b/test-module-business/src/main/java/com/ghb/base/business/controller/BusinessTestController.java @@ -0,0 +1,84 @@ +package com.ghb.base.business.controller; + +import com.ghb.base.common.api.vo.Result; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 业务模块测试接口,用于验证 ghb-module-business 已被单体和微服务启动包加载。 + */ +@RestController +@RequestMapping("/business/test") +public class BusinessTestController { + + /** + * 查询业务模块测试状态。 + * + * @return 业务模块加载状态 + */ + @GetMapping + public Result> getStatus() { + Map data = new LinkedHashMap<>(); + data.put("module", "ghb-module-business"); + data.put("method", "GET"); + data.put("status", "loaded"); + return Result.ok(data); + } + + /** + * 创建业务模块测试数据。 + * + * @param requestBody 请求内容 + * @return 回显后的测试数据 + */ + @PostMapping + public Result> create(@RequestBody(required = false) Map requestBody) { + return Result.ok(buildEcho("POST", null, requestBody)); + } + + /** + * 更新业务模块测试数据。 + * + * @param id 测试资源 ID + * @param requestBody 请求内容 + * @return 回显后的测试数据 + */ + @PutMapping("/{id}") + public Result> update(@PathVariable String id, + @RequestBody(required = false) Map requestBody) { + return Result.ok(buildEcho("PUT", id, requestBody)); + } + + /** + * 删除业务模块测试数据。 + * + * @param id 测试资源 ID + * @return 删除结果 + */ + @DeleteMapping("/{id}") + public Result> delete(@PathVariable String id) { + return Result.ok(buildEcho("DELETE", id, null)); + } + + private Map buildEcho(String method, String id, Map requestBody) { + Map data = new LinkedHashMap<>(); + data.put("module", "ghb-module-business"); + data.put("method", method); + if (id != null) { + data.put("id", id); + } + if (requestBody != null) { + data.put("body", requestBody); + } + return data; + } +} diff --git a/test-module-system/pom.xml b/test-module-system/pom.xml new file mode 100644 index 0000000..394ee8b --- /dev/null +++ b/test-module-system/pom.xml @@ -0,0 +1,21 @@ + + + + test-base-parent + com.ghb + 3.9.2 + + 4.0.0 + + test-module-system + pom + + + test-system-api + test-system-biz + test-system-start + + + \ No newline at end of file diff --git a/test-module-system/test-system-api/pom.xml b/test-module-system/test-system-api/pom.xml new file mode 100644 index 0000000..7a8a781 --- /dev/null +++ b/test-module-system/test-system-api/pom.xml @@ -0,0 +1,26 @@ + + + + test-module-system + com.ghb + 3.9.2 + + 4.0.0 + + test-system-api + pom + + + test-system-local-api + test-system-cloud-api + + + + + com.ghb + test-base-core + + + \ No newline at end of file diff --git a/test-module-system/test-system-api/test-system-cloud-api/pom.xml b/test-module-system/test-system-api/test-system-cloud-api/pom.xml new file mode 100644 index 0000000..32372d3 --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/pom.xml @@ -0,0 +1,21 @@ + + + + test-system-api + com.ghb + 3.9.2 + + 4.0.0 + + test-system-cloud-api + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + \ No newline at end of file diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/IAiragBaseApi.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/IAiragBaseApi.java new file mode 100644 index 0000000..d9748ea --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/IAiragBaseApi.java @@ -0,0 +1,74 @@ +package com.ghb.base.common.airag.api; + +import com.ghb.base.common.airag.api.fallback.AiragBaseApiFallback; +import com.ghb.base.common.constant.ServiceNameConstants; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * airag baseAPI + * + * @author sjlei + * @date 2025-12-30 + */ +@Component +@FeignClient(contextId = "airagBaseRemoteApi", value = ServiceNameConstants.SERVICE_SYSTEM, fallbackFactory = AiragBaseApiFallback.class) +@ConditionalOnMissingClass("com.ghb.base.modules.airag.llm.service.impl.AiragBaseApiImpl") +public interface IAiragBaseApi { + + /** + * 知识库写入文本文档(支持自定义分段策略) + * + * @param knowledgeId 知识库ID + * @param title 文档标题 + * @param content 文档内容 + * @param segmentConfig 【可选】分段策略配置JSON + * @return 新增的文档ID + * @author sjlei + * @date 2025-12-30 + */ + @PostMapping("/airag/api/knowledgeWriteTextDocument") + String knowledgeWriteTextDocument( + @RequestParam("knowledgeId") String knowledgeId, + @RequestParam("title") String title, + @RequestParam("content") String content, + @RequestParam(value = "segmentConfig", required = false) String segmentConfig + ); + + /** + * 读取会话变量 + */ + @PostMapping("/airag/api/getChatVariable") + String getChatVariable( + @RequestParam("appId") String appId, + @RequestParam("username") String username, + @RequestParam("name") String name + ); + + /** + * 设置会话变量 + */ + @PostMapping("/airag/api/setChatVariable") + void setChatVariable( + @RequestParam("appId") String appId, + @RequestParam("username") String username, + @RequestParam("name") String name, + @RequestParam("value") String value + ); + + /** + * 根据应用ID查询记忆库ID + */ + @PostMapping("/airag/api/getMemoryIdByAppId") + String getMemoryIdByAppId(@RequestParam("appId") String appId); + + /** + * 根据提示词ID查询提示词内容 + */ + @PostMapping("/airag/api/getPromptContent") + String getPromptContent(@RequestParam("promptId") String promptId); + +} diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/factory/AiragBaseApiFallbackFactory.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/factory/AiragBaseApiFallbackFactory.java new file mode 100644 index 0000000..7789b5e --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/factory/AiragBaseApiFallbackFactory.java @@ -0,0 +1,18 @@ +package com.ghb.base.common.airag.api.factory; + +import com.ghb.base.common.airag.api.IAiragBaseApi; +import com.ghb.base.common.airag.api.fallback.AiragBaseApiFallback; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +@Component +public class AiragBaseApiFallbackFactory implements FallbackFactory { + + @Override + public IAiragBaseApi create(Throwable cause) { + AiragBaseApiFallback fallback = new AiragBaseApiFallback(); + fallback.setCause(cause); + return fallback; + } + +} diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/fallback/AiragBaseApiFallback.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/fallback/AiragBaseApiFallback.java new file mode 100644 index 0000000..491ba8f --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/airag/api/fallback/AiragBaseApiFallback.java @@ -0,0 +1,35 @@ +package com.ghb.base.common.airag.api.fallback; + +import lombok.Setter; +import com.ghb.base.common.airag.api.IAiragBaseApi; + +public class AiragBaseApiFallback implements IAiragBaseApi { + + @Setter + private Throwable cause; + + @Override + public String knowledgeWriteTextDocument(String knowledgeId, String title, String content, String segmentConfig) { + return null; + } + + @Override + public String getChatVariable(String appId, String username, String name) { + return null; + } + + @Override + public void setChatVariable(String appId, String username, String name, String value) { + } + + @Override + public String getMemoryIdByAppId(String appId) { + return null; + } + + @Override + public String getPromptContent(String promptId) { + return null; + } + +} diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/IOnlineBaseExtApi.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/IOnlineBaseExtApi.java new file mode 100644 index 0000000..4065e96 --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/IOnlineBaseExtApi.java @@ -0,0 +1,89 @@ +package com.ghb.base.common.online.api; + +import com.alibaba.fastjson.JSONObject; +import com.ghb.base.common.constant.ServiceNameConstants; +import com.ghb.base.common.online.api.factory.OnlineBaseExtApiFallbackFactory; +import com.ghb.base.common.system.vo.DictModel; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * @Description: 【Online】online表单对外 Feign API接口 + * + * @ConditionalOnMissingClass("com.ghb.base.modules.online.cgform.service.impl.OnlineBaseExtApiImpl") => 有实现类的时候,不实例化Feign接口 + * @author: Ghb-boot + */ +@Component +//@FeignClient(contextId = "onlineBaseRemoteApi", value = ServiceNameConstants.SERVICE_ONLINE, fallbackFactory = OnlineBaseExtApiFallbackFactory.class) +@FeignClient(contextId = "onlineBaseRemoteApi", value = ServiceNameConstants.SERVICE_SYSTEM, fallbackFactory = OnlineBaseExtApiFallbackFactory.class) +@ConditionalOnMissingClass("com.ghb.base.modules.online.cgform.service.impl.OnlineBaseExtApiImpl") +public interface IOnlineBaseExtApi { + + /** + * 【Online】 表单设计器专用:同步新增 + * @param tableName 表名 + * @param jsonObject + * @throws Exception + * @return String + */ + @PostMapping(value = "/online/api/cgform/crazyForm/{name}") + String cgformPostCrazyForm(@PathVariable("name") String tableName, @RequestBody JSONObject jsonObject) throws Exception; + + /** + * 【Online】 表单设计器专用:同步编辑 + * @param tableName 表名 + * @param jsonObject + * @throws Exception + * @return String + */ + @PutMapping(value = "/online/api/cgform/crazyForm/{name}") + String cgformPutCrazyForm(@PathVariable("name") String tableName, @RequestBody JSONObject jsonObject) throws Exception; + + /** + * 通过online表名查询数据,同时查询出子表的数据 + * + * @param tableName online表名 + * @param dataIds online数据ID + * @return + */ + @GetMapping(value = "/online/api/cgform/queryAllDataByTableName") + JSONObject cgformQueryAllDataByTableName(@RequestParam("tableName") String tableName, @RequestParam("dataIds") String dataIds); + + /** + * online表单删除数据 + * + * @param cgformCode Online表单code + * @param dataIds 数据ID,可逗号分割 + * @return + */ + @DeleteMapping("/online/api/cgform/cgformDeleteDataByCode") + String cgformDeleteDataByCode(@RequestParam("cgformCode") String cgformCode, @RequestParam("dataIds") String dataIds); + + /** + * 【cgreport】通过 head code 获取 sql语句,并执行该语句返回查询数据 + * + * @param code 报表Code,如果没传ID就通过code查 + * @param forceKey + * @param dataList + * @return + */ + @GetMapping("/online/api/cgreportGetData") + Map cgreportGetData(@RequestParam("code") String code, @RequestParam("forceKey") String forceKey, @RequestParam("dataList") String dataList); + + /** + * 【cgreport】对 cgreportGetData 的返回值做优化,封装 DictModel 集合 + * @param code + * @param dictText + * @param dictCode + * @param dataList + * @return + */ + @GetMapping("/online/api/cgreportGetDataPackage") + List cgreportGetDataPackage(@RequestParam("code") String code, @RequestParam("dictText") String dictText, @RequestParam("dictCode") String dictCode, @RequestParam("dataList") String dataList); + +} diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/factory/OnlineBaseExtApiFallbackFactory.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/factory/OnlineBaseExtApiFallbackFactory.java new file mode 100644 index 0000000..2ec3ed7 --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/factory/OnlineBaseExtApiFallbackFactory.java @@ -0,0 +1,21 @@ +package com.ghb.base.common.online.api.factory; + +import com.ghb.base.common.online.api.IOnlineBaseExtApi; +import com.ghb.base.common.online.api.fallback.OnlineBaseExtApiFallback; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * @Description: OnlineBaseExtAPIFallbackFactory + * @author: Ghb-boot + */ +@Component +public class OnlineBaseExtApiFallbackFactory implements FallbackFactory { + + @Override + public IOnlineBaseExtApi create(Throwable throwable) { + OnlineBaseExtApiFallback fallback = new OnlineBaseExtApiFallback(); + fallback.setCause(throwable); + return fallback; + } +} \ No newline at end of file diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/fallback/OnlineBaseExtApiFallback.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/fallback/OnlineBaseExtApiFallback.java new file mode 100644 index 0000000..4fd06d9 --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/online/api/fallback/OnlineBaseExtApiFallback.java @@ -0,0 +1,52 @@ +package com.ghb.base.common.online.api.fallback; + +import com.alibaba.fastjson.JSONObject; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.online.api.IOnlineBaseExtApi; +import com.ghb.base.common.system.vo.DictModel; + +import java.util.List; +import java.util.Map; + +/** + * 进入fallback的方法 检查是否token未设置 + * @author: Ghb-boot + */ +@Slf4j +public class OnlineBaseExtApiFallback implements IOnlineBaseExtApi { + + @Setter + private Throwable cause; + + @Override + public String cgformPostCrazyForm(String tableName, JSONObject jsonObject) { + return null; + } + + @Override + public String cgformPutCrazyForm(String tableName, JSONObject jsonObject) { + return null; + } + + @Override + public JSONObject cgformQueryAllDataByTableName(String tableName, String dataIds) { + return null; + } + + @Override + public String cgformDeleteDataByCode(String cgformCode, String dataIds) { + return null; + } + + @Override + public Map cgreportGetData(String code, String forceKey, String dataList) { + return null; + } + + @Override + public List cgreportGetDataPackage(String code, String dictText, String dictCode, String dataList) { + return null; + } + +} diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/ISysBaseAPI.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/ISysBaseAPI.java new file mode 100644 index 0000000..b78ead7 --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/ISysBaseAPI.java @@ -0,0 +1,913 @@ +package com.ghb.base.common.system.api; + +import com.alibaba.fastjson.JSONObject; +import com.ghb.base.common.api.CommonAPI; +import com.ghb.base.common.api.dto.DataLogDTO; +import com.ghb.base.common.api.dto.OnlineAuthDTO; +import com.ghb.base.common.api.dto.PushMessageDTO; +import com.ghb.base.common.api.dto.message.*; +import com.ghb.base.common.constant.ServiceNameConstants; +import com.ghb.base.common.constant.enums.DySmsEnum; +import com.ghb.base.common.constant.enums.EmailTemplateEnum; +import com.ghb.base.common.desensitization.annotation.SensitiveDecode; +import com.ghb.base.common.system.api.factory.SysBaseAPIFallbackFactory; +import com.ghb.base.common.system.vo.*; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * 1、cloud接口数量43 local:35 common:9 额外一个特殊queryAllRole一个当两个用 + * - 相比较local版 + * - 去掉了一些方法:addLog、getDatabaseType、queryAllDepart、queryAllUser(Wrapper wrapper)、queryAllUser(String[] userIds, int pageNo, int pageSize) + * - 修改了一些方法:createLog、sendSysAnnouncement(只保留了一个,其余全部干掉) + * 2、@ConditionalOnMissingClass("com.ghb.base.modules.system.service.impl.SysBaseApiImpl")=> 有实现类的时候,不实例化Feign接口 + * @author: Ghb-boot + */ +@Component +@FeignClient(contextId = "sysBaseRemoteApi", value = ServiceNameConstants.SERVICE_SYSTEM, fallbackFactory = SysBaseAPIFallbackFactory.class) +@ConditionalOnMissingClass("com.ghb.base.modules.system.service.impl.SysBaseApiImpl") +public interface ISysBaseAPI extends CommonAPI { + + /** + * 1发送系统消息 + * @param message 使用构造器赋值参数 如果不设置category(消息类型)则默认为2 发送系统消息 + */ + @PostMapping("/sys/api/sendSysAnnouncement") + void sendSysAnnouncement(@RequestBody MessageDTO message); + + /** + * 2发送消息 附带业务参数 + * @param message 使用构造器赋值参数 + */ + @PostMapping("/sys/api/sendBusAnnouncement") + void sendBusAnnouncement(@RequestBody BusMessageDTO message); + + /** + * 3通过模板发送消息 + * @param message 使用构造器赋值参数 + */ + @PostMapping("/sys/api/sendTemplateAnnouncement") + void sendTemplateAnnouncement(@RequestBody TemplateMessageDTO message); + + /** + * 4通过模板发送消息 附带业务参数 + * @param message 使用构造器赋值参数 + */ + @PostMapping("/sys/api/sendBusTemplateAnnouncement") + void sendBusTemplateAnnouncement(@RequestBody BusTemplateMessageDTO message); + + /** + * 5通过消息中心模板,生成推送内容 + * @param templateDTO 使用构造器赋值参数 + * @return + */ + @PostMapping("/sys/api/parseTemplateByCode") + String parseTemplateByCode(@RequestBody TemplateDTO templateDTO); + + /** + * 6根据用户id查询用户信息 + * @param id + * @return + */ + @SensitiveDecode + @GetMapping("/sys/api/getUserById") + LoginUser getUserById(@RequestParam("id") String id); + + /** + * 7通过用户账号查询角色集合 + * @param username + * @return + */ + @GetMapping("/sys/api/getRolesByUsername") + List getRolesByUsername(@RequestParam("username") String username); + + /** + * 7通过用户账号查询角色集合 + * @param userId + * @return + */ + @GetMapping("/sys/api/getRolesByUserId") + List getRolesByUserId(@RequestParam("userId") String userId); + + /** + * 8通过用户账号查询部门集合 + * @param username + * @return 部门 id + */ + @GetMapping("/sys/api/getDepartIdsByUsername") + List getDepartIdsByUsername(@RequestParam("username") String username); + + /** + * 8通过用户账号查询部门集合 + * @param userId + * @return 部门 id + */ + @GetMapping("/sys/api/getDepartIdsByUserId") + List getDepartIdsByUserId(@RequestParam("userId") String userId); + + /** + * 8.2 通过用户账号查询部门父ID集合 + * @param username + * @return 部门 parentIds + */ + @GetMapping("/sys/api/getDepartParentIdsByUsername") + Set getDepartParentIdsByUsername(@RequestParam("username")String username); + + /** + * 8.3 查询部门父ID集合 + * @param depIds + * @return 部门 parentIds + */ + @GetMapping("/sys/api/getDepartParentIdsByDepIds") + Set getDepartParentIdsByDepIds(@RequestParam("depIds") Set depIds); + + /** + * 8.4 通过 userIds 查询部门ID列表 + * + * @param userIds + * @return key = userId; value = 用户拥有的部门ID列表 + */ + @GetMapping("/sys/api/getDepartIdsByUserIds") + Map> getDepartIdsByUserIds(@RequestParam("userIds") Collection userIds); + + /** + * 9通过用户账号查询部门 name + * @param username + * @return 部门 name + */ + @GetMapping("/sys/api/getDepartNamesByUsername") + List getDepartNamesByUsername(@RequestParam("username") String username); + + /** + * 10获取数据字典 + * @param code + * @return + */ + @Override + @GetMapping("/sys/api/queryDictItemsByCode") + List queryDictItemsByCode(@RequestParam("code") String code); + + /** + * 获取有效的数据字典项 + * @param code + * @return + */ + @Override + @GetMapping("/sys/api/queryEnableDictItemsByCode") + public List queryEnableDictItemsByCode(@RequestParam("code") String code); + + /** 11查询所有的父级字典,按照create_time排序 + * @return List 字典值集合 + */ + @GetMapping("/sys/api/queryAllDict") + List queryAllDict(); + + /** + * 12查询所有分类字典 + * @return + */ + @GetMapping("/sys/api/queryAllSysCategory") + List queryAllSysCategory(); + + /** + * 13获取表数据字典 + * @param tableFilterSql + * @param text + * @param code + * @return + */ + @Override + @GetMapping("/sys/api/queryTableDictItemsByCode") + List queryTableDictItemsByCode(@RequestParam("tableFilterSql") String tableFilterSql, @RequestParam("text") String text, @RequestParam("code") String code); + + /** + * 14查询所有部门 作为字典信息 id -->value,departName -->text + * @return + */ + @GetMapping("/sys/api/queryAllDepartBackDictModel") + List queryAllDepartBackDictModel(); + + /** + * 15根据业务类型 busType 及业务 busId 修改消息已读 + * @param busType 业务类型 + * @param busId 业务id + */ + @GetMapping("/sys/api/updateSysAnnounReadFlag") + public void updateSysAnnounReadFlag(@RequestParam("busType") String busType, @RequestParam("busId")String busId); + + /** + * 16查询表字典 支持过滤数据 + * @param table + * @param text + * @param code + * @param filterSql + * @return + */ + @GetMapping("/sys/api/queryFilterTableDictInfo") + List queryFilterTableDictInfo(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("filterSql") String filterSql); + + /** + * 17查询指定table的 text code 获取字典,包含text和value + * @param table + * @param text + * @param code + * @param keyArray + * @return + */ + @Deprecated + @GetMapping("/sys/api/queryTableDictByKeys") + public List queryTableDictByKeys(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("keyArray") String[] keyArray); + + /** + * 18查询所有用户 返回ComboModel + * @return + */ + @GetMapping("/sys/api/queryAllUserBackCombo") + public List queryAllUserBackCombo(); + + /** + * 19分页查询用户 返回JSONObject + * @param userIds 多个用户id + * @param pageNo 当前页数 + * @param pageSize 每页条数 + * @return + */ + @GetMapping("/sys/api/queryAllUser") + public JSONObject queryAllUser(@RequestParam(name="userIds",required=false)String userIds, @RequestParam(name="pageNo",required=false) Integer pageNo,@RequestParam(name="pageSize",required=false) Integer pageSize); + + + /** + * 20获取所有角色 带参 + * @param roleIds 默认选中角色 + * @return + */ + @GetMapping("/sys/api/queryAllRole") + public List queryAllRole(@RequestParam(name = "roleIds",required = false)String[] roleIds); + + /** + * 21通过用户账号查询角色Id集合 + * @param username + * @return + */ + @GetMapping("/sys/api/getRoleIdsByUsername") + public List getRoleIdsByUsername(@RequestParam("username")String username); + + /** + * 22通过部门编号查询部门id + * @param orgCode + * @return + */ + @GetMapping("/sys/api/getDepartIdsByOrgCode") + public String getDepartIdsByOrgCode(@RequestParam("orgCode")String orgCode); + + /** + * 23查询所有部门 + * @return + */ + @GetMapping("/sys/api/getAllSysDepart") + public List getAllSysDepart(); + + /** + * 24查找父级部门 + * @param departId + * @return + */ + @GetMapping("/sys/api/getParentDepartId") + DictModel getParentDepartId(@RequestParam("departId")String departId); + + /** + * 25根据部门Id获取部门负责人 + * @param deptId + * @return + */ + @GetMapping("/sys/api/getDeptHeadByDepId") + public List getDeptHeadByDepId(@RequestParam("deptId") String deptId); + + /** + * 26给指定用户发消息 + * @param userIds + * @param cmd + */ + @GetMapping("/sys/api/sendWebSocketMsg") + public void sendWebSocketMsg(@RequestParam("userIds")String[] userIds, @RequestParam("cmd") String cmd); + + /** + * 27根据id获取所有参与用户 + * @param userIds 多个用户id + * @return + */ + @GetMapping("/sys/api/queryAllUserByIds") + public List queryAllUserByIds(@RequestParam("userIds") String[] userIds); + + /** + * 28将会议签到信息推动到预览 + * userIds + * @return + * @param userId + */ + @GetMapping("/sys/api/meetingSignWebsocket") + void meetingSignWebsocket(@RequestParam("userId")String userId); + + /** + * 29根据name获取所有参与用户 + * @param userNames 多个用户账号 + * @return + */ + @GetMapping("/sys/api/queryUserByNames") + List queryUserByNames(@RequestParam("userNames")String[] userNames); + + + /** + * 30获取用户的角色集合 + * @param username + * @return + */ + @GetMapping("/sys/api/getUserRoleSet") + Set getUserRoleSet(@RequestParam("username")String username); + + /** + * 30获取用户的角色集合 + * @param userId + * @return + */ + @GetMapping("/sys/api/getUserRoleSetById") + Set getUserRoleSetById(@RequestParam("userId")String userId); + + /** + * 31获取用户的权限集合 + * @param userId + * @return + */ + @GetMapping("/sys/api/getUserPermissionSet") + Set getUserPermissionSet(@RequestParam("userId") String userId); + + /** + * 32判断是否有online访问的权限 + * @param onlineAuthDTO + * @return + */ + @PostMapping("/sys/api/hasOnlineAuth") + boolean hasOnlineAuth(@RequestBody OnlineAuthDTO onlineAuthDTO); + + /** + * 33通过部门id获取部门全部信息 + * @param id 部门id + * @return SysDepartModel 部门信息 + */ + @GetMapping("/sys/api/selectAllById") + SysDepartModel selectAllById(@RequestParam("id") String id); + + /** + * 34根据用户id查询用户所属公司下所有用户ids + * @param userId + * @return + */ + @GetMapping("/sys/api/queryDeptUsersByUserId") + List queryDeptUsersByUserId(@RequestParam("userId") String userId); + + + //--- + + /** + * 35查询用户角色信息 + * @param username + * @return + */ + @Override + @GetMapping("/sys/api/queryUserRoles") + Set queryUserRoles(@RequestParam("username")String username); + + /** + * 35查询用户角色信息 + * @param userId + * @return + */ + @Override + @GetMapping("/sys/api/queryUserRolesById") + Set queryUserRolesById(@RequestParam("userId")String userId); + + /** + * 36查询用户权限信息 + * @param userId + * @return + */ + @Override + @GetMapping("/sys/api/queryUserAuths") + Set queryUserAuths(@RequestParam("userId")String userId); + + /** + * 37根据 id 查询数据库中存储的 DynamicDataSourceModel + * + * @param dbSourceId + * @return + */ + @Override + @GetMapping("/sys/api/getDynamicDbSourceById") + DynamicDataSourceModel getDynamicDbSourceById(@RequestParam("dbSourceId") String dbSourceId); + + /** + * 38根据 code 查询数据库中存储的 DynamicDataSourceModel + * + * @param dbSourceCode + * @return + */ + @Override + @GetMapping("/sys/api/getDynamicDbSourceByCode") + DynamicDataSourceModel getDynamicDbSourceByCode(@RequestParam("dbSourceCode") String dbSourceCode); + + /** + * 39根据用户账号查询用户信息 CommonAPI中定义 + * @param username + * @return LoginUser 用户信息 + */ + @Override + @SensitiveDecode + @GetMapping("/sys/api/getUserByName") + LoginUser getUserByName(@RequestParam("username") String username); + + /** + * 39根据用户账号查询用户ID CommonAPI中定义 + * @param username + * @return 用户ID + */ + @Override + @GetMapping("/sys/api/getUserIdByName") + String getUserIdByName(@RequestParam("username") String username); + + /** + * 40字典表的 翻译 + * @param table + * @param text + * @param code + * @param key + * @return + */ + @Override + @GetMapping("/sys/api/translateDictFromTable") + String translateDictFromTable(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("key") String key); + + /** + * 41普通字典的翻译 + * @param code + * @param key + * @return + */ + @Override + @GetMapping("/sys/api/translateDict") + String translateDict(@RequestParam("code") String code, @RequestParam("key") String key); + + /** + * 42查询数据权限 + * @param component + * @param requestPath + * @param username 用户姓名 + * @return + */ + @Override + @GetMapping("/sys/api/queryPermissionDataRule") + List queryPermissionDataRule(@RequestParam("component") String component, @RequestParam("requestPath")String requestPath, @RequestParam("username") String username); + + /** + * 43查询用户信息 + * @param username + * @return + */ + @Override + @GetMapping("/sys/api/getCacheUser") + SysUserCacheInfo getCacheUser(@RequestParam("username") String username); + + /** + * 36根据多个用户账号(逗号分隔),查询返回多个用户信息 + * @param usernames + * @return + */ + @GetMapping("/sys/api/queryUsersByUsernames") + List queryUsersByUsernames(@RequestParam("usernames") String usernames); + + /** + * 37根据多个用户ID(逗号分隔),查询返回多个用户信息 + * @param ids + * @return + */ + @RequestMapping("/sys/api/queryUsersByIds") + List queryUsersByIds(@RequestParam("ids") String ids); + + /** + * 38根据多个部门编码(逗号分隔),查询返回多个部门信息 + * @param orgCodes + * @return + */ + @RequestMapping("/sys/api/queryDepartsByOrgcodes") + List queryDepartsByOrgcodes(@RequestParam("orgCodes") String orgCodes); + +// /** +// * 39根据多个部门编码(逗号分隔),查询返回多个部门信息 +// * @param ids +// * @return +// */ +// @GetMapping("/sys/api/queryDepartsByOrgIds") +// List queryDepartsByOrgIds(@RequestParam("ids") String ids); + + /** + * 40发送邮件消息 + * @param email + * @param title + * @param content + */ + @GetMapping("/sys/api/sendEmailMsg") + void sendEmailMsg(@RequestParam("email")String email,@RequestParam("title")String title,@RequestParam("content")String content); + + /** + * 发送html模版邮件消息 + * + * @param email + * @param title + * @param emailTemplateEnum 邮件模版枚举 + * @param params 模版参数 + */ + @GetMapping("/sys/api/sendHtmlTemplateEmail") + void sendHtmlTemplateEmail(@RequestParam("email") String email, @RequestParam("title") String title, @RequestParam("emailEnum") EmailTemplateEnum emailTemplateEnum, @RequestParam("params") JSONObject params); + /** + /** + * 发送短信消息 + * + * @param phone 手机号码 + * @param params 模版参数 + * @param dySmsEnum 短信模版枚举 + */ + @GetMapping("/sys/api/sendSmsMsg") + void sendSmsMsg(@RequestParam("phone") String phone, @RequestParam("params") JSONObject params,@RequestParam("dySmsEnum") DySmsEnum dySmsEnum); + /** + * 41 获取公司下级部门和公司下所有用户id + * @param orgCode 部门编号 + * @return List + */ + @GetMapping("/sys/api/getDeptUserByOrgCode") + List getDeptUserByOrgCode(@RequestParam("orgCode")String orgCode); + + /** + * 42 查询分类字典翻译 + * @param ids 多个分类字典id + * @return List + */ + @GetMapping("/sys/api/loadCategoryDictItem") + List loadCategoryDictItem(@RequestParam("ids") String ids); + + /** + * 44 反向翻译分类字典,用于导入 + * + * @param names 名称,逗号分割 + */ + @GetMapping("/sys/api/loadCategoryDictItemByNames") + List loadCategoryDictItemByNames(@RequestParam("names") String names, @RequestParam("delNotExist") boolean delNotExist); + + /** + * 43 根据字典code加载字典text + * + * @param dictCode 顺序:tableName,text,code + * @param keys 要查询的key + * @return + */ + @GetMapping("/sys/api/loadDictItem") + List loadDictItem(@RequestParam("dictCode") String dictCode, @RequestParam("keys") String keys); + + /** + * 复制应用下的所有字典配置到新的租户下 + * + * @param originalAppId 原始低代码应用ID + * @param appId 新的低代码应用ID + * @param tenantId 新的租户ID + * @return Map Map<原字典编码, 新字典编码> + */ + @GetMapping("/sys/api/copyLowAppDict") + Map copyLowAppDict(@RequestParam("originalAppId") String originalAppId, @RequestParam("appId") String appId, @RequestParam("tenantId") String tenantId); + + /** + * 44 根据字典code查询字典项 + * + * @param dictCode 顺序:tableName,text,code + * @param dictCode 要查询的key + * @return + */ + @GetMapping("/sys/api/getDictItems") + List getDictItems(@RequestParam("dictCode") String dictCode); + + /** + * 45 根据多个字典code查询多个字典项 + * + * @param dictCodeList + * @return key = dictCode ; value=对应的字典项 + */ + @RequestMapping("/sys/api/getManyDictItems") + Map> getManyDictItems(@RequestParam("dictCodeList") List dictCodeList); + + /** + * 46 【JSearchSelectTag下拉搜索组件专用接口】 + * 大数据量的字典表 走异步加载 即前端输入内容过滤数据 + * + * @param dictCode 字典code格式:table,text,code + * @param keyword 过滤关键字 + * @param pageSize 每页条数 + * @return + */ + @GetMapping("/sys/api/loadDictItemByKeyword") + List loadDictItemByKeyword(@RequestParam("dictCode") String dictCode, @RequestParam("keyword") String keyword, @RequestParam(value = "pageNo", defaultValue = "1", required = false) Integer pageNo, @RequestParam(value = "pageSize", required = false) Integer pageSize); + + /** + * 47 根据多个部门id(逗号分隔),查询返回多个部门信息 + * @param ids + * @return + */ + @GetMapping("/sys/api/queryDepartsByIds") + List queryDepartsByIds(@RequestParam("ids") String ids); + + /** + * 48 普通字典的翻译,根据多个dictCode和多条数据,多个以逗号分割 + * @param dictCodes + * @param keys + * @return + */ + @Override + @GetMapping("/sys/api/translateManyDict") + Map> translateManyDict(@RequestParam("dictCodes") String dictCodes, @RequestParam("keys") String keys); + + /** + * 49 字典表的 翻译,可批量 + * @param table + * @param text + * @param code + * @param keys 多个用逗号分割 + * @param ds + * @return + */ + @Override + @GetMapping("/sys/api/translateDictFromTableByKeys") + List translateDictFromTableByKeys(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("keys") String keys, @RequestParam("ds") String ds); + + /** + * 发送模板消息 + */ + @PostMapping("/sys/api/sendTemplateMessage") + void sendTemplateMessage(@RequestBody MessageDTO message); + + /** + * 获取模板内容 + * @param code + * @return + */ + @GetMapping("/sys/api/getTemplateContent") + String getTemplateContent(@RequestParam("code") String code); + + /** + * 新增数据日志 + * @param dataLogDto + */ + @PostMapping("/sys/api/saveDataLog") + void saveDataLog(@RequestBody DataLogDTO dataLogDto); + + /** + * 更新头像 + * @param loginUser + * @return + */ + @PutMapping("/sys/api/updateAvatar") + void updateAvatar(@RequestBody LoginUser loginUser); + + @GetMapping("/sys/api/sendAppChatSocket") + void sendAppChatSocket(@RequestParam(name="userId") String userId); + + /** + * 根据角色id查询角色code + * @param id + * @return + */ + @GetMapping("/sys/api/getRoleCode") + String getRoleCodeById(@RequestParam(name = "id") String id); + + /** + * 根据roleCode查询角色信息,可逗号分隔多个 + * + * @param roleCodes + * @return + */ + @GetMapping("/sys/api/queryRoleDictByCode") + List queryRoleDictByCode(@RequestParam(name = "roleCodes") String roleCodes); + + + /** + * 根据高级查询条件查询用户 + * @param superQuery + * @param matchType + * @return + */ + @GetMapping("/sys/api/queryUserBySuperQuery") + List queryUserBySuperQuery(@RequestParam(name="superQuery")String superQuery,@RequestParam(name="matchType")String matchType); + + + /** + * 根据ID条件查询用户 + * @param id + * @return JSONObject + */ + @GetMapping("/sys/api/queryUserById") + JSONObject queryUserById(@RequestParam(name="id") String id); + + + /** + * 根据高级查询条件查询部门 + * @param superQuery + * @param matchType + * @return + */ + @GetMapping("/sys/api/queryDeptBySuperQuery") + List queryDeptBySuperQuery(@RequestParam(name="superQuery")String superQuery,@RequestParam(name="matchType")String matchType); + + /** + * 根据高级查询条件查询角色 + * @param superQuery + * @param matchType + * @return + */ + @GetMapping("/sys/api/queryRoleBySuperQuery") + List queryRoleBySuperQuery(@RequestParam(name="superQuery")String superQuery,@RequestParam(name="matchType")String matchType); + + + /** + * 根据租户ID查询用户ID + * @param tenantId 租户ID + * @return List + */ + @GetMapping("/sys/api/selectUserIdByTenantId") + List selectUserIdByTenantId(@RequestParam("tenantId")String tenantId); + + + /** + * 根据部门ID查询用户ID + * @param deptIds + * @return + */ + @GetMapping("/sys/api/queryUserIdsByDeptIds") + List queryUserIdsByDeptIds(@RequestParam("deptIds") List deptIds); + + /** + * 根据部门ID查询用户账号 + * @param deptIds + * @return + */ + @GetMapping("/sys/api/queryUserAccountsByDeptIds") + List queryUserAccountsByDeptIds(@RequestParam("deptIds") List deptIds); + + /** + * 根据角色编码 查询用户ID + * @param roleCodes + * @return + */ + @GetMapping("/sys/api/queryUserIdsByRoleds") + List queryUserIdsByRoleds(@RequestParam("roleCodes") List roleCodes); + + /** + * 根据用户ID查询用户名称 + * @param userIds + * @return + */ + @GetMapping("/sys/api/queryUsernameByIds") + List queryUsernameByIds(@RequestParam("userIds") List userIds); + + /** + * 根据部门岗位ID查询用户ID + * @param deptPostIds + * @return + */ + @GetMapping("/sys/api/queryUserIdsByDeptPostIds") + public List queryUserIdsByDeptPostIds(@RequestParam("deptPostIds") List deptPostIds); + + /** + * 根据部门主岗位和兼职岗位,查询用户账号 + * @param positionIds + * @return + */ + @GetMapping("/sys/api/queryUsernameByDepartPositIds") + List queryUsernameByDepartPositIds(@RequestParam("departPositIds") List positionIds); + + /** + * 根据职务ID查询用户账号 + * @param positionIds + * @return + */ + @GetMapping("/sys/api/queryUserIdsByPositionIds") + List queryUserIdsByPositionIds(@RequestParam("positionIds") List positionIds); + + /** + * 根据部门和子部门下的所有用户账号 + * + * @param orgCode 部门编码 + * @return + */ + @GetMapping("/sys/api/getUserAccountsByDepCode") + public List getUserAccountsByDepCode(@RequestParam("orgCode")String orgCode); + + /** + * 检查查询sql的表和字段是否在白名单中 + * + * @param selectSql + * @return + */ + @GetMapping("/sys/api/dictTableWhiteListCheckBySql") + boolean dictTableWhiteListCheckBySql(@RequestParam("selectSql") String selectSql); + + /** + * 根据字典表或者字典编码,校验是否在白名单中 + * + * @param tableOrDictCode 表名或dictCode + * @param fields 如果传的是dictCode,则该参数必须传null + * @return + */ + @GetMapping("/sys/api/dictTableWhiteListCheckByDict") + boolean dictTableWhiteListCheckByDict( + @RequestParam("tableOrDictCode") String tableOrDictCode, + @RequestParam(value = "fields", required = false) String... fields + ); + /** + * 自动发布通告 + * + * @param dataId 通告ID + * @param currentUserName 发送人 + * @return + */ + @GetMapping("/sys/api/announcementAutoRelease") + void announcementAutoRelease( + @RequestParam("dataId") String dataId, + @RequestParam(value = "currentUserName") String currentUserName + ); + + /** + * 根据部门编码查询公司信息 + * @param orgCode 部门编码 + * @return + * @author chenrui + * @date 2025/8/12 14:45 + */ + @GetMapping(value = "/sys/api/queryCompByOrgCode") + SysDepartModel queryCompByOrgCode(@RequestParam(name = "sysCode") String orgCode); + + /** + * 根据部门编码和层次查询上级公司 + * + * @param orgCode 部门编码 + * @param level 可以传空 默认为1级 最小值为1 + * @return + */ + @GetMapping(value = "/sys/api/queryCompByOrgCodeAndLevel") + SysDepartModel queryCompByOrgCodeAndLevel(@RequestParam("orgCode") String orgCode, @RequestParam("level") Integer level); + + /** + * 根据部门code或部门id获取部门名称(当前和上级部门) + * + * @param orgCode 部门编码 + * @param depId 部门id + * @return String 部门名称 + */ + @GetMapping("/getDepartPathNameByOrgCode") + String getDepartPathNameByOrgCode(@RequestParam(name = "orgCode", required = false) String orgCode, @RequestParam(name = "depId", required = false) String depId); + + + /** + * 根据部门ID查询部门及其子部门下用户ID
+ * @param deptIds + * @return + * @author chenrui + * @date 2025/09/08 15:28 + */ + @GetMapping("/sys/api/queryUserIdsByCascadeDeptIds") + List queryUserIdsByCascadeDeptIds(@RequestParam("deptIds") List deptIds); + /** + * 根据用户信息推送移动端Push消息 + * @param pushMessageDTO + * @return + */ + @PostMapping("/sys/api/uniPushMsgToUser") + void uniPushMsgToUser(@RequestBody PushMessageDTO pushMessageDTO); + + /** + * 根据用户名查询用户主部门信息。 + *

+ * 逻辑:取用户的主岗位(mainDepPostId),再查询该岗位节点在 sys_depart 中的父节点, + * 父节点即为用户的主部门,返回其信息。 + *

+ * + * @param username 用户账号 + * @return 主部门信息,若用户未配置主岗位则返回 {@code null} + */ + @GetMapping("/sys/api/queryMainDepartByUsername") + SysDepartModel queryMainDepartByUsername(@RequestParam("username") String username); + +} diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/factory/SysBaseAPIFallbackFactory.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/factory/SysBaseAPIFallbackFactory.java new file mode 100644 index 0000000..2b7a64c --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/factory/SysBaseAPIFallbackFactory.java @@ -0,0 +1,21 @@ +package com.ghb.base.common.system.api.factory; + +import org.springframework.cloud.openfeign.FallbackFactory; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.api.fallback.SysBaseAPIFallback; +import org.springframework.stereotype.Component; + +/** + * @Description: SysBaseAPIFallbackFactory + * @author: Ghb-boot + */ +@Component +public class SysBaseAPIFallbackFactory implements FallbackFactory { + + @Override + public ISysBaseAPI create(Throwable throwable) { + SysBaseAPIFallback fallback = new SysBaseAPIFallback(); + fallback.setCause(throwable); + return fallback; + } +} \ No newline at end of file diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/fallback/SysBaseAPIFallback.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/fallback/SysBaseAPIFallback.java new file mode 100644 index 0000000..74fa0bf --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/common/system/api/fallback/SysBaseAPIFallback.java @@ -0,0 +1,529 @@ +package com.ghb.base.common.system.api.fallback; + +import com.alibaba.fastjson.JSONObject; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.dto.DataLogDTO; +import com.ghb.base.common.api.dto.OnlineAuthDTO; +import com.ghb.base.common.api.dto.PushMessageDTO; +import com.ghb.base.common.api.dto.message.*; +import com.ghb.base.common.constant.enums.DySmsEnum; +import com.ghb.base.common.constant.enums.EmailTemplateEnum; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.vo.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 进入fallback的方法 检查是否token未设置 + * @author: Ghb-boot + */ +@Slf4j +public class SysBaseAPIFallback implements ISysBaseAPI { + + @Setter + private Throwable cause; + + @Override + public void sendSysAnnouncement(MessageDTO message) { + log.error("发送消息失败 {}", cause); + } + + @Override + public void sendBusAnnouncement(BusMessageDTO message) { + log.error("发送消息失败 {}", cause); + } + + @Override + public void sendTemplateAnnouncement(TemplateMessageDTO message) { + log.error("发送消息失败 {}", cause); + } + + @Override + public void sendBusTemplateAnnouncement(BusTemplateMessageDTO message) { + log.error("发送消息失败 {}", cause); + } + + @Override + public String parseTemplateByCode(TemplateDTO templateDTO) { + log.error("通过模板获取消息内容失败 {}", cause); + return null; + } + + @Override + public LoginUser getUserById(String id) { + return null; + } + + @Override + public List getRolesByUsername(String username) { + return null; + } + + @Override + public List getRolesByUserId(String userId) { + return null; + } + + @Override + public List getDepartIdsByUsername(String username) { + return null; + } + + @Override + public List getDepartIdsByUserId(String userId) { + return null; + } + + @Override + public Set getDepartParentIdsByUsername(String username) { + return null; + } + + @Override + public Set getDepartParentIdsByDepIds(Set depIds) { + return null; + } + + @Override + public Map> getDepartIdsByUserIds(Collection userIds) { + return Map.of(); + } + + @Override + public List getDepartNamesByUsername(String username) { + return null; + } + + @Override + public List queryDictItemsByCode(String code) { + return null; + } + + @Override + public List queryEnableDictItemsByCode(String code) { + return null; + } + + @Override + public List queryAllDict() { + log.error("fegin接口queryAllDict失败:"+cause.getMessage(), cause); + return null; + } + + @Override + public List queryAllSysCategory() { + return null; + } + + @Override + public List queryTableDictItemsByCode(String tableFilterSql, String text, String code) { + return null; + } + + @Override + public List queryAllDepartBackDictModel() { + return null; + } + + @Override + public void updateSysAnnounReadFlag(String busType, String busId) { + + } + + @Override + public List queryFilterTableDictInfo(String table, String text, String code, String filterSql) { + return null; + } + + @Override + public List queryTableDictByKeys(String table, String text, String code, String[] keyArray) { + log.error("queryTableDictByKeys查询失败 {}", cause); + return null; + } + + @Override + public List queryAllUserBackCombo() { + return null; + } + + @Override + public JSONObject queryAllUser(String userIds, Integer pageNo, Integer pageSize) { + return null; + } + + @Override + public List queryAllRole(String[] roleIds) { + log.error("获取角色信息失败 {}", cause); + return null; + } + + @Override + public List getRoleIdsByUsername(String username) { + return null; + } + + @Override + public String getDepartIdsByOrgCode(String orgCode) { + return null; + } + + @Override + public List getAllSysDepart() { + return null; + } + + @Override + public DictModel getParentDepartId(String departId) { + return null; + } + + @Override + public List getDeptHeadByDepId(String deptId) { + return null; + } + + @Override + public void sendWebSocketMsg(String[] userIds, String cmd) { + + } + + @Override + public List queryAllUserByIds(String[] userIds) { + return null; + } + + @Override + public void meetingSignWebsocket(String userId) { + + } + + @Override + public List queryUserByNames(String[] userNames) { + return null; + } + + @Override + public Set getUserRoleSet(String username) { + return null; + } + + @Override + public Set getUserRoleSetById(String userId) { + return null; + } + + @Override + public Set getUserPermissionSet(String userId) { + return null; + } + + @Override + public boolean hasOnlineAuth(OnlineAuthDTO onlineAuthDTO) { + return false; + } + + @Override + public SysDepartModel selectAllById(String id) { + return null; + } + + @Override + public List queryDeptUsersByUserId(String userId) { + return null; + } + + @Override + public Set queryUserRoles(String username) { + return null; + } + + @Override + public Set queryUserRolesById(String userId) { + return null; + } + + @Override + public Set queryUserAuths(String userId) { + return null; + } + + @Override + public DynamicDataSourceModel getDynamicDbSourceById(String dbSourceId) { + return null; + } + + @Override + public DynamicDataSourceModel getDynamicDbSourceByCode(String dbSourceCode) { + return null; + } + + @Override + public LoginUser getUserByName(String username) { + log.error("Ghb-system服务节点不通,导致获取登录用户信息失败: " + cause.getMessage(), cause); + return null; + } + + @Override + public String getUserIdByName(String username) { + return null; + } + + @Override + public String translateDictFromTable(String table, String text, String code, String key) { + return null; + } + + @Override + public String translateDict(String code, String key) { + return null; + } + + @Override + public List queryPermissionDataRule(String component, String requestPath, String username) { + return null; + } + + @Override + public SysUserCacheInfo getCacheUser(String username) { + log.error("获取用户信息失败 {}", cause); + return null; + } + + @Override + public List queryUsersByUsernames(String usernames) { + return null; + } + + @Override + public List queryUsersByIds(String ids) { + return null; + } + + @Override + public List queryDepartsByOrgcodes(String orgCodes) { + return null; + } + + @Override + public List queryDepartsByIds(String ids) { + return null; + } + + @Override + public Map> translateManyDict(String dictCodes, String keys) { + return null; + } + + // 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------ + @Override + public List translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource) { + return null; + } + + @Override + public void sendTemplateMessage(MessageDTO message) { + } + + @Override + public String getTemplateContent(String code) { + return null; + } + + @Override + public void saveDataLog(DataLogDTO dataLogDto) { + + } + + @Override + public void sendEmailMsg(String email,String title,String content) { + + } + + @Override + public void sendHtmlTemplateEmail(String email, String title, EmailTemplateEnum emailTemplateEnum, JSONObject params) { + + } + + @Override + public void sendSmsMsg(String phone, JSONObject params, DySmsEnum dySmsEnum) { + + } + + @Override + public List getDeptUserByOrgCode(String orgCode) { + return null; + } + +// @Override +// public List queryDepartsByOrgIds(String ids) { +// return null; +// } + + @Override + public List loadCategoryDictItem(String ids) { + return null; + } + + @Override + public List loadCategoryDictItemByNames(String names, boolean delNotExist) { + return null; + } + + @Override + public List loadDictItem(String dictCode, String keys) { + return null; + } + + @Override + public Map copyLowAppDict(String originalAppId, String appId, String tenantId) { + return null; + } + + @Override + public List getDictItems(String dictCode) { + return null; + } + + @Override + public Map> getManyDictItems(List dictCodeList) { + return null; + } + + @Override + public List loadDictItemByKeyword(String dictCode, String keyword, Integer pageNo, Integer pageSize) { + return null; + } + + @Override + public void updateAvatar(LoginUser loginUser) { } + + @Override + public void sendAppChatSocket(String userId) { + + } + + @Override + public String getRoleCodeById(String id) { + return null; + } + + @Override + public List queryRoleDictByCode(String roleCodes) { + return null; + } + + @Override + public List queryUserBySuperQuery(String superQuery, String matchType) { + return null; + } + + @Override + public JSONObject queryUserById(String id) { + return null; + } + + @Override + public List queryDeptBySuperQuery(String superQuery, String matchType) { + return null; + } + + @Override + public List queryRoleBySuperQuery(String superQuery, String matchType) { + return null; + } + + @Override + public List selectUserIdByTenantId(String tenantId) { + return null; + } + + @Override + public List queryUserIdsByDeptIds(List deptIds) { + return null; + } + + @Override + public List queryUserIdsByDeptPostIds(List deptPostIds) { + return List.of(); + } + + @Override + public List queryUserAccountsByDeptIds(List deptIds) { + return null; + } + + @Override + public List queryUserIdsByRoleds(List roleCodes) { + return null; + } + + @Override + public List queryUsernameByIds(List userIds) { + return List.of(); + } + + @Override + public List queryUsernameByDepartPositIds(List positionIds) { + return null; + } + + @Override + public List queryUserIdsByPositionIds(List positionIds) { + return null; + } + + @Override + public List getUserAccountsByDepCode(String orgCode) { + return null; + } + + @Override + public boolean dictTableWhiteListCheckBySql(String selectSql) { + return false; + } + + @Override + public boolean dictTableWhiteListCheckByDict(String tableOrDictCode, String... fields) { + return false; + } + + @Override + public void announcementAutoRelease(String dataId, String currentUserName) { + + } + + @Override + public SysDepartModel queryCompByOrgCode(String orgCode) { + return null; + } + + @Override + public SysDepartModel queryCompByOrgCodeAndLevel(String orgCode, Integer level) { + return null; + } + + @Override + public void uniPushMsgToUser(PushMessageDTO pushMessageDTO) { + + } + + @Override + public SysDepartModel queryMainDepartByUsername(String username) { + return null; + } + + @Override + public String getDepartPathNameByOrgCode(String orgCode, String depId) { + return ""; + } + + @Override + public List queryUserIdsByCascadeDeptIds(List deptIds) { + return null; + } +} diff --git a/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/config/FeignConfig.java b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/config/FeignConfig.java new file mode 100644 index 0000000..42b8935 --- /dev/null +++ b/test-module-system/test-system-api/test-system-cloud-api/src/main/java/com/ghb/base/config/FeignConfig.java @@ -0,0 +1,179 @@ +//package com.ghb.base.config; +// +//import java.io.IOException; +//import java.util.ArrayList; +//import java.util.Arrays; +//import java.util.List; +//import java.util.SortedMap; +// +//import jakarta.servlet.http.HttpServletRequest; +// +//import com.ghb.base.common.config.mqtoken.UserTokenContext; +//import com.ghb.base.common.constant.CommonConstant; +//import com.ghb.base.common.util.DateUtils; +//import com.ghb.base.common.util.PathMatcherUtil; +//import com.ghb.base.config.sign.interceptor.SignAuthConfiguration; +//import com.ghb.base.config.sign.util.HttpUtils; +//import com.ghb.base.config.sign.util.SignUtil; +//import org.springframework.beans.factory.ObjectFactory; +//import org.springframework.boot.autoconfigure.AutoConfigureBefore; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +//import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +//import org.springframework.cloud.openfeign.FeignAutoConfiguration; +//import org.springframework.cloud.openfeign.support.SpringDecoder; +//import org.springframework.cloud.openfeign.support.SpringEncoder; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.context.annotation.Primary; +//import org.springframework.context.annotation.Scope; +//import org.springframework.http.MediaType; +//import org.springframework.web.context.request.RequestContextHolder; +//import org.springframework.web.context.request.ServletRequestAttributes; +// +//import com.alibaba.fastjson.JSON; +//import com.alibaba.fastjson.serializer.SerializerFeature; +//import com.alibaba.fastjson.support.config.FastJsonConfig; +//import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +//import com.alibaba.fastjson.support.springfox.SwaggerJsonSerializer; +// +//import feign.Feign; +//import feign.Logger; +//import feign.RequestInterceptor; +//import feign.codec.Decoder; +//import feign.codec.Encoder; +//import feign.form.spring.SpringFormEncoder; +//import lombok.extern.slf4j.Slf4j; +// +///** +// * @Description: FeignConfig +// * @author: GhbBoot +// */ +//@ConditionalOnClass(Feign.class) +//@AutoConfigureBefore(FeignAutoConfiguration.class) +//@Slf4j +//@Configuration +//public class FeignConfig { +// +// /** +// * 设置feign header参数 +// * 【X_ACCESS_TOKEN】【X_SIGN】【X_TIMESTAMP】 +// * @return +// */ +// @Bean +// public RequestInterceptor requestInterceptor() { +// return requestTemplate -> { +// ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); +// if (null != attributes) { +// HttpServletRequest request = attributes.getRequest(); +// log.debug("Feign request: {}", request.getRequestURI()); +// // 将token信息放入header中 +// String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); +// if(token==null || "".equals(token)){ +// token = request.getParameter("token"); +// } +// log.info("Feign Login Request token: {}", token); +// requestTemplate.header(CommonConstant.X_ACCESS_TOKEN, token); +// }else{ +// //解决后台任务、MQ中调用feign接口,无会话token的问题 +// String token = UserTokenContext.getToken(); +// log.info("Feign No Login token: {}", token); +// requestTemplate.header(CommonConstant.X_ACCESS_TOKEN, token); +// } +// +// //================================================================================================================ +// //针对特殊接口,进行加签验证 ——根据URL地址过滤请求 【字典表参数签名验证】 +// if (PathMatcherUtil.matches(Arrays.asList(SignAuthConfiguration.SIGN_URL_LIST),requestTemplate.path())) { +// try { +// log.info("============================ [begin] fegin api url ============================"); +// log.info(requestTemplate.path()); +// log.info(requestTemplate.method()); +// String queryLine = requestTemplate.queryLine(); +// String questionMark="?"; +// if(queryLine!=null && queryLine.startsWith(questionMark)){ +// queryLine = queryLine.substring(1); +// } +// log.info(queryLine); +// if(requestTemplate.body()!=null){ +// log.info(new String(requestTemplate.body())); +// } +// SortedMap allParams = HttpUtils.getAllParams(requestTemplate.path(),queryLine,requestTemplate.body(),requestTemplate.method()); +// String sign = SignUtil.getParamsSign(allParams); +// log.info(" Feign request params sign: {}",sign); +// log.info("============================ [end] fegin api url ============================"); +// requestTemplate.header(CommonConstant.X_SIGN, sign); +// requestTemplate.header(CommonConstant.X_TIMESTAMP, String.valueOf(System.currentTimeMillis())); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// //================================================================================================================ +// }; +// } +// +// +// +// /** +// * Feign 客户端的日志记录,默认级别为NONE +// * Logger.Level 的具体级别如下: +// * NONE:不记录任何信息 +// * BASIC:仅记录请求方法、URL以及响应状态码和执行时间 +// * HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息 +// * FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据 +// */ +// @Bean +// Logger.Level feignLoggerLevel() { +// return Logger.Level.FULL; +// } +// +// /** +// * Feign支持文件上传 +// * @param messageConverters +// * @return +// */ +// @Bean +// @Primary +// @Scope("prototype") +// public Encoder multipartFormEncoder(ObjectFactory messageConverters) { +// return new SpringFormEncoder(new SpringEncoder(messageConverters)); +// } +// +// /** +// * 给 Feign 添加 FastJson 的解析支持 +// */ +// @Bean +// public Encoder feignEncoder() { +// return new SpringEncoder(feignHttpMessageConverter()); +// } +// +// @Bean("apiFeignDecoder") +// public Decoder feignDecoder() { +// return new SpringDecoder(feignHttpMessageConverter()); +// } +// +// /** +// * 设置解码器为fastjson +// * +// * @return +// */ +// private ObjectFactory feignHttpMessageConverter() { +// final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(this.getFastJsonConverter()); +// return () -> httpMessageConverters; +// } +// +// private FastJsonHttpMessageConverter getFastJsonConverter() { +// FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); +// +// List supportedMediaTypes = new ArrayList<>(); +// MediaType mediaTypeJson = MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE); +// supportedMediaTypes.add(mediaTypeJson); +// converter.setSupportedMediaTypes(supportedMediaTypes); +// FastJsonConfig config = new FastJsonConfig(); +// config.getSerializeConfig().put(JSON.class, new SwaggerJsonSerializer()); +// config.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect); +// converter.setFastJsonConfig(config); +// +// return converter; +// } +// +// +//} diff --git a/test-module-system/test-system-api/test-system-local-api/pom.xml b/test-module-system/test-system-api/test-system-local-api/pom.xml new file mode 100644 index 0000000..e0c935b --- /dev/null +++ b/test-module-system/test-system-api/test-system-local-api/pom.xml @@ -0,0 +1,14 @@ + + + + test-system-api + com.ghb + 3.9.2 + + 4.0.0 + + test-system-local-api + + \ No newline at end of file diff --git a/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/airag/api/IAiragBaseApi.java b/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/airag/api/IAiragBaseApi.java new file mode 100644 index 0000000..a03df9d --- /dev/null +++ b/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/airag/api/IAiragBaseApi.java @@ -0,0 +1,60 @@ +package com.ghb.base.common.airag.api; + +/** + * airag baseAPI + * + * @author sjlei + * @date 2025-12-30 + */ +public interface IAiragBaseApi { + + /** + * 知识库写入文本文档(支持自定义分段策略) + * + * @param knowledgeId 知识库ID + * @param title 文档标题 + * @param content 文档内容 + * @param segmentConfig 【可选】分段策略配置JSON,包含 segmentStrategy/separator/customSeparator/maxSegment/overlap/textRules + * @return 新增的文档ID + */ + String knowledgeWriteTextDocument(String knowledgeId, String title, String content, String segmentConfig); + + /** + * 读取会话变量 + * + * @param appId 应用ID + * @param username 用户名 + * @param name 变量名 + * @return 变量值,不存在时返回null + */ + String getChatVariable(String appId, String username, String name); + + /** + * 设置会话变量 + * + * @param appId 应用ID + * @param username 用户名 + * @param name 变量名 + * @param value 变量值 + */ + void setChatVariable(String appId, String username, String name, String value); + + /** + * 根据应用ID查询记忆库ID + * 当应用开启了记忆功能(izOpenMemory=1)时返回memoryId,否则返回null + * + * @param appId 应用ID + * @return 记忆库ID,未开启记忆功能时返回null + */ + String getMemoryIdByAppId(String appId); + + /** + * 根据提示词ID查询提示词内容 + * 供 LLM 节点关联模式在运行时动态加载提示词内容 + * + * @param promptId 提示词表主键ID + * @return 提示词内容,提示词不存在时返回null + */ + String getPromptContent(String promptId); + +} diff --git a/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/online/api/IOnlineBaseExtApi.java b/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/online/api/IOnlineBaseExtApi.java new file mode 100644 index 0000000..4c863e3 --- /dev/null +++ b/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/online/api/IOnlineBaseExtApi.java @@ -0,0 +1,72 @@ +package com.ghb.base.common.online.api; + +import com.alibaba.fastjson.JSONObject; +import com.ghb.base.common.system.vo.DictModel; + +import java.util.List; +import java.util.Map; + +/** + * 【Online】online表单对外接口 + * + * @author sunjianlei + */ +public interface IOnlineBaseExtApi { + + /** + * 【Online】 表单设计器专用:同步新增 + * @param tableName 表名 + * @param jsonObject + * @throws Exception + * @return String + */ + String cgformPostCrazyForm(String tableName, JSONObject jsonObject) throws Exception; + + /** + * 【Online】 表单设计器专用:同步编辑 + * @param tableName 表名 + * @param jsonObject + * @throws Exception + * @return String + */ + String cgformPutCrazyForm(String tableName, JSONObject jsonObject) throws Exception; + + /** + * online表单删除数据 + * + * @param cgformCode Online表单code + * @param dataIds 数据ID,可逗号分割 + * @return + */ + String cgformDeleteDataByCode(String cgformCode, String dataIds); + + /** + * 通过online表名查询数据,同时查询出子表的数据 + * + * @param tableName online表名 + * @param dataIds online数据ID + * @return + */ + JSONObject cgformQueryAllDataByTableName(String tableName, String dataIds); + + /** + * 对 cgreportGetData 的返回值做优化,封装 DictModel 集合 + * @param code + * @param dictCode + * @param dataList + * @param dictText 字典文本 + * @return + */ + List cgreportGetDataPackage(String code, String dictText, String dictCode, String dataList); + + /** + * 【cgreport】通过 head code 获取 sql语句,并执行该语句返回查询数据 + * + * @param code 报表Code,如果没传ID就通过code查 + * @param forceKey + * @param dataList + * @return + */ + Map cgreportGetData(String code, String forceKey, String dataList); + +} diff --git a/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/system/api/ISysBaseAPI.java b/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/system/api/ISysBaseAPI.java new file mode 100644 index 0000000..1bfce2f --- /dev/null +++ b/test-module-system/test-system-api/test-system-local-api/src/main/java/com/ghb/base/common/system/api/ISysBaseAPI.java @@ -0,0 +1,646 @@ +package com.ghb.base.common.system.api; + +import com.alibaba.fastjson.JSONObject; +import com.ghb.base.common.api.CommonAPI; +import com.ghb.base.common.api.dto.DataLogDTO; +import com.ghb.base.common.api.dto.OnlineAuthDTO; +import com.ghb.base.common.api.dto.PushMessageDTO; +import com.ghb.base.common.api.dto.message.*; +import com.ghb.base.common.constant.enums.DySmsEnum; +import com.ghb.base.common.constant.enums.EmailTemplateEnum; +import com.ghb.base.common.system.vo.*; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @Description 底层共通业务API,提供其他独立模块调用 + * @Author scott + * @Date 2019-4-20 + * @Version V1.0 + */ +public interface ISysBaseAPI extends CommonAPI { + + //=======OLD 系统消息推送接口============================ + /** + * 1发送系统消息 + * @param message 使用构造器赋值参数 如果不设置category(消息类型)则默认为2 发送系统消息 + */ + void sendSysAnnouncement(MessageDTO message); + + /** + * 2发送消息 附带业务参数 + * @param message 使用构造器赋值参数 + */ + void sendBusAnnouncement(BusMessageDTO message); + + /** + * 3通过模板发送消息 + * @param message 使用构造器赋值参数 + */ + void sendTemplateAnnouncement(TemplateMessageDTO message); + + /** + * 4通过模板发送消息 附带业务参数 + * @param message 使用构造器赋值参数 + */ + void sendBusTemplateAnnouncement(BusTemplateMessageDTO message); + + /** + * 5通过消息中心模板,生成推送内容 + * @param templateDTO 使用构造器赋值参数 + * @return + */ + String parseTemplateByCode(TemplateDTO templateDTO); + //=======OLD 系统消息推送接口============================ + + //=======TY NEW 自定义消息推送接口,邮件、钉钉、企业微信、系统消息============================ + /** + * NEW发送模板消息【新,支持自定义推送类型: 邮件、钉钉、企业微信、系统消息】 + * @param message + */ + void sendTemplateMessage(MessageDTO message); + + /** + * NEW根据模板编码获取模板内容【新,支持自定义推送类型】 + * @param templateCode + * @return + */ + String getTemplateContent(String templateCode); + //=======TY NEW 自定义消息推送接口,邮件、钉钉、企业微信、系统消息============================ + + /** + * 6根据用户id查询用户信息 + * @param id + * @return + */ + LoginUser getUserById(String id); + + /** + * 7通过用户账号查询角色集合 + * @param username + * @return + */ + List getRolesByUsername(String username); + + /** + * 7通过用户账号查询角色集合 + * @param userId + * @return + */ + List getRolesByUserId(String userId); + + /** + * 8通过用户账号查询部门集合 + * @param username + * @return 部门 id + */ + List getDepartIdsByUsername(String username); + /** + * 8通过用户账号查询部门集合 + * @param userId + * @return 部门 id + */ + List getDepartIdsByUserId(String userId); + + /** + * 8.2 通过用户账号查询部门父ID集合 + * @param username + * @return 部门 parentIds + */ + Set getDepartParentIdsByUsername(String username); + + /** + * 8.2 查询部门父ID集合 + * @param depIds + * @return 部门 parentIds + */ + Set getDepartParentIdsByDepIds(Set depIds); + + /** + * 8.4 通过 userIds 查询部门ID列表 + * + * @param userIds + * @return key = userId; value = 用户拥有的部门ID列表 + */ + Map> getDepartIdsByUserIds(Collection userIds); + + /** + * 9通过用户账号查询部门 name + * @param username + * @return 部门 name + */ + List getDepartNamesByUsername(String username); + + + + /** 11查询所有的父级字典,按照create_time排序 + * @return List 字典集合 + */ + public List queryAllDict(); + + /** + * 12查询所有分类字典 + * @return + */ + public List queryAllSysCategory(); + + + /** + * 14查询所有部门 作为字典信息 id -->value,departName -->text + * @return + */ + public List queryAllDepartBackDictModel(); + + /** + * 15根据业务类型及业务id修改消息已读 + * @param busType + * @param busId + */ + public void updateSysAnnounReadFlag(String busType, String busId); + + /** + * 16查询表字典 支持过滤数据 + * @param table + * @param text + * @param code + * @param filterSql + * @return + */ + public List queryFilterTableDictInfo(String table, String text, String code, String filterSql); + + /** + * 17查询指定table的 text code 获取字典,包含text和value + * @param table + * @param text + * @param code + * @param keyArray + * @return + */ + @Deprecated + public List queryTableDictByKeys(String table, String text, String code, String[] keyArray); + + /** + * 18查询所有用户 返回ComboModel + * @return + */ + public List queryAllUserBackCombo(); + + /** + * 19分页查询用户 返回JSONObject + * @param userIds 多个用户id + * @param pageNo 当前页数 + * @param pageSize 每页显示条数 + * @return + */ + public JSONObject queryAllUser(String userIds, Integer pageNo, Integer pageSize); + + /** + * 20获取所有角色 + * @return + */ + public List queryAllRole(); + + /** + * 21获取所有角色 带参 + * @param roleIds 默认选中角色 + * @return + */ + public List queryAllRole(String[] roleIds ); + + /** + * 22通过用户账号查询角色Id集合 + * @param username + * @return + */ + public List getRoleIdsByUsername(String username); + + /** + * 23通过部门编号查询部门id + * @param orgCode + * @return + */ + public String getDepartIdsByOrgCode(String orgCode); + + /** + * 24查询所有部门 + * @return + */ + public List getAllSysDepart(); + + /** + * 25查找父级部门 + * @param departId + * @return + */ + DictModel getParentDepartId(String departId); + + /** + * 26根据部门Id获取部门负责人 + * @param deptId + * @return + */ + public List getDeptHeadByDepId(String deptId); + + /** + * 27给指定用户发消息 + * @param userIds + * @param cmd + */ + public void sendWebSocketMsg(String[] userIds, String cmd); + + /** + * 28根据id获取所有参与用户 + * @param userIds 多个用户id + * @return + */ + public List queryAllUserByIds(String[] userIds); + + /** + * 29将会议签到信息推动到预览 + * userIds + * @return + * @param userId + */ + void meetingSignWebsocket(String userId); + + /** + * 30根据name获取所有参与用户 + * @param userNames 多个用户账户 + * @return + */ + List queryUserByNames(String[] userNames); + + + /** + * 根据高级查询条件查询用户 + * @param superQuery + * @param matchType + * @return + */ + List queryUserBySuperQuery(String superQuery,String matchType); + + + /** + * 根据ID查询用户 + * @param id + * @return + */ + JSONObject queryUserById(String id); + + + /** + * 根据高级查询条件查询部门 + * @param superQuery + * @param matchType + * @return + */ + List queryDeptBySuperQuery(String superQuery,String matchType); + + /** + * 根据高级查询条件查询角色 + * @param superQuery + * @param matchType + * @return + */ + List queryRoleBySuperQuery(String superQuery,String matchType); + + + /** + * 根据租户ID查询用户ID + * @param tenantId 租户ID + * @return List + */ + List selectUserIdByTenantId(String tenantId); + + + + /** + * 31获取用户的角色集合 + * @param username + * @return + */ + Set getUserRoleSet(String username); + /** + * 31获取用户的角色集合 + * @param useId + * @return + */ + Set getUserRoleSetById(String useId); + + /** + * 32获取用户的权限集合 + * @param userId + * @return + */ + Set getUserPermissionSet(String userId); + + /** + * 33判断是否有online访问的权限 + * @param onlineAuthDTO + * @return + */ + boolean hasOnlineAuth(OnlineAuthDTO onlineAuthDTO); + + /** + * 34通过部门id获取部门全部信息 + * @param id 部门id + * @return SysDepartModel对象 + */ + SysDepartModel selectAllById(String id); + + /** + * 35根据用户id查询用户所属公司下所有用户ids + * @param userId + * @return + */ + List queryDeptUsersByUserId(String userId); + + /** + * 36根据多个用户账号(逗号分隔),查询返回多个用户信息 + * @param usernames + * @return + */ + List queryUsersByUsernames(String usernames); + + /** + * 37根据多个用户ID(逗号分隔),查询返回多个用户信息 + * @param ids + * @return + */ + List queryUsersByIds(String ids); + + /** + * 38根据多个部门编码(逗号分隔),查询返回多个部门信息 + * @param orgCodes + * @return + */ + List queryDepartsByOrgcodes(String orgCodes); + + /** + * 39根据多个部门id(逗号分隔),查询返回多个部门信息 + * @param ids + * @return + */ + List queryDepartsByIds(String ids); + + /** + * 40发送邮件消息 + * @param email + * @param title + * @param content + */ + void sendEmailMsg(String email,String title,String content); + + /** + * 40发送模版邮件消息 + * + * @param email 接收邮箱 + * @param title 邮件标题 + * @param emailTemplateEnum 邮件模版枚举 + * @param params 模版参数 + */ + void sendHtmlTemplateEmail(String email, String title, EmailTemplateEnum emailTemplateEnum, JSONObject params); + /** + * 41 获取公司下级部门和公司下所有用户信息 + * @param orgCode + * @return List + */ + List getDeptUserByOrgCode(String orgCode); + /** + * 42 发送短信消息 + * @param phone 手机号 + * @param param 模版参数 + * @param dySmsEnum 短信模版 + */ + void sendSmsMsg(String phone, JSONObject param, DySmsEnum dySmsEnum); + /** + * 查询分类字典翻译 + * @param ids 多个分类字典id + * @return List + */ + List loadCategoryDictItem(String ids); + + /** + * 反向翻译分类字典,用于导入 + * + * @param names 名称,逗号分割 + */ + List loadCategoryDictItemByNames(String names, boolean delNotExist); + + /** + * 根据字典code加载字典text + * + * @param dictCode 顺序:tableName,text,code + * @param keys 要查询的key + * @return + */ + List loadDictItem(String dictCode, String keys); + + /** + * 复制应用下的所有字典配置到新的租户下 + * + * @param originalAppId 原始低代码应用ID + * @param appId 新的低代码应用ID + * @param tenantId 新的租户ID + * @return Map Map<原字典编码, 新字典编码> + */ + Map copyLowAppDict(String originalAppId, String appId, String tenantId); + + /** + * 根据字典code查询字典项 + * + * @param dictCode 顺序:tableName,text,code + * @param dictCode 要查询的key + * @return + */ + List getDictItems(String dictCode); + + /** + * 根据多个字典code查询多个字典项 + * @param dictCodeList + * @return key = dictCode ; value=对应的字典项 + */ + Map> getManyDictItems(List dictCodeList); + + /** + * 【JSearchSelectTag下拉搜索组件专用接口】 + * 大数据量的字典表 走异步加载 即前端输入内容过滤数据 + * + * @param dictCode 字典code格式:table,text,code + * @param keyword 过滤关键字 + * @param pageSize 分页条数 + * @return + */ + List loadDictItemByKeyword(String dictCode, String keyword, Integer pageNo, Integer pageSize); + + /** + * 新增数据日志 + * @param dataLogDto + */ + void saveDataLog(DataLogDTO dataLogDto); + /** + * 更新头像 + * @param loginUser + */ + void updateAvatar(LoginUser loginUser); + + /** + * 向app端 websocket推送聊天刷新消息 + * @param userId + */ + void sendAppChatSocket(String userId); + + /** + * 根据角色id查询角色code + * @param id + * @return + */ + String getRoleCodeById(String id); + + /** + * 根据roleCode查询角色信息,可逗号分隔多个 + * + * @param roleCodes + * @return + */ + List queryRoleDictByCode(String roleCodes); + + /** + * 根据部门ID查询用户ID + * @param deptIds + * @return + */ + List queryUserIdsByDeptIds(List deptIds); + + /** + * 根据用户ID查询用户名称 + * @param userIds + * @return + */ + List queryUsernameByIds(List userIds); + + /** + * 根据部门ID查询部门及其子部门下用户ID
+ * @param deptIds + * @return + */ + List queryUserIdsByCascadeDeptIds(List deptIds); + + /** + * 根据部门ID查询用户账号 + * @param deptIds + * @return + */ + List queryUserAccountsByDeptIds(List deptIds); + + /** + * 根据角色编码 查询用户ID + * @param roleCodes + * @return + */ + List queryUserIdsByRoleds(List roleCodes); + + /** + * 根据部门岗位ID查询用户 + * @param deptPostIds + * @return + */ + public List queryUserIdsByDeptPostIds(List deptPostIds); + + /** + * 根据主岗位和兼职岗位ID查询用户ID + * @param departPositIds + * @return + */ + List queryUsernameByDepartPositIds(List departPositIds); + + /** + * 根据职位ID查询用户信息(老方法) + * @param positionIds + * @return + */ + public List queryUserIdsByPositionIds(List positionIds); + + /** + * 根据部门和子部门下的所有用户账号 + * + * @param orgCode 部门编码 + * @return + */ + public List getUserAccountsByDepCode(String orgCode); + + /** + * 检查查询sql的表和字段是否在白名单中 + * + * @param selectSql + * @return + */ + boolean dictTableWhiteListCheckBySql(String selectSql); + + /** + * 根据字典表或者字典编码,校验是否在白名单中 + * + * @param tableOrDictCode 表名或dictCode + * @param fields 如果传的是dictCode,则该参数必须传null + * @return + */ + boolean dictTableWhiteListCheckByDict(String tableOrDictCode, String... fields); + + /** + * 消息自动发布 + * @param dataId + * @param currentUserName + */ + void announcementAutoRelease(String dataId, String currentUserName); + + /** + * 根据部门编码查询公司信息 + * @param orgCode 部门编码 + * @return + * @author chenrui + * @date 2025/8/12 14:53 + */ + SysDepartModel queryCompByOrgCode(@RequestParam(name = "sysCode") String orgCode); + + /** + * 根据部门编码和层次查询上级公司 + * + * @param orgCode 部门编码 + * @param level 可以传空 默认为1级 最小值为1 + * @return + */ + SysDepartModel queryCompByOrgCodeAndLevel(String orgCode, Integer level); + + /** + * 根据部门code或部门id获取部门名称(当前和上级部门) + * + * @param orgCode 部门编码 + * @param depId 部门id + * @return String 部门名称 + */ + String getDepartPathNameByOrgCode(String orgCode, String depId); + /** + * 根据用户信息推送PUSH消息 + * + * @param pushMessageDTO 推送消息 + */ + void uniPushMsgToUser(PushMessageDTO pushMessageDTO); + + /** + * 根据用户名查询用户主部门信息。 + *

+ * 逻辑:取用户的主岗位(mainDepPostId),再查询该岗位节点在 sys_depart 中的父节点, + * 父节点即为用户的主部门,返回其信息。 + *

+ * + * @param username 用户账号 + * @return 主部门信息,若用户未配置主岗位则返回 {@code null} + */ + SysDepartModel queryMainDepartByUsername(String username); + +} diff --git a/test-module-system/test-system-biz/.gitattributes b/test-module-system/test-system-biz/.gitattributes new file mode 100644 index 0000000..d479839 --- /dev/null +++ b/test-module-system/test-system-biz/.gitattributes @@ -0,0 +1,4 @@ +*.js linguist-language=Java +*.css linguist-language=Java +*.html linguist-language=Java +*.vue linguist-language=Java diff --git a/test-module-system/test-system-biz/pom.xml b/test-module-system/test-system-biz/pom.xml new file mode 100644 index 0000000..e123c0e --- /dev/null +++ b/test-module-system/test-system-biz/pom.xml @@ -0,0 +1,55 @@ + + + com.ghb + test-module-system + 3.9.2 + + 4.0.0 + + test-system-biz + + + + com.ghb + test-system-local-api + + + org.hibernate + hibernate-core + + + org.jeecgframework.boot3 + jeecg-online + + + + + + org.jeecgframework + weixin4j + + + + org.jeecgframework.jimureport + jimureport-spring-boot3-starter + + + + + + org.jeecgframework.jimureport + jimubi-spring-boot3-starter + + + + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/firewall/SqlInjection/impl/DictTableWhiteListHandlerImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/firewall/SqlInjection/impl/DictTableWhiteListHandlerImpl.java new file mode 100644 index 0000000..43c88c6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/firewall/SqlInjection/impl/DictTableWhiteListHandlerImpl.java @@ -0,0 +1,279 @@ +package com.ghb.base.config.firewall.SqlInjection.impl; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbSqlInjectionException; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.firewall.SqlInjection.IDictTableWhiteListHandler; +import com.ghb.base.config.firewall.interceptor.LowCodeModeInterceptor; +import com.ghb.base.modules.system.entity.SysTableWhiteList; +import com.ghb.base.modules.system.security.DictQueryBlackListHandler; +import com.ghb.base.modules.system.service.ISysTableWhiteListService; +import org.jeecgframework.minidao.sqlparser.impl.vo.SelectSqlInfo; +import org.jeecgframework.minidao.util.MiniDaoUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.net.URLDecoder; +import java.util.*; + +/** + * 通用情况的白名单处理,若有无法处理的情况,可以单独写实现类 + */ +@Slf4j +@Component("dictTableWhiteListHandlerImpl") +public class DictTableWhiteListHandlerImpl implements IDictTableWhiteListHandler { + + /** + * key-表名 + * value-字段名,多个逗号隔开 + * 两种配置方式-- 全部配置成小写 + * whiteTablesRuleMap.put("sys_user", "*") sys_user所有的字段都支持查询 + * whiteTablesRuleMap.put("sys_user", "username,password") sys_user中的username和password支持查询 + */ + private static final Map whiteTablesRuleMap = new HashMap<>(); + /** + * LowCode 是否为 dev 模式 + */ + private static Boolean LOW_CODE_IS_DEV = null; + + + @Autowired + private ISysTableWhiteListService sysTableWhiteListService; + @Autowired + private GhbBaseConfig GhbBaseConfig; + + + /** + * 初始化 whiteTablesRuleMap 方法 + */ + private void init() { + // 如果当前为dev模式,则每次都查询数据库,防止缓存 + if (this.isDev()) { + DictTableWhiteListHandlerImpl.whiteTablesRuleMap.clear(); + } + // 如果map为空,则从数据库中查询 + if (DictTableWhiteListHandlerImpl.whiteTablesRuleMap.isEmpty()) { + Map ruleMap = sysTableWhiteListService.getAllConfigMap(); + log.debug("表字典白名单初始化完成:{}", ruleMap); + DictTableWhiteListHandlerImpl.whiteTablesRuleMap.putAll(ruleMap); + } + } + + @Override + public boolean isPassBySql(String sql) { + Map parsedMap = null; + try { + parsedMap = MiniDaoUtil.parseAllSelectTable(sql); + } catch (Exception e) { + log.warn("校验sql语句,解析报错:{}", e.getMessage()); + } + // 如果sql有问题,则肯定执行不了,所以直接返回true + if (parsedMap == null) { + return true; + } + log.debug("获取select sql信息 :{} ", parsedMap); + // 遍历当前sql中的所有表名,如果有其中一个表或表的字段不在白名单中,则不通过 + for (Map.Entry entry : parsedMap.entrySet()) { + SelectSqlInfo sqlInfo = entry.getValue(); + if (sqlInfo.isSelectAll()) { + log.warn("查询语句中包含 * 字段,暂时先通过"); + continue; + } + Set queryFields = sqlInfo.getAllRealSelectFields(); + // 校验表名和字段是否允许查询 + String tableName = entry.getKey(); + if (!this.checkWhiteList(tableName, queryFields)) { + return false; + } + } + return true; + } + + @Override + public boolean isPassByDict(String dictCodeString) { + if (oConvertUtils.isEmpty(dictCodeString)) { + return true; + } + try { + // 针对转义字符进行解码 + dictCodeString = URLDecoder.decode(dictCodeString, "UTF-8"); + } catch (Exception e) { + log.warn(e.getMessage()); + //this.throwException("字典code解码失败,可能是使用了非法字符,请检查!"); + } + dictCodeString = dictCodeString.trim(); + String[] arr = dictCodeString.split(SymbolConstant.COMMA); + // 获取表名 + String tableName = this.getTableName(arr[0]); + // 获取查询字段 + arr = Arrays.copyOfRange(arr, 1, arr.length); + // distinct的作用是去重,相当于 Set + String[] fields = Arrays.stream(arr).map(String::trim).distinct().toArray(String[]::new); + // 校验表名和字段是否允许查询 + return this.isPassByDict(tableName, fields); + } + + @Override + public boolean isPassByDict(String tableName, String... fields) { + if (oConvertUtils.isEmpty(tableName)) { + return true; + } + if (fields == null || fields.length == 0) { + fields = new String[]{"*"}; + } + String sql = "select " + String.join(",", fields) + " from " + tableName; + log.debug("字典拼接的查询SQL:{}", sql); + try { + // 进行SQL解析 + MiniDaoUtil.parseSelectSqlInfo(sql); + } catch (Exception e) { + // 如果SQL解析失败,则通过字段名和表名进行校验 + return checkWhiteList(tableName, new HashSet<>(Arrays.asList(fields))); + } + // 通过SQL解析进行校验,可防止SQL注入 + return this.isPassBySql(sql); + } + + /** + * 校验表名和字段是否在白名单内 + * + * @param tableName + * @param queryFields + * @return + */ + public boolean checkWhiteList(String tableName, Set queryFields) { + this.init(); + // 1、判断“表名”是否通过校验,如果为空则未通过校验 + if (oConvertUtils.isEmpty(tableName)) { + log.error("白名单校验:表名为空"); + this.throwException(); + } + // 统一转成小写 + tableName = tableName.toLowerCase(); + String allowFieldStr = DictTableWhiteListHandlerImpl.whiteTablesRuleMap.get(tableName); + log.debug("checkWhiteList tableName: {}", tableName); + if (oConvertUtils.isEmpty(allowFieldStr)) { + // 如果是dev模式,自动向数据库里添加数据 + if (this.isDev()) { + this.autoAddWhiteList(tableName, String.join(",", queryFields)); + allowFieldStr = DictTableWhiteListHandlerImpl.whiteTablesRuleMap.get(tableName); + } else { + // prod模式下,直接抛出异常 + log.error("白名单校验:表\"{}\"未通过校验", tableName); + this.throwException(); + } + } + // 2、判断“字段名”是否通过校验 + // 统一转成小写 + allowFieldStr = allowFieldStr.toLowerCase(); + Set allowFields = new HashSet<>(Arrays.asList(allowFieldStr.split(","))); + // 需要合并的字段 + Set waitMergerFields = new HashSet<>(); + for (String field : queryFields) { + if(oConvertUtils.isEmpty(field)){ + continue; + } + // 统一转成小写 + field = field.toLowerCase(); + // 如果允许的字段里不包含查询的字段,则直接抛出异常 + if (!allowFields.contains(field)) { + // 如果是dev模式,记录需要合并的字段 + if (this.isDev()) { + waitMergerFields.add(field); + } else { + log.error("白名单校验:字段 {} 不在 {} 范围内,拒绝访问!", field, allowFields); + this.throwException(); + } + } + } + // 自动向数据库中合并未通过的字段 + if (!waitMergerFields.isEmpty()) { + this.autoAddWhiteList(tableName, String.join(",", waitMergerFields)); + } + log.debug("白名单校验:查询表\"{}\",查询字段 {} 通过校验", tableName, queryFields); + return true; + } + + /** + * 自动添加白名单,如果数据库已有,则字段会自动合并 + * + * @param tableName + * @param allowFieldStr + */ + private void autoAddWhiteList(String tableName, String allowFieldStr) { + try { + SysTableWhiteList entity = sysTableWhiteListService.autoAdd(tableName, allowFieldStr); + DictTableWhiteListHandlerImpl.whiteTablesRuleMap.put(tableName, entity.getFieldName()); + log.warn("表\"{}\"未通过校验,且当前为 dev 模式,已自动向数据库中增加白名单数据。查询字段:{}", tableName, allowFieldStr); + } catch (Exception e) { + log.error("表\"{}\"未通过校验,且当前为 dev 模式,但自动向数据库中增加白名单数据失败,请排查后重试。错误原因:{}", tableName, e.getMessage(), e); + this.throwException(); + } + } + + /** + * 判断当前 LowCode 是否为 dev 模式 + */ + private boolean isDev() { + if (DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV == null) { + if (this.GhbBaseConfig.getFirewall() != null) { + String lowCodeMode = this.GhbBaseConfig.getFirewall().getLowCodeMode(); + DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV = LowCodeModeInterceptor.LOW_CODE_MODE_DEV.equals(lowCodeMode); + } else { + // 如果没有 firewall 配置,则默认为 false + DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV = false; + } + } + return DictTableWhiteListHandlerImpl.LOW_CODE_IS_DEV; + } + + @Override + public boolean clear() { + DictTableWhiteListHandlerImpl.whiteTablesRuleMap.clear(); + return true; + } + + + /** + * 取where前面的为:table name + * + * @param str + * @see DictQueryBlackListHandler#getTableName(String) + */ + @SuppressWarnings("JavadocReference") + private String getTableName(String str) { + String[] arr = str.split("\\s+(?i)where\\s+"); + String tableName = arr[0].trim(); + //【20230814】解决使用参数tableName=sys_user t&复测,漏洞仍然存在 + if (tableName.contains(".")) { + tableName = tableName.substring(tableName.indexOf(".") + 1, tableName.length()).trim(); + } + if (tableName.contains(" ")) { + tableName = tableName.substring(0, tableName.indexOf(" ")).trim(); + } + + //【issues/4393】 sys_user , (sys_user), sys_user%20, %60sys_user%60 + String reg = "\\s+|\\(|\\)|`"; + return tableName.replaceAll(reg, ""); + } + + private void throwException() throws GhbSqlInjectionException { + this.throwException(this.getErrorMsg()); + } + + private void throwException(String message) throws GhbSqlInjectionException { + if (oConvertUtils.isEmpty(message)) { + message = this.getErrorMsg(); + } + log.error(message); + throw new GhbSqlInjectionException(message); + } + + @Override + public String getErrorMsg() { + return "白名单校验未通过!"; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/CodeGenerateDbConfig.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/CodeGenerateDbConfig.java new file mode 100644 index 0000000..a6846d1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/CodeGenerateDbConfig.java @@ -0,0 +1,54 @@ +package com.ghb.base.config.init; + +import com.alibaba.druid.filter.config.ConfigTools; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jeecgframework.codegenerate.database.CodegenDatasourceConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @Description: 代码生成器,自定义DB配置 + * 【加了此类,则online模式DB连接,使用平台的配置,Ghb_database.properties配置无效; + * 但是使用GUI模式代码生成,还是走Ghb_database.properties配置】 + * 提醒: 达梦数据库需要修改下面的参数${spring.datasource.dynamic.datasource.master.url:}配置 + * @author: scott + * @date: 2021年02月18日 16:30 + * + * 重要说明:此类改路径或者名称,需要同步修改 + * org/Ghb/interceptor/OnlineRepairCodeGenerateDbConfig.java里面的注解 + * @ConditionalOnMissingClass("com.ghb.base.config.init.CodeGenerateDbConfig") + */ +@Slf4j +@Configuration +public class CodeGenerateDbConfig { + @Value("${spring.datasource.dynamic.datasource.master.url:}") + private String url; + @Value("${spring.datasource.dynamic.datasource.master.username:}") + private String username; + @Value("${spring.datasource.dynamic.datasource.master.password:}") + private String password; + @Value("${spring.datasource.dynamic.datasource.master.driver-class-name:}") + private String driverClassName; + @Value("${spring.datasource.dynamic.datasource.master.druid.public-key:}") + private String publicKey; + + + @Bean + public CodeGenerateDbConfig initCodeGenerateDbConfig() { + if(StringUtils.isNotBlank(url)){ + if(StringUtils.isNotBlank(publicKey)){ + try { + password = ConfigTools.decrypt(publicKey, password); + } catch (Exception e) { + e.printStackTrace(); + log.error(" 代码生成器数据库连接,数据库密码解密失败!"); + } + } + CodegenDatasourceConfig.initDbConfig(driverClassName,url, username, password); + log.info(" Init CodeGenerate Config [ Get Db Config From application.yml ] "); + } + return null; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/CodeTemplateInitListener.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/CodeTemplateInitListener.java new file mode 100644 index 0000000..9e0bcdb --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/CodeTemplateInitListener.java @@ -0,0 +1,69 @@ +package com.ghb.base.config.init; + +import cn.hutool.core.io.FileUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * 自动初始化代码生成器模板 + *

+ * 解决JAR发布需要手工配置代码生成器模板问题 + * @author zhang + */ +@Slf4j +@Component +public class CodeTemplateInitListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + try { + long startTime = System.currentTimeMillis(); // 记录开始时间 + log.info(" Init Code Generate Template [ 检测如果是JAR启动,Copy模板到config目录 ] "); + this.initJarConfigCodeGeneratorTemplate(); + long endTime = System.currentTimeMillis(); // 记录结束时间 + log.info(" Init Code Generate Template completed in " + (endTime - startTime) + " ms"); // 计算并记录耗时 + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * ::Jar包启动模式下:: + * 初始化代码生成器模板文件 + */ + private void initJarConfigCodeGeneratorTemplate() throws Exception { + //1.获取jar同级下的config路径 + String configPath = System.getProperty("user.dir") + File.separator + "config" + File.separator; + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources("classpath*:Ghb/code-template-online/**/*"); + for (Resource re : resources) { + URL url = re.getURL(); + String filepath = url.getPath(); + //System.out.println("native url= " + filepath); + filepath = java.net.URLDecoder.decode(filepath, "utf-8"); + //System.out.println("decode url= " + filepath); + + //2.在config下,创建Ghb/code-template-online/*模板 + String createFilePath = configPath + filepath.substring(filepath.indexOf("Ghb/code-template-online")); + + // 非jar模式不生成模板 + // 不生成目录,只生成具体模板文件 + if ((!filepath.contains(".jar!/BOOT-INF/lib/") && !filepath.contains(".jar/!BOOT-INF/lib/")) || !createFilePath.contains(".")) { + continue; + } + if (!FileUtil.exist(createFilePath)) { + log.info("create file codeTemplate = " + createFilePath); + FileUtil.writeString(IOUtils.toString(url, StandardCharsets.UTF_8), createFilePath, "UTF-8"); + } + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/ShiroCacheClearRunner.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/ShiroCacheClearRunner.java new file mode 100644 index 0000000..82c1978 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/ShiroCacheClearRunner.java @@ -0,0 +1,33 @@ +package com.ghb.base.config.init; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.constant.CommonConstant; +import org.jeecg.common.util.RedisUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +/** + * Shiro缓存清理 + * 在应用启动时清除所有的Shiro授权缓存 + * 主要用于解决重启项目,用户未重新登录,按钮权限不生效的问题 + */ +@Slf4j +@Component +@ConditionalOnBean(RedisTemplate.class) +public class ShiroCacheClearRunner implements ApplicationRunner { + + @Autowired + private RedisUtil redisUtil; + + @Override + public void run(ApplicationArguments args) { + // 清空所有授权redis缓存 + log.info("——— Service restart, clearing all user shiro authorization cache ——— "); + redisUtil.removeAll(CommonConstant.PREFIX_USER_SHIRO_CACHE); + + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/SystemInitListener.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/SystemInitListener.java new file mode 100644 index 0000000..ede1656 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/SystemInitListener.java @@ -0,0 +1,42 @@ +package com.ghb.base.config.init; + +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.config.GhbCloudCondition; +import com.ghb.base.modules.system.service.ISysGatewayRouteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; + +/** + * @desc: 启动程序,初始化路由配置 + * @author: flyme + */ +@Slf4j +@Component +@Conditional(GhbCloudCondition.class) +public class SystemInitListener implements ApplicationListener, Ordered { + + + @Autowired + private ISysGatewayRouteService sysGatewayRouteService; + + @Override + public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { + + log.info(" 服务已启动,初始化路由配置 ###################"); + String context = "AnnotationConfigServletWebServerApplicationContext"; + if (applicationReadyEvent.getApplicationContext().getDisplayName().indexOf(context) > -1) { + sysGatewayRouteService.addRoute2Redis(CacheConstant.GATEWAY_ROUTES); + } + + } + + @Override + public int getOrder() { + return 1; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/TomcatFactoryConfig.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/TomcatFactoryConfig.java new file mode 100644 index 0000000..10fc372 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/TomcatFactoryConfig.java @@ -0,0 +1,33 @@ +package com.ghb.base.config.init; + +import org.apache.catalina.Context; +import org.apache.tomcat.util.scan.StandardJarScanner; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @Description: TomcatFactoryConfig + * @author: scott + * @date: 2021年01月25日 11:40 + */ +@Configuration +public class TomcatFactoryConfig { + /** + * tomcat-embed-jasper引用后提示jar找不到的问题 + */ + @Bean + public TomcatServletWebServerFactory tomcatFactory() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() { + @Override + protected void postProcessContext(Context context) { + ((StandardJarScanner) context.getJarScanner()).setScanManifest(false); + } + }; + factory.addConnectorCustomizers(connector -> { + connector.setProperty("relaxedPathChars", "[]{}"); + connector.setProperty("relaxedQueryChars", "[]{}"); + }); + return factory; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/UndertowConfiguration.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/UndertowConfiguration.java new file mode 100644 index 0000000..25e0454 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/init/UndertowConfiguration.java @@ -0,0 +1,48 @@ +//package com.ghb.base.config.init; +// +//import io.undertow.UndertowOptions; +//import io.undertow.server.DefaultByteBufferPool; +//import io.undertow.server.handlers.BlockingHandler; +//import io.undertow.websockets.jsr.WebSocketDeploymentInfo; +//import com.ghb.base.modules.monitor.actuator.undertow.CustomUndertowMetricsHandler; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; +//import org.springframework.boot.web.server.WebServerFactoryCustomizer; +//import org.springframework.context.annotation.Configuration; +// +///** +// * Undertow配置 +// * +// * 解决启动提示: WARN io.undertow.websockets.jsr:68 - UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used +// */ +//@Configuration +//public class UndertowConfiguration implements WebServerFactoryCustomizer { +// +// /** +// * 自定义undertow监控指标工具类 +// * for [QQYUN-11902]tomcat 替换undertow 这里的功能还没修改 +// */ +// @Autowired +// private CustomUndertowMetricsHandler customUndertowMetricsHandler; +// +// @Override +// public void customize(UndertowServletWebServerFactory factory) { +// // 设置 Undertow 服务器参数(底层网络配置) +// factory.addBuilderCustomizers(builder -> { +// builder.setServerOption(UndertowOptions.MAX_HEADER_SIZE, 65536); // header 最大64KB +// builder.setServerOption(UndertowOptions.MAX_PARAMETERS, 10000); // 最大参数数 +// }); +// factory.addDeploymentInfoCustomizers(deploymentInfo -> { +// +// WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo(); +// +// // 设置合理的参数 +// webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 8192)); +// +// deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo); +// +// // 添加自定义 监控 handler +// deploymentInfo.addInitialHandlerChainWrapper(next -> new BlockingHandler(customUndertowMetricsHandler.wrap(next))); +// }); +// } +//} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/jimureport/JimuDragExternalServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/jimureport/JimuDragExternalServiceImpl.java new file mode 100644 index 0000000..b4c9d4b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/jimureport/JimuDragExternalServiceImpl.java @@ -0,0 +1,124 @@ +package com.ghb.base.config.jimureport; + +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.dto.LogDTO; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.vo.DictModel; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.base.service.BaseCommonService; +import org.jeecg.modules.drag.service.IOnlDragExternalService; +import org.jeecg.modules.drag.vo.DragDictModel; +import org.jeecg.modules.drag.vo.DragLogDTO; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Description: 字典处理 + * @Author: lsq + * @Date:2023-01-09 + * @Version:V1.0 + */ +@Slf4j +@Service("onlDragExternalServiceImpl") +public class JimuDragExternalServiceImpl implements IOnlDragExternalService { + + @Autowired + @Lazy + private BaseCommonService baseCommonService; + + @Autowired + @Lazy + private ISysBaseAPI sysBaseApi; + /** + * 根据多个字典code查询多个字典项 + * @param codeList + * @return key = dictCode ; value=对应的字典项 + */ + @Override + public Map> getManyDictItems(List codeList, List tableDictList) { + Map> manyDragDictItems = new HashMap<>(); + if(!CollectionUtils.isEmpty(codeList)){ + Map> dictItemsMap = sysBaseApi.getManyDictItems(codeList); + dictItemsMap.forEach((k,v)->{ + List dictItems = new ArrayList<>(); + v.forEach(dictItem->{ + DragDictModel dictModel = new DragDictModel(); + BeanUtils.copyProperties(dictItem,dictModel); + dictItems.add(dictModel); + }); + manyDragDictItems.put(k,dictItems); + }); + } + + if(!CollectionUtils.isEmpty(tableDictList)){ + tableDictList.forEach(item->{ + List dictItems = new ArrayList<>(); + JSONObject object = JSONObject.parseObject(item.toString()); + String dictField = object.getString("dictField"); + String dictTable = object.getString("dictTable"); + String dictText = object.getString("dictText"); + String fieldName = object.getString("fieldName"); + List dictItemsList = sysBaseApi.queryTableDictItemsByCode(dictTable,dictText,dictField); + dictItemsList.forEach(dictItem->{ + DragDictModel dictModel = new DragDictModel(); + BeanUtils.copyProperties(dictItem,dictModel); + dictItems.add(dictModel); + }); + manyDragDictItems.put(fieldName,dictItems); + }); + } + return manyDragDictItems; + } + + /** + * + * @param dictCode + * @return + */ + @Override + public List getDictItems(String dictCode) { + List dictItems = new ArrayList<>(); + if(oConvertUtils.isNotEmpty(dictCode)){ + List dictItemsList = sysBaseApi.getDictItems(dictCode); + dictItemsList.forEach(dictItem->{ + DragDictModel dictModel = new DragDictModel(); + BeanUtils.copyProperties(dictItem,dictModel); + dictItems.add(dictModel); + }); + } + return dictItems; + } + + /** + * 添加日志 + * @param dragLogDTO + */ + @Override + public void addLog(DragLogDTO dragLogDTO) { + if(oConvertUtils.isNotEmpty(dragLogDTO)){ + LogDTO dto = new LogDTO(); + BeanUtils.copyProperties(dragLogDTO,dto); + baseCommonService.addLog(dto); + } + } + + /** + * 保存日志 + * @param logMsg + * @param logType + * @param operateType + */ + @Override + public void addLog(String logMsg, int logType, int operateType) { + baseCommonService.addLog(logMsg,logType,operateType); + } +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/jimureport/JimuReportTokenService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/jimureport/JimuReportTokenService.java new file mode 100644 index 0000000..70ae808 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/config/jimureport/JimuReportTokenService.java @@ -0,0 +1,130 @@ +package com.ghb.base.config.jimureport; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.DictModel; +import com.ghb.base.common.system.vo.SysUserCacheInfo; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.TokenUtils; +import com.ghb.base.common.util.oConvertUtils; +import org.jeecg.modules.jmreport.api.JmReportTokenServiceI; +import org.jeecg.modules.jmreport.common.vo.JmDictModel; +import com.ghb.base.modules.system.service.impl.SysBaseApiImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.*; + +/** + * 自定义积木报表鉴权(如果不进行自定义,则所有请求不做权限控制) + * * 1.自定义获取登录token + * * 2.自定义获取登录用户 + * @author: Ghb-boot + */ + + +@Slf4j +@Component +public class JimuReportTokenService implements JmReportTokenServiceI { + @Autowired + private SysBaseApiImpl sysBaseApi; + @Autowired + @Lazy + private RedisUtil redisUtil; + + @Override + public String getToken(HttpServletRequest request) { + try { + return TokenUtils.getTokenByRequest(request); + } catch (Exception e) { + return null; + } + } + + @Override + public String getUsername(String token) { + return JwtUtil.getUsername(token); + } + + @Override + public String[] getRoles(String token) { + String username = JwtUtil.getUsername(token); + Set roles = sysBaseApi.getUserRoleSet(username); + if(CollectionUtils.isEmpty(roles)){ + return null; + } + return (String[]) roles.toArray(new String[roles.size()]); + } + + @Override + public Boolean verifyToken(String token) { + return TokenUtils.verifyToken(token, sysBaseApi, redisUtil); + } + + @Override + public Map getUserInfo(String token) { + Map map = new HashMap(5); + String username = JwtUtil.getUsername(token); + //此处通过token只能拿到一个信息 用户账号 后面的就是根据账号获取其他信息 查询数据或是走redis 用户根据自身业务可自定义 + SysUserCacheInfo userInfo = null; + try { + userInfo = sysBaseApi.getCacheUser(username); + } catch (Exception e) { + log.error("获取用户信息异常:"+ e.getMessage()); + return map; + } + //设置账号名 + map.put(SYS_USER_CODE, userInfo.getSysUserCode()); + //设置部门编码 + map.put(SYS_ORG_CODE, userInfo.getSysOrgCode()); + // 将所有信息存放至map 解析sql/api会根据map的键值解析 + return map; + } + + /** + * 将Ghbboot平台的权限传递给积木报表 + * @param token + * @return + */ + @Override + public String[] getPermissions(String token) { + // 获取用户信息 + String username = JwtUtil.getUsername(token); + SysUserCacheInfo userInfo = null; + try { + userInfo = sysBaseApi.getCacheUser(username); + } catch (Exception e) { + log.error("获取用户信息异常:"+ e.getMessage()); + } + if(userInfo == null){ + return null; + } + // 查询权限 + Set userPermissions = sysBaseApi.getUserPermissionSet(userInfo.getSysUserId()); + if(CollectionUtils.isEmpty(userPermissions)){ + return null; + } + return userPermissions.toArray(new String[0]); + } + + //TODO 待升级积木报表依赖版本后启用 +// @Override + public List getDictItems(String dictCode) { + List dictItems = new ArrayList<>(); + if(oConvertUtils.isNotEmpty(dictCode)){ + List dictItemsList = sysBaseApi.getDictItems(dictCode); + dictItemsList.forEach(dictItem->{ + JmDictModel dictModel = new JmDictModel(); + dictModel.setText(dictItem.getText()); + dictModel.setValue(dictItem.getValue()); + dictModel.setDictCode(dictCode); + dictItems.add(dictModel); + }); + } + return dictItems; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/airag/GhbBizToolsProvider.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/airag/GhbBizToolsProvider.java new file mode 100644 index 0000000..3548e04 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/airag/GhbBizToolsProvider.java @@ -0,0 +1,9 @@ +package com.ghb.base.modules.airag; + +/** + * airag(AI 大模型)模块已从 ghb-base 移除——其依赖 jeecg-boot-module-airag 未在公共仓发布、 + * 且 langchain4j 仅随该 jar 传递引入。原 GhbBizToolsProvider 实现的 llm 工具扩展随之作废。 + * 本文件为占位空壳,待最终清理时整体删除(连同 modules/airag 目录)。 + */ +class GhbBizToolsProvider { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/aop/TenantLog.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/aop/TenantLog.java new file mode 100644 index 0000000..bf58e2c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/aop/TenantLog.java @@ -0,0 +1,27 @@ +package com.ghb.base.modules.aop; + +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.enums.ModuleType; + +import java.lang.annotation.*; + +/** + * 系统日志注解 + * + * @Author scott + * @email Ghbos@163.com + * @Date 2019年1月14日 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantLog { + + /** + * 操作日志类型(1查询,2添加,3修改,4删除) + * + * @return + */ + int value() default 0; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/aop/TenantPackUserLogAspect.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/aop/TenantPackUserLogAspect.java new file mode 100644 index 0000000..272917d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/aop/TenantPackUserLogAspect.java @@ -0,0 +1,100 @@ +package com.ghb.base.modules.aop; + +import org.apache.shiro.SecurityUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import com.ghb.base.common.api.dto.LogDTO; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.entity.SysTenantPack; +import com.ghb.base.modules.system.entity.SysTenantPackUser; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.lang.reflect.Method; +import java.util.Date; + +/** + * @Author taoYan + * @Date 2023/2/16 14:27 + **/ +@Aspect +@Component +public class TenantPackUserLogAspect { + + @Resource + private BaseCommonService baseCommonService; + + @Pointcut("@annotation(com.ghb.base.modules.aop.TenantLog)") + public void tenantLogPointCut() { + + } + + @Around("tenantLogPointCut()") + public Object aroundMethod(ProceedingJoinPoint joinPoint)throws Throwable { + //System.out.println("环绕通知>>>>>>>>>"); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + TenantLog log = method.getAnnotation(TenantLog.class); + if(log != null){ + int opType = log.value(); + Integer logType = null; + String content = null; + Integer tenantId = null; + //获取参数 + Object[] args = joinPoint.getArgs(); + if(args.length>0){ + for(Object obj: args){ + if(obj instanceof SysTenantPack){ + // logType=3 租户操作日志 + logType = CommonConstant.LOG_TYPE_3; + SysTenantPack pack = (SysTenantPack)obj; + if(opType==2){ + content = "创建了角色权限 "+ pack.getPackName(); + } + tenantId = pack.getTenantId(); + break; + }else if(obj instanceof SysTenantPackUser){ + logType = CommonConstant.LOG_TYPE_3; + SysTenantPackUser packUser = (SysTenantPackUser)obj; + if(opType==2){ + content = "将 "+packUser.getRealname()+" 添加到角色 "+ packUser.getPackName(); + }else if(opType==4){ + content = "移除了 "+packUser.getPackName()+" 成员 "+ packUser.getRealname(); + } + tenantId = packUser.getTenantId(); + } + } + } + if(logType!=null){ + LogDTO dto = new LogDTO(); + dto.setLogType(logType); + dto.setLogContent(content); + dto.setOperateType(opType); + dto.setTenantId(tenantId); + //获取登录用户信息 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + if(sysUser!=null){ + dto.setUserid(sysUser.getUsername()); + dto.setUsername(sysUser.getRealname()); + + } + dto.setCreateTime(new Date()); + //保存系统日志 + baseCommonService.addLog(dto); + } + } + return joinPoint.proceed(); + } + + @AfterThrowing("tenantLogPointCut()") + public void afterThrowing()throws Throwable{ + System.out.println("异常通知"); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/api/controller/SystemApiController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/api/controller/SystemApiController.java new file mode 100644 index 0000000..ba1ada6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/api/controller/SystemApiController.java @@ -0,0 +1,1129 @@ +package com.ghb.base.modules.api.controller; + +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.dto.DataLogDTO; +import com.ghb.base.common.api.dto.OnlineAuthDTO; +import com.ghb.base.common.api.dto.PushMessageDTO; +import com.ghb.base.common.api.dto.message.*; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.enums.DySmsEnum; +import com.ghb.base.common.constant.enums.EmailTemplateEnum; +import com.ghb.base.common.desensitization.util.SensitiveInfoUtil; +import com.ghb.base.common.system.vo.*; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.service.impl.SysBaseApiImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * 服务化 system模块 对外接口请求类 + * @author: Ghb-boot + */ +@Slf4j +@RestController +@RequestMapping("/sys/api") +public class SystemApiController { + + @Autowired + private SysBaseApiImpl sysBaseApi; + @Autowired + private ISysUserService sysUserService; + + /** + * 发送系统消息 + * @param message 使用构造器赋值参数 如果不设置category(消息类型)则默认为2 发送系统消息 + */ + @PostMapping("/sendSysAnnouncement") + public void sendSysAnnouncement(@RequestBody MessageDTO message){ + sysBaseApi.sendSysAnnouncement(message); + } + + /** + * 发送消息 附带业务参数 + * @param message 使用构造器赋值参数 + */ + @PostMapping("/sendBusAnnouncement") + public void sendBusAnnouncement(@RequestBody BusMessageDTO message){ + sysBaseApi.sendBusAnnouncement(message); + } + + /** + * 通过模板发送消息 + * @param message 使用构造器赋值参数 + */ + @PostMapping("/sendTemplateAnnouncement") + public void sendTemplateAnnouncement(@RequestBody TemplateMessageDTO message){ + sysBaseApi.sendTemplateAnnouncement(message); + } + + /** + * 通过模板发送消息 附带业务参数 + * @param message 使用构造器赋值参数 + */ + @PostMapping("/sendBusTemplateAnnouncement") + public void sendBusTemplateAnnouncement(@RequestBody BusTemplateMessageDTO message){ + sysBaseApi.sendBusTemplateAnnouncement(message); + } + + /** + * 通过消息中心模板,生成推送内容 + * @param templateDTO 使用构造器赋值参数 + * @return + */ + @PostMapping("/parseTemplateByCode") + public String parseTemplateByCode(@RequestBody TemplateDTO templateDTO){ + return sysBaseApi.parseTemplateByCode(templateDTO); + } + + /** + * 根据业务类型busType及业务busId修改消息已读 + */ + @GetMapping("/updateSysAnnounReadFlag") + public void updateSysAnnounReadFlag(@RequestParam("busType") String busType, @RequestParam("busId")String busId){ + sysBaseApi.updateSysAnnounReadFlag(busType, busId); + } + + /** + * 根据用户账号查询用户信息 + * @param username + * @return + */ + @GetMapping("/getUserByName") + public LoginUser getUserByName(@RequestParam("username") String username){ + LoginUser loginUser = sysBaseApi.getUserByName(username); + //用户信息加密 + try { + SensitiveInfoUtil.handlerObject(loginUser, true); + } catch (IllegalAccessException e) { + log.error(e.getMessage(), e); + } + return loginUser; + } + + /** + * 根据用户账号查询用户ID + * @param username + * @return + */ + @GetMapping("/getUserIdByName") + public String getUserIdByName(@RequestParam("username") String username){ + String userId = sysBaseApi.getUserIdByName(username); + return userId; + } + + /** + * 根据用户id查询用户信息 + * @param id + * @return + */ + @GetMapping("/getUserById") + LoginUser getUserById(@RequestParam("id") String id){ + LoginUser loginUser = sysBaseApi.getUserById(id); + //用户信息加密 + try { + SensitiveInfoUtil.handlerObject(loginUser, true); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return loginUser; + } + + /** + * 通过用户账号查询角色集合 + * @param username + * @return + */ + @GetMapping("/getRolesByUsername") + List getRolesByUsername(@RequestParam("username") String username){ + return sysBaseApi.getRolesByUsername(username); + } + + /** + * 通过用户账号查询角色集合 + * @param userId + * @return + */ + @GetMapping("/getRolesByUserId") + List getRolesByUserId(@RequestParam("userId") String userId){ + return sysBaseApi.getRolesByUserId(userId); + } + + /** + * 通过用户账号查询部门集合 + * @param username + * @return 部门 id + */ + @GetMapping("/getDepartIdsByUsername") + List getDepartIdsByUsername(@RequestParam("username") String username){ + return sysBaseApi.getDepartIdsByUsername(username); + } + + /** + * 通过用户账号查询部门集合 + * @param userId + * @return 部门 id + */ + @GetMapping("/getDepartIdsByUserId") + List getDepartIdsByUserId(@RequestParam("userId") String userId){ + return sysBaseApi.getDepartIdsByUserId(userId); + } + + /** + * 通过用户账号查询部门父ID集合 + * @param username + * @return 部门 id + */ + @GetMapping("/getDepartParentIdsByUsername") + Set getDepartParentIdsByUsername(@RequestParam("username") String username){ + return sysBaseApi.getDepartParentIdsByUsername(username); + } + + /** + * 查询部门父ID集合 + * @param depIds + * @return 部门 id + */ + @GetMapping("/getDepartParentIdsByDepIds") + Set getDepartParentIdsByDepIds(@RequestParam("depIds") Set depIds){ + return sysBaseApi.getDepartParentIdsByDepIds(depIds); + } + + /** + * 通过 userIds 查询部门ID列表 + * + * @param userIds + * @return key = userId; value = 用户拥有的部门ID列表 + */ + @GetMapping("/getDepartIdsByUserIds") + Map> getDepartIdsByUserIds(@RequestParam("userIds") Collection userIds) { + return sysBaseApi.getDepartIdsByUserIds(userIds); + } + + /** + * 通过用户账号查询部门 name + * @param username + * @return 部门 name + */ + @GetMapping("/getDepartNamesByUsername") + List getDepartNamesByUsername(@RequestParam("username") String username){ + return sysBaseApi.getDepartNamesByUsername(username); + } + + + /** + * 获取数据字典 + * @param code + * @return + */ + @GetMapping("/queryDictItemsByCode") + List queryDictItemsByCode(@RequestParam("code") String code){ + return sysBaseApi.queryDictItemsByCode(code); + } + + /** + * 获取有效的数据字典 + * @param code + * @return + */ + @GetMapping("/queryEnableDictItemsByCode") + List queryEnableDictItemsByCode(@RequestParam("code") String code){ + return sysBaseApi.queryEnableDictItemsByCode(code); + } + + + /** 查询所有的父级字典,按照create_time排序 */ + @GetMapping("/queryAllDict") + List queryAllDict(){ +// try{ +// //睡10秒,gateway网关5秒超时,会触发熔断降级操作 +// Thread.sleep(10000); +// }catch (Exception e){ +// e.printStackTrace(); +// } + + log.info("--我是Ghb-system服务节点,微服务接口queryAllDict被调用--"); + return sysBaseApi.queryAllDict(); + } + + /** + * 查询所有分类字典 + * @return + */ + @GetMapping("/queryAllSysCategory") + List queryAllSysCategory(){ + return sysBaseApi.queryAllSysCategory(); + } + + + /** + * 查询所有部门 作为字典信息 id -->value,departName -->text + * @return + */ + @GetMapping("/queryAllDepartBackDictModel") + List queryAllDepartBackDictModel(){ + return sysBaseApi.queryAllDepartBackDictModel(); + } + + /** + * 获取所有角色 带参 + * roleIds 默认选中角色 + * @return + */ + @GetMapping("/queryAllRole") + public List queryAllRole(@RequestParam(name = "roleIds",required = false)String[] roleIds){ + if(roleIds==null || roleIds.length==0){ + return sysBaseApi.queryAllRole(); + }else{ + return sysBaseApi.queryAllRole(roleIds); + } + } + + /** + * 通过用户账号查询角色Id集合 + * @param username + * @return + */ + @GetMapping("/getRoleIdsByUsername") + public List getRoleIdsByUsername(@RequestParam("username")String username){ + return sysBaseApi.getRoleIdsByUsername(username); + } + + /** + * 通过部门编号查询部门id + * @param orgCode + * @return + */ + @GetMapping("/getDepartIdsByOrgCode") + public String getDepartIdsByOrgCode(@RequestParam("orgCode")String orgCode){ + return sysBaseApi.getDepartIdsByOrgCode(orgCode); + } + + /** + * 查询所有部门 + * @return + */ + @GetMapping("/getAllSysDepart") + public List getAllSysDepart(){ + return sysBaseApi.getAllSysDepart(); + } + + /** + * 根据 id 查询数据库中存储的 DynamicDataSourceModel + * + * @param dbSourceId + * @return + */ + @GetMapping("/getDynamicDbSourceById") + DynamicDataSourceModel getDynamicDbSourceById(@RequestParam("dbSourceId")String dbSourceId){ + return sysBaseApi.getDynamicDbSourceById(dbSourceId); + } + + + + /** + * 根据部门Id获取部门负责人 + * @param deptId + * @return + */ + @GetMapping("/getDeptHeadByDepId") + public List getDeptHeadByDepId(@RequestParam("deptId") String deptId){ + return sysBaseApi.getDeptHeadByDepId(deptId); + } + + /** + * 查找父级部门 + * @param departId + * @return + */ + @GetMapping("/getParentDepartId") + public DictModel getParentDepartId(@RequestParam("departId")String departId){ + return sysBaseApi.getParentDepartId(departId); + } + + /** + * 根据 code 查询数据库中存储的 DynamicDataSourceModel + * + * @param dbSourceCode + * @return + */ + @GetMapping("/getDynamicDbSourceByCode") + public DynamicDataSourceModel getDynamicDbSourceByCode(@RequestParam("dbSourceCode") String dbSourceCode){ + return sysBaseApi.getDynamicDbSourceByCode(dbSourceCode); + } + + /** + * 给指定用户发消息 + * @param userIds + * @param cmd + */ + @GetMapping("/sendWebSocketMsg") + public void sendWebSocketMsg(String[] userIds, String cmd){ + sysBaseApi.sendWebSocketMsg(userIds, cmd); + } + + + /** + * 根据id获取所有参与用户 + * userIds + * @return + */ + @GetMapping("/queryAllUserByIds") + public List queryAllUserByIds(@RequestParam("userIds") String[] userIds){ + return sysBaseApi.queryAllUserByIds(userIds); + } + + /** + * 查询所有用户 返回ComboModel + * @return + */ + @GetMapping("/queryAllUserBackCombo") + public List queryAllUserBackCombo(){ + return sysBaseApi.queryAllUserBackCombo(); + } + + /** + * 分页查询用户 返回JSONObject + * @return + */ + @GetMapping("/queryAllUser") + public JSONObject queryAllUser(@RequestParam(name="userIds",required=false)String userIds, @RequestParam(name="pageNo",required=false) Integer pageNo,@RequestParam(name="pageSize",required=false) Integer pageSize){ + return sysBaseApi.queryAllUser(userIds, pageNo, pageSize); + } + + + + /** + * 将会议签到信息推动到预览 + * userIds + * @return + * @param userId + */ + @GetMapping("/meetingSignWebsocket") + public void meetingSignWebsocket(@RequestParam("userId")String userId){ + sysBaseApi.meetingSignWebsocket(userId); + } + + /** + * 根据name获取所有参与用户 + * userNames + * @return + */ + @GetMapping("/queryUserByNames") + public List queryUserByNames(@RequestParam("userNames")String[] userNames){ + return sysBaseApi.queryUserByNames(userNames); + } + + /** + * 获取用户的角色集合 + * @param username + * @return + */ + @GetMapping("/getUserRoleSet") + public Set getUserRoleSet(@RequestParam("username")String username){ + return sysBaseApi.getUserRoleSet(username); + } + + /** + * 获取用户的角色集合 + * @param userId + * @return + */ + @GetMapping("/getUserRoleSetById") + public Set getUserRoleSetById(@RequestParam("userId")String userId){ + return sysBaseApi.getUserRoleSetById(userId); + } + + /** + * 获取用户的权限集合 + * @param userId 用户表ID + * @return + */ + @GetMapping("/getUserPermissionSet") + public Set getUserPermissionSet(@RequestParam("userId") String userId){ + return sysBaseApi.getUserPermissionSet(userId); + } + + //----- + + /** + * 判断是否有online访问的权限 + * @param onlineAuthDTO + * @return + */ + @PostMapping("/hasOnlineAuth") + public boolean hasOnlineAuth(@RequestBody OnlineAuthDTO onlineAuthDTO){ + return sysBaseApi.hasOnlineAuth(onlineAuthDTO); + } + + /** + * 查询用户角色信息 + * @param username + * @return + */ + @GetMapping("/queryUserRoles") + public Set queryUserRoles(@RequestParam("username") String username){ + return sysUserService.getUserRolesSet(username); + } + + /** + * 查询用户角色信息 + * @param userId + * @return + */ + @GetMapping("/queryUserRolesById") + public Set queryUserRolesById(@RequestParam("userId") String userId){ + return sysUserService.getUserRoleSetById(userId); + } + + + /** + * 查询用户权限信息 + * @param userId + * @return + */ + @GetMapping("/queryUserAuths") + public Set queryUserAuths(@RequestParam("userId") String userId){ + return sysUserService.getUserPermissionsSet(userId); + } + + /** + * 通过部门id获取部门全部信息 + */ + @GetMapping("/selectAllById") + public SysDepartModel selectAllById(@RequestParam("id") String id){ + return sysBaseApi.selectAllById(id); + } + + /** + * 根据用户id查询用户所属公司下所有用户ids + * @param userId + * @return + */ + @GetMapping("/queryDeptUsersByUserId") + public List queryDeptUsersByUserId(@RequestParam("userId") String userId){ + return sysBaseApi.queryDeptUsersByUserId(userId); + } + + + /** + * 查询数据权限 + * @return + */ + @GetMapping("/queryPermissionDataRule") + public List queryPermissionDataRule(@RequestParam("component") String component, @RequestParam("requestPath")String requestPath, @RequestParam("username") String username){ + return sysBaseApi.queryPermissionDataRule(component, requestPath, username); + } + + /** + * 查询用户信息 + * @param username + * @return + */ + @GetMapping("/getCacheUser") + public SysUserCacheInfo getCacheUser(@RequestParam("username") String username){ + return sysBaseApi.getCacheUser(username); + } + + /** + * 普通字典的翻译 + * @param code + * @param key + * @return + */ + @GetMapping("/translateDict") + public String translateDict(@RequestParam("code") String code, @RequestParam("key") String key){ + return sysBaseApi.translateDict(code, key); + } + + + /** + * 36根据多个用户账号(逗号分隔),查询返回多个用户信息 + * @param usernames + * @return + */ + @RequestMapping("/queryUsersByUsernames") + List queryUsersByUsernames(@RequestParam("usernames") String usernames){ + return this.sysBaseApi.queryUsersByUsernames(usernames); + } + + /** + * 37根据多个用户id(逗号分隔),查询返回多个用户信息 + * @param ids + * @return + */ + @RequestMapping("/queryUsersByIds") + List queryUsersByIds(@RequestParam("ids") String ids){ + return this.sysBaseApi.queryUsersByIds(ids); + } + + /** + * 38根据多个部门编码(逗号分隔),查询返回多个部门信息 + * @param orgCodes + * @return + */ + @GetMapping("/queryDepartsByOrgcodes") + List queryDepartsByOrgcodes(@RequestParam("orgCodes") String orgCodes){ + return this.sysBaseApi.queryDepartsByOrgcodes(orgCodes); + } + + /** + * 39根据多个部门ID(逗号分隔),查询返回多个部门信息 + * @param ids + * @return + */ + @GetMapping("/queryDepartsByIds") + List queryDepartsByIds(@RequestParam("ids") String ids){ + return this.sysBaseApi.queryDepartsByIds(ids); + } + + /** + * 40发送邮件消息 + * @param email + * @param title + * @param content + */ + @GetMapping("/sendEmailMsg") + public void sendEmailMsg(@RequestParam("email")String email,@RequestParam("title")String title,@RequestParam("content")String content){ + this.sysBaseApi.sendEmailMsg(email,title,content); + }; + /** + * 发送html模版邮件消息 + * @param email + * @param title + * @param emailTemplateEnum 邮件模版枚举 + * @param params 模版参数 + */ + @GetMapping("/sendHtmlTemplateEmail") + public void sendHtmlTemplateEmail(@RequestParam("email")String email, @RequestParam("title")String title, @RequestParam("emailEnum") EmailTemplateEnum emailTemplateEnum, @RequestParam("params") JSONObject params){ + this.sysBaseApi.sendHtmlTemplateEmail(email,title,emailTemplateEnum,params); + }; + /** + * 发送短信消息 + * @param phone 手机号码 + * @param params 模版参数 + * @param dySmsEnum 短信模版枚举 + */ + @GetMapping("/sendSmsMsg") + public void sendSmsMsg(@RequestParam("phone")String phone, @RequestParam("params") JSONObject params, @RequestParam("dySmsEnum") DySmsEnum dySmsEnum){ + this.sysBaseApi.sendSmsMsg(phone,params,dySmsEnum); + }; + /** + * 41 获取公司下级部门和公司下所有用户信息 + * @param orgCode + */ + @GetMapping("/getDeptUserByOrgCode") + List getDeptUserByOrgCode(@RequestParam("orgCode")String orgCode){ + return this.sysBaseApi.getDeptUserByOrgCode(orgCode); + } + + /** + * 查询分类字典翻译 + * + * @param ids 分类字典表id + * @return + */ + @GetMapping("/loadCategoryDictItem") + public List loadCategoryDictItem(@RequestParam("ids") String ids) { + return sysBaseApi.loadCategoryDictItem(ids); + } + + /** + * 反向翻译分类字典,用于导入 + * + * @param names 名称,逗号分割 + * @return + */ + @GetMapping("/loadCategoryDictItemByNames") + List loadCategoryDictItemByNames(@RequestParam("names") String names, @RequestParam("delNotExist") boolean delNotExist) { + return sysBaseApi.loadCategoryDictItemByNames(names, delNotExist); + } + + /** + * 根据字典code加载字典text + * + * @param dictCode 顺序:tableName,text,code + * @param keys 要查询的key + * @return + */ + @GetMapping("/loadDictItem") + public List loadDictItem(@RequestParam("dictCode") String dictCode, @RequestParam("keys") String keys) { + return sysBaseApi.loadDictItem(dictCode, keys); + } + + /** + * 复制应用下的所有字典配置到新的租户下 + * + * @param originalAppId 原始低代码应用ID + * @param appId 新的低代码应用ID + * @param tenantId 新的租户ID + * @return Map Map<原字典编码, 新字典编码> + */ + @GetMapping("/copyLowAppDict") + Map copyLowAppDict(@RequestParam("originalAppId") String originalAppId, @RequestParam("appId") String appId, @RequestParam("tenantId") String tenantId) { + return sysBaseApi.copyLowAppDict(originalAppId, appId, tenantId); + } + + /** + * 根据字典code查询字典项 + * + * @param dictCode 顺序:tableName,text,code + * @param dictCode 要查询的key + * @return + */ + @GetMapping("/getDictItems") + public List getDictItems(@RequestParam("dictCode") String dictCode) { + return sysBaseApi.getDictItems(dictCode); + } + + /** + * 根据多个字典code查询多个字典项 + * + * @param dictCodeList + * @return key = dictCode ; value=对应的字典项 + */ + @RequestMapping("/getManyDictItems") + public Map> getManyDictItems(@RequestParam("dictCodeList") List dictCodeList) { + return sysBaseApi.getManyDictItems(dictCodeList); + } + + /** + * 【下拉搜索】 + * 大数据量的字典表 走异步加载,即前端输入内容过滤数据 + * + * @param dictCode 字典code格式:table,text,code + * @param keyword 过滤关键字 + * @return + */ + @GetMapping("/loadDictItemByKeyword") + public List loadDictItemByKeyword(@RequestParam("dictCode") String dictCode, + @RequestParam("keyword") String keyword, + @RequestParam(value = "pageNo", defaultValue = "1", required = false) Integer pageNo, + @RequestParam(value = "pageSize", required = false) Integer pageSize) { + return sysBaseApi.loadDictItemByKeyword(dictCode, keyword,pageNo, pageSize); + } + + /** + * 48 普通字典的翻译,根据多个dictCode和多条数据,多个以逗号分割 + * @param dictCodes + * @param keys + * @return + */ + @GetMapping("/translateManyDict") + public Map> translateManyDict(@RequestParam("dictCodes") String dictCodes, @RequestParam("keys") String keys){ + return this.sysBaseApi.translateManyDict(dictCodes, keys); + } + + + /** + * 获取表数据字典 【接口签名验证】 + * @param tableFilterSql 表名可以带where条件 + * @param text + * @param code + * @return + */ + @GetMapping("/queryTableDictItemsByCode") + List queryTableDictItemsByCode(@RequestParam("tableFilterSql") String tableFilterSql, @RequestParam("text") String text, @RequestParam("code") String code){ + return sysBaseApi.queryTableDictItemsByCode(tableFilterSql, text, code); + } + + /** + * 查询表字典 支持过滤数据 【接口签名验证】 + * @param table + * @param text + * @param code + * @param filterSql + * @return + */ + @GetMapping("/queryFilterTableDictInfo") + List queryFilterTableDictInfo(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("filterSql") String filterSql){ + return sysBaseApi.queryFilterTableDictInfo(table, text, code, filterSql); + } + + /** + * 【接口签名验证】 + * 查询指定table的 text code 获取字典,包含text和value + * @param table + * @param text + * @param code + * @param keyArray + * @return + */ + @Deprecated + @GetMapping("/queryTableDictByKeys") + public List queryTableDictByKeys(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("keyArray") String[] keyArray){ + return sysBaseApi.queryTableDictByKeys(table, text, code, keyArray); + } + + + /** + * 字典表的 翻译【接口签名验证】 + * @param table + * @param text + * @param code + * @param key + * @return + */ + @GetMapping("/translateDictFromTable") + public String translateDictFromTable(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("key") String key){ + return sysBaseApi.translateDictFromTable(table, text, code, key); + } + + + /** + * 【接口签名验证】 + * 49 字典表的 翻译,可批量 + * + * @param table + * @param text + * @param code + * @param keys 多个用逗号分割 + * @param ds 数据源 + * @return + */ + @GetMapping("/translateDictFromTableByKeys") + public List translateDictFromTableByKeys(@RequestParam("table") String table, @RequestParam("text") String text, @RequestParam("code") String code, @RequestParam("keys") String keys, @RequestParam("ds") String ds) { + return this.sysBaseApi.translateDictFromTableByKeys(table, text, code, keys, ds); + } + + /** + * 发送模板信息 + * @param message + */ + @PostMapping("/sendTemplateMessage") + public void sendTemplateMessage(@RequestBody MessageDTO message){ + sysBaseApi.sendTemplateMessage(message); + } + + /** + * 获取消息模板内容 + * @param code + * @return + */ + @GetMapping("/getTemplateContent") + public String getTemplateContent(@RequestParam("code") String code){ + return this.sysBaseApi.getTemplateContent(code); + } + + /** + * 保存数据日志 + * @param dataLogDto + */ + @PostMapping("/saveDataLog") + public void saveDataLog(@RequestBody DataLogDTO dataLogDto){ + this.sysBaseApi.saveDataLog(dataLogDto); + } + + /** + * 更新头像 + * @param loginUser + * @return + */ + @PutMapping("/updateAvatar") + public void updateAvatar(@RequestBody LoginUser loginUser){ + this.sysBaseApi.updateAvatar(loginUser); + } + + /** + * 向app端 websocket推送聊天刷新消息 + * @param userId + * @return + */ + @GetMapping("/sendAppChatSocket") + public void sendAppChatSocket(@RequestParam(name="userId") String userId){ + this.sysBaseApi.sendAppChatSocket(userId); + } + + /** + * 根据roleCode查询角色信息,可逗号分隔多个 + * + * @param roleCodes + * @return + */ + @GetMapping("/queryRoleDictByCode") + public List queryRoleDictByCode(@RequestParam(name = "roleCodes") String roleCodes) { + return this.sysBaseApi.queryRoleDictByCode(roleCodes); + } + + /** + * 获取消息模板内容 + * @param id + * @return + */ + @GetMapping("/getRoleCode") + public String getRoleCode(@RequestParam("id") String id){ + return this.sysBaseApi.getRoleCodeById(id); + } + + /** + * VUEN-2584【issue】平台sql注入漏洞几个问题 + * 部分特殊函数 可以将查询结果混夹在错误信息中,导致数据库的信息暴露 + * @param e + * @return + */ + @ExceptionHandler(java.sql.SQLException.class) + public Result handleSQLException(Exception e){ + String msg = e.getMessage(); + String extractvalue = "extractvalue"; + String updatexml = "updatexml"; + if(msg!=null && (msg.toLowerCase().indexOf(extractvalue)>=0 || msg.toLowerCase().indexOf(updatexml)>=0)){ + return Result.error("校验失败,sql解析异常!"); + } + return Result.error("校验失败,sql解析异常!" + msg); + } + + /** + * 根据高级查询条件查询用户 + * @param superQuery + * @param matchType + * @return + */ + @GetMapping("/queryUserBySuperQuery") + public List queryUserBySuperQuery(@RequestParam("superQuery") String superQuery, @RequestParam("matchType") String matchType) { + return sysBaseApi.queryUserBySuperQuery(superQuery,matchType); + } + + /** + * 根据id条件查询用户 + * @param id + * @return + */ + @GetMapping("/queryUserById") + public JSONObject queryUserById(@RequestParam("id") String id) { + return sysBaseApi.queryUserById(id); + } + + /** + * 根据高级查询条件查询部门 + * @param superQuery + * @param matchType + * @return + */ + @GetMapping("/queryDeptBySuperQuery") + public List queryDeptBySuperQuery(@RequestParam("superQuery") String superQuery, @RequestParam("matchType") String matchType) { + return sysBaseApi.queryDeptBySuperQuery(superQuery,matchType); + } + + /** + * 根据高级查询条件查询角色 + * @param superQuery + * @param matchType + * @return + */ + @GetMapping("/queryRoleBySuperQuery") + public List queryRoleBySuperQuery(@RequestParam("superQuery") String superQuery, @RequestParam("matchType") String matchType) { + return sysBaseApi.queryRoleBySuperQuery(superQuery,matchType); + } + + + /** + * 根据租户ID查询用户ID + * @param tenantId 租户ID + * @return List + */ + @GetMapping("/selectUserIdByTenantId") + public List selectUserIdByTenantId(@RequestParam("tenantId") String tenantId) { + return sysBaseApi.selectUserIdByTenantId(tenantId); + } + + + /** + * 根据部门ID查询用户ID + * @param deptIds + * @return + */ + @GetMapping("/queryUserIdsByDeptIds") + public List queryUserIdsByDeptIds(@RequestParam("deptIds") List deptIds){ + return sysBaseApi.queryUserIdsByDeptIds(deptIds); + } + + /** + * 根据部门岗位ID查询用户ID + * @param deptPostIds + * @return + */ + @GetMapping("/queryUserIdsByDeptPostIds") + public List queryUserIdsByDeptPostIds(@RequestParam("deptPostIds") List deptPostIds){ + return sysBaseApi.queryUserIdsByDeptPostIds(deptPostIds); + } + + /** + * 根据部门ID查询用户ID + * @param deptIds + * @return + */ + @GetMapping("/queryUserAccountsByDeptIds") + public List queryUserAccountsByDeptIds(@RequestParam("deptIds") List deptIds){ + return sysBaseApi.queryUserAccountsByDeptIds(deptIds); + } + + /** + * 根据角色编码 查询用户ID + * @param roleCodes + * @return + */ + @GetMapping("/queryUserIdsByRoleds") + public List queryUserIdsByRoleds(@RequestParam("roleCodes") List roleCodes){ + return sysBaseApi.queryUserIdsByRoleds(roleCodes); + } + + /** + * 根据用户ID查询用户名 + * @param userIds + * @return + */ + @GetMapping("/queryUsernameByIds") + public List queryUsernameByIds(@RequestParam("userIds") List userIds){ + return sysBaseApi.queryUsernameByIds(userIds); + } + + /** + * 根据岗位的职级ID查询用户ID + * @param departPositIds + * @return + */ + @GetMapping("/queryUsernameByDepartPositIds") + public List queryUsernameByDepartPositIds(@RequestParam("departPositIds") List departPositIds){ + return sysBaseApi.queryUsernameByDepartPositIds(departPositIds); + } + + /** + * 根据职务ID查询用户ID + * @param positionIds + * @return + */ + @GetMapping("/queryUserIdsByPositionIds") + public List queryUserIdsByPositionIds(@RequestParam("positionIds") List positionIds){ + return sysBaseApi.queryUserIdsByPositionIds(positionIds); + } + + + /** + * 根据部门和子部门下的所有用户账号 + * + * @param orgCode 部门编码 + * @return + */ + @GetMapping("/getUserAccountsByDepCode") + public List getUserAccountsByDepCode(@RequestParam("orgCode") String orgCode){ + return sysBaseApi.getUserAccountsByDepCode(orgCode); + } + + /** + * 检查查询sql的表和字段是否在白名单中 + * + * @param selectSql + * @return + */ + @GetMapping("/dictTableWhiteListCheckBySql") + public boolean dictTableWhiteListCheckBySql(@RequestParam("selectSql") String selectSql) { + return sysBaseApi.dictTableWhiteListCheckBySql(selectSql); + } + + /** + * 根据字典表或者字典编码,校验是否在白名单中 + * + * @param tableOrDictCode 表名或dictCode + * @param fields 如果传的是dictCode,则该参数必须传null + * @return + */ + @GetMapping("/dictTableWhiteListCheckByDict") + public boolean dictTableWhiteListCheckByDict( + @RequestParam("tableOrDictCode") String tableOrDictCode, + @RequestParam(value = "fields", required = false) String... fields + ) { + return sysBaseApi.dictTableWhiteListCheckByDict(tableOrDictCode, fields); + } + /** + * 自动发布通告 + * + * @param dataId 通告ID + * @param currentUserName 发送人 + * @return + */ + @GetMapping("/announcementAutoRelease") + public void announcementAutoRelease( + @RequestParam("dataId") String dataId, + @RequestParam(value = "currentUserName", required = false) String currentUserName + ) { + sysBaseApi.announcementAutoRelease(dataId, currentUserName); + } + + /** + * 根据部门编码查询公司信息 + * @param orgCode 部门编码 + * @return + * @author chenrui + * @date 2025/8/12 14:45 + */ + @GetMapping(value = "/queryCompByOrgCode") + SysDepartModel queryCompByOrgCode(@RequestParam(name = "sysCode") String orgCode) { + return sysBaseApi.queryCompByOrgCode(orgCode); + } + + /** + * 根据部门编码和层次查询上级公司 + * + * @param orgCode 部门编码 + * @param level 可以传空 默认为1级 最小值为1 + * @return + */ + @GetMapping(value = "/queryCompByOrgCodeAndLevel") + SysDepartModel queryCompByOrgCodeAndLevel(@RequestParam("orgCode") String orgCode, @RequestParam("level") Integer level){ + return sysBaseApi.queryCompByOrgCodeAndLevel(orgCode,level); + } + + /** + * 根据部门code或部门id获取部门名称(当前和上级部门) + * + * @param orgCode 部门编码 + * @param depId 部门id + * @return String 部门名称 + */ + @GetMapping(value = "/getDepartPathNameByOrgCode") + String getDepartPathNameByOrgCode(@RequestParam(name = "orgCode", required = false) String orgCode, @RequestParam(name = "depId", required = false) String depId) { + return sysBaseApi.getDepartPathNameByOrgCode(orgCode, depId); + } + + /** + * 根据部门ID查询用户ID + * @param deptIds + * @return + */ + @GetMapping("/queryUserIdsByCascadeDeptIds") + public List queryUserIdsByCascadeDeptIds(@RequestParam("deptIds") List deptIds){ + return sysBaseApi.queryUserIdsByCascadeDeptIds(deptIds); + } + /** + * 推送uniapp 消息 + * @param pushMessageDTO + * @return + */ + @PostMapping("/uniPushMsgToUser") + public void uniPushMsgToUser(@RequestBody PushMessageDTO pushMessageDTO){ + sysBaseApi.uniPushMsgToUser(pushMessageDTO); + } + + /** + * 根据用户名查询用户主部门信息。 + *

+ * 逻辑:取用户的主岗位(mainDepPostId),再查询该岗位节点在 sys_depart 中的父节点, + * 父节点即为用户的主部门,返回其信息。 + *

+ * + * @param username 用户账号 + * @return 主部门信息,若用户未配置主岗位则返回 {@code null} + */ + @GetMapping("/queryMainDepartByUsername") + SysDepartModel queryMainDepartByUsername(@RequestParam("username") String username) { + return sysBaseApi.queryMainDepartByUsername(username); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/controller/CasClientController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/controller/CasClientController.java new file mode 100644 index 0000000..7d28822 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/controller/CasClientController.java @@ -0,0 +1,111 @@ +package com.ghb.base.modules.cas.controller; + +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.util.JwtUtil; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.modules.cas.util.CasServiceUtil; +import com.ghb.base.modules.cas.util.XmlUtils; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.service.ISysDepartService; +import com.ghb.base.modules.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.alibaba.fastjson.JSONObject; + +import lombok.extern.slf4j.Slf4j; + +/** + *

+ * CAS单点登录客户端登录认证 + *

+ * + * @Author zhoujf + * @since 2018-12-20 + */ +@Slf4j +@RestController +@RequestMapping("/sys/cas/client") +public class CasClientController { + + @Autowired + private ISysUserService sysUserService; + @Autowired + private ISysDepartService sysDepartService; + @Autowired + private RedisUtil redisUtil; + + @Value("${cas.prefixUrl}") + private String prefixUrl; + + + @GetMapping("/validateLogin") + public Object validateLogin(@RequestParam(name="ticket") String ticket, + @RequestParam(name="service") String service, + HttpServletRequest request, + HttpServletResponse response) throws Exception { + Result result = new Result(); + log.info("Rest api login."); + try { + String validateUrl = prefixUrl+"/p3/serviceValidate"; + String res = CasServiceUtil.getStValidate(validateUrl, ticket, service); + log.info("res."+res); + final String error = XmlUtils.getTextForElement(res, "authenticationFailure"); + if(StringUtils.isNotEmpty(error)) { + throw new Exception(error); + } + final String principal = XmlUtils.getTextForElement(res, "user"); + if (StringUtils.isEmpty(principal)) { + throw new Exception("No principal was found in the response from the CAS server."); + } + log.info("-------token----username---"+principal); + //1. 校验用户是否有效 + SysUser sysUser = sysUserService.getUserByName(principal); + result = sysUserService.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + return result; + } + String token = JwtUtil.sign(sysUser.getUsername(), sysUser.getPassword(), CommonConstant.CLIENT_TYPE_PC); + // 设置超时时间 + redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); + redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME*2 / 1000); + + //获取用户部门信息 + JSONObject obj = new JSONObject(); + List departs = sysDepartService.queryUserDeparts(sysUser.getId()); + obj.put("departs", departs); + if (departs == null || departs.size() == 0) { + obj.put("multi_depart", 0); + } else if (departs.size() == 1) { + sysUserService.updateUserDepart(principal, departs.get(0).getOrgCode(),null); + obj.put("multi_depart", 1); + } else { + obj.put("multi_depart", 2); + } + obj.put("token", token); + obj.put("userInfo", sysUser); + result.setResult(obj); + result.success("登录成功"); + + } catch (Exception e) { + //e.printStackTrace(); + result.error500(e.getMessage()); + } + return new HttpEntity<>(result); + } + + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/util/CasServiceUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/util/CasServiceUtil.java new file mode 100644 index 0000000..b727e61 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/util/CasServiceUtil.java @@ -0,0 +1,107 @@ +package com.ghb.base.modules.cas.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +/** + * @Description: CasServiceUtil + * @author: Ghb-boot + */ +public class CasServiceUtil { + + public static void main(String[] args) { + String serviceUrl = "https://cas.8f8.com.cn:8443/cas/p3/serviceValidate"; + String service = "http://localhost:3003/user/login"; + String ticket = "ST-5-1g-9cNES6KXNRwq-GuRET103sm0-DESKTOP-VKLS8B3"; + String res = getStValidate(serviceUrl,ticket, service); + + System.out.println("---------res-----"+res); + } + + + /** + * 验证ST + */ + public static String getStValidate(String url, String st, String service){ + try { + url = url+"?service="+service+"&ticket="+st; + CloseableHttpClient httpclient = createHttpClientWithNoSsl(); + HttpGet httpget = new HttpGet(url); + HttpResponse response = httpclient.execute(httpget); + String res = readResponse(response); + return res == null ? null : (res == "" ? null : res); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + + /** + * 读取 response body 内容为字符串 + * + * @param response + * @return + * @throws IOException + */ + private static String readResponse(HttpResponse response) throws IOException { + BufferedReader in = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + String result = new String(); + String line; + while ((line = in.readLine()) != null) { + result += line; + } + return result; + } + + + /** + * 创建模拟客户端(针对 https 客户端禁用 SSL 验证) + * + * @param cookieStore 缓存的 Cookies 信息 + * @return + * @throws Exception + */ + private static CloseableHttpClient createHttpClientWithNoSsl() throws Exception { + // Create a trust manager that does not validate certificate chains + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // don't check + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // don't check + } + } + }; + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, trustAllCerts, null); + LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(ctx); + return HttpClients.custom() + .setSSLSocketFactory(sslSocketFactory) + .build(); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/util/XmlUtils.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/util/XmlUtils.java new file mode 100644 index 0000000..1a8d8d1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/cas/util/XmlUtils.java @@ -0,0 +1,316 @@ +package com.ghb.base.modules.cas.util; + + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import com.ghb.base.common.constant.CommonConstant; +import org.w3c.dom.Document; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import lombok.extern.slf4j.Slf4j; + +/** + * 解析cas,ST验证后的xml + * @author: Ghb-boot + */ +@Slf4j +public final class XmlUtils { + + /** + * attributes + */ + private static final String ATTRIBUTES = "attributes"; + + /** + * Creates a new namespace-aware DOM document object by parsing the given XML. + * + * @param xml XML content. + * + * @return DOM document. + */ + public static Document newDocument(final String xml) { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final Map features = new HashMap(5); + features.put(XMLConstants.FEATURE_SECURE_PROCESSING, true); + features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + for (final Map.Entry entry : features.entrySet()) { + try { + factory.setFeature(entry.getKey(), entry.getValue()); + } catch (ParserConfigurationException e) { + log.warn("Failed setting XML feature {}: {}", entry.getKey(), e); + } + } + factory.setNamespaceAware(true); + try { + return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); + } catch (Exception e) { + throw new RuntimeException("XML parsing error: " + e); + } + } + + /** + * Get an instance of an XML reader from the XMLReaderFactory. + * + * @return the XMLReader. + */ + public static XMLReader getXmlReader() { + try { + //update-begin---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞--- + final SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + spf.setFeature("http://xml.org/sax/features/external-general-entities", false); + spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + final XMLReader reader = spf.newSAXParser().getXMLReader(); + //update-end---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞--- + reader.setFeature("http://xml.org/sax/features/namespaces", true); + reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + return reader; + } catch (final Exception e) { + throw new RuntimeException("Unable to create XMLReader", e); + } + } + + + /** + * Retrieve the text for a group of elements. Each text element is an entry + * in a list. + *

This method is currently optimized for the use case of two elements in a list. + * + * @param xmlAsString the xml response + * @param element the element to look for + * @return the list of text from the elements. + */ + public static List getTextForElements(final String xmlAsString, final String element) { + final List elements = new ArrayList(2); + final XMLReader reader = getXmlReader(); + + final DefaultHandler handler = new DefaultHandler() { + + private boolean foundElement = false; + + private StringBuilder buffer = new StringBuilder(); + + @Override + public void startElement(final String uri, final String localName, final String qName, + final Attributes attributes) throws SAXException { + if (localName.equals(element)) { + this.foundElement = true; + } + } + + @Override + public void endElement(final String uri, final String localName, final String qName) throws SAXException { + if (localName.equals(element)) { + this.foundElement = false; + elements.add(this.buffer.toString()); + this.buffer = new StringBuilder(); + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (this.foundElement) { + this.buffer.append(ch, start, length); + } + } + }; + + reader.setContentHandler(handler); + reader.setErrorHandler(handler); + + try { + reader.parse(new InputSource(new StringReader(xmlAsString))); + } catch (final Exception e) { + log.error(e.getMessage(), e); + return null; + } + + return elements; + } + + /** + * Retrieve the text for a specific element (when we know there is only + * one). + * + * @param xmlAsString the xml response + * @param element the element to look for + * @return the text value of the element. + */ + public static String getTextForElement(final String xmlAsString, final String element) { + final XMLReader reader = getXmlReader(); + final StringBuilder builder = new StringBuilder(); + + final DefaultHandler handler = new DefaultHandler() { + + private boolean foundElement = false; + + @Override + public void startElement(final String uri, final String localName, final String qName, + final Attributes attributes) throws SAXException { + if (localName.equals(element)) { + this.foundElement = true; + } + } + + @Override + public void endElement(final String uri, final String localName, final String qName) throws SAXException { + if (localName.equals(element)) { + this.foundElement = false; + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (this.foundElement) { + builder.append(ch, start, length); + } + } + }; + + reader.setContentHandler(handler); + reader.setErrorHandler(handler); + + try { + reader.parse(new InputSource(new StringReader(xmlAsString))); + } catch (final Exception e) { + log.error(e.getMessage(), e); + return null; + } + + return builder.toString(); + } + + + public static Map extractCustomAttributes(final String xml) { + final SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setNamespaceAware(true); + spf.setValidating(false); + try { + //update-begin---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞--- + spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + spf.setFeature("http://xml.org/sax/features/external-general-entities", false); + spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + //update-end---author:wangshuai---date:2026-03-30---for:【issues/9422】XmlUtils.extractCustomAttributes可能存在疑似的外部实体依赖漏洞--- + final SAXParser saxParser = spf.newSAXParser(); + final XMLReader xmlReader = saxParser.getXMLReader(); + final CustomAttributeHandler handler = new CustomAttributeHandler(); + xmlReader.setContentHandler(handler); + xmlReader.parse(new InputSource(new StringReader(xml))); + return handler.getAttributes(); + } catch (final Exception e) { + log.error(e.getMessage(), e); + return Collections.emptyMap(); + } + } + + private static class CustomAttributeHandler extends DefaultHandler { + + private Map attributes; + + private boolean foundAttributes; + + private String currentAttribute; + + private StringBuilder value; + + @Override + public void startDocument() throws SAXException { + this.attributes = new HashMap(5); + } + + @Override + public void startElement(final String nameSpaceUri, final String localName, final String qName, + final Attributes attributes) throws SAXException { + if (ATTRIBUTES.equals(localName)) { + this.foundAttributes = true; + } else if (this.foundAttributes) { + this.value = new StringBuilder(); + this.currentAttribute = localName; + } + } + + @Override + public void characters(final char[] chars, final int start, final int length) throws SAXException { + if (this.currentAttribute != null) { + value.append(chars, start, length); + } + } + + @Override + public void endElement(final String nameSpaceUri, final String localName, final String qName) + throws SAXException { + if (ATTRIBUTES.equals(localName)) { + this.foundAttributes = false; + this.currentAttribute = null; + } else if (this.foundAttributes) { + final Object o = this.attributes.get(this.currentAttribute); + + if (o == null) { + this.attributes.put(this.currentAttribute, this.value.toString()); + } else { + final List items; + if (o instanceof List) { + items = (List) o; + } else { + items = new LinkedList(); + items.add(o); + this.attributes.put(this.currentAttribute, items); + } + items.add(this.value.toString()); + } + } + } + + public Map getAttributes() { + return this.attributes; + } + } + + + public static void main(String[] args) { + String result = "\r\n" + + " \r\n" + + " admin\r\n" + + " \r\n" + + " UsernamePasswordCredential\r\n" + + " true\r\n" + + " 2019-08-01T19:33:21.527+08:00[Asia/Shanghai]\r\n" + + " RestAuthenticationHandler\r\n" + + " RestAuthenticationHandler\r\n" + + " false\r\n" + + " \r\n" + + " \r\n" + + ""; + + String errorRes = "\r\n" + + " 未能够识别出目标 'ST-5-1g-9cNES6KXNRwq-GuRET103sm0-DESKTOP-VKLS8B3'票根\r\n" + + ""; + + String error = XmlUtils.getTextForElement(errorRes, "authenticationFailure"); + //System.out.println("------"+error); + + String error2 = XmlUtils.getTextForElement(result, "authenticationFailure"); + //System.out.println("------"+error2); + String principal = XmlUtils.getTextForElement(result, "user"); + //System.out.println("---principal---"+principal); + Map attributes = XmlUtils.extractCustomAttributes(result); + System.out.println("---attributes---"+attributes); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/SysMessageController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/SysMessageController.java new file mode 100644 index 0000000..33be6d5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/SysMessageController.java @@ -0,0 +1,145 @@ +package com.ghb.base.modules.message.controller; + +import java.util.Arrays; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.message.entity.SysMessage; +import com.ghb.base.modules.message.service.ISysMessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + +/** + * @Description: 消息 + * @author: Ghb-boot + * @date: 2019-04-09 + * @version: V1.0 + */ +@Slf4j +@RestController +@RequestMapping("/sys/message/sysMessage") +public class SysMessageController extends GhbController { + @Autowired + private ISysMessageService sysMessageService; + + /** + * 分页列表查询 + * + * @param sysMessage + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/list") + public Result queryPageList(SysMessage sysMessage, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysMessage, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysMessageService.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 添加 + * + * @param sysMessage + * @return + */ + @PostMapping(value = "/add") + public Result add(@RequestBody SysMessage sysMessage) { + sysMessageService.save(sysMessage); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param sysMessage + * @return + */ + @PutMapping(value = "/edit") + public Result edit(@RequestBody SysMessage sysMessage) { + sysMessageService.updateById(sysMessage); + return Result.ok("修改成功!"); + + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysMessageService.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + + this.sysMessageService.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysMessage sysMessage = sysMessageService.getById(id); + return Result.ok(sysMessage); + } + + /** + * 导出excel + * + * @param request + */ + @GetMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysMessage sysMessage) { + return super.exportXls(request,sysMessage,SysMessage.class, "推送消息模板"); + } + + /** + * excel导入 + * + * @param request + * @param response + * @return + */ + @PostMapping(value = "/importExcel") + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysMessage.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/SysMessageTemplateController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/SysMessageTemplateController.java new file mode 100644 index 0000000..ca4dc3f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/SysMessageTemplateController.java @@ -0,0 +1,180 @@ +package com.ghb.base.modules.message.controller; + +import java.util.Arrays; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.message.entity.MsgParams; +import com.ghb.base.modules.message.entity.SysMessageTemplate; +import com.ghb.base.modules.message.service.ISysMessageTemplateService; +import com.ghb.base.modules.message.util.PushMsgUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + +/** + * @Description: 消息模板 + * @Author: Ghb-boot + * @Sate: 2019-04-09 + * @Version: V1.0 + */ +@Slf4j +@RestController +@RequestMapping("/sys/message/sysMessageTemplate") +public class SysMessageTemplateController extends GhbController { + @Autowired + private ISysMessageTemplateService sysMessageTemplateService; + @Autowired + private PushMsgUtil pushMsgUtil; + + @Autowired + private ISysBaseAPI sysBaseApi; + + /** + * 分页列表查询 + * + * @param sysMessageTemplate + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/list") + public Result queryPageList(SysMessageTemplate sysMessageTemplate, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysMessageTemplate, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysMessageTemplateService.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 添加 + * + * @param sysMessageTemplate + * @return + */ + @PostMapping(value = "/add") + public Result add(@RequestBody SysMessageTemplate sysMessageTemplate) { + sysMessageTemplateService.save(sysMessageTemplate); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param sysMessageTemplate + * @return + */ + @PutMapping(value = "/edit") + public Result edit(@RequestBody SysMessageTemplate sysMessageTemplate) { + sysMessageTemplateService.updateById(sysMessageTemplate); + return Result.ok("更新成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysMessageTemplateService.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + this.sysMessageTemplateService.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysMessageTemplate sysMessageTemplate = sysMessageTemplateService.getById(id); + return Result.ok(sysMessageTemplate); + } + + /** + * 导出excel + * + * @param request + */ + @GetMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request,SysMessageTemplate sysMessageTemplate) { + return super.exportXls(request, sysMessageTemplate, SysMessageTemplate.class,"推送消息模板"); + } + + /** + * excel导入 + * + * @param request + * @param response + * @return + */ + @PostMapping(value = "/importExcel") + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysMessageTemplate.class); + } + + /** + * 发送消息 + */ + @PostMapping(value = "/sendMsg") + public Result sendMessage(@RequestBody MsgParams msgParams) { + Result result = new Result(); + try { + MessageDTO md = new MessageDTO(); + md.setToAll(false); + md.setTitle("消息发送测试"); + md.setTemplateCode(msgParams.getTemplateCode()); + md.setToUser(msgParams.getReceiver()); + md.setType(msgParams.getMsgType()); + String testData = msgParams.getTestData(); + if(oConvertUtils.isNotEmpty(testData)){ + Map data = JSON.parseObject(testData, Map.class); + md.setData(data); + } + sysBaseApi.sendTemplateMessage(md); + return result.success("消息发送成功!"); + } catch (Exception e) { + log.error("发送消息出错:" + e.getMessage(), e); + return result.error500("发送消息出错!"); + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/TestSocketController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/TestSocketController.java new file mode 100644 index 0000000..4b98ca5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/controller/TestSocketController.java @@ -0,0 +1,53 @@ +package com.ghb.base.modules.message.controller; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.WebsocketConst; +import com.ghb.base.modules.message.websocket.WebSocket; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.alibaba.fastjson.JSONObject; + +/** + * @Description: TestSocketController + * @author: Ghb-boot + */ +@RestController +@RequestMapping("/sys/socketTest") +public class TestSocketController { + + @Autowired + private WebSocket webSocket; + + @PostMapping("/sendAll") + public Result sendAll(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + String message = jsonObject.getString("message"); + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_TOPIC); + obj.put(WebsocketConst.MSG_ID, "M0001"); + obj.put(WebsocketConst.MSG_TXT, message); + webSocket.sendMessage(obj.toJSONString()); + result.setResult("群发!"); + return result; + } + + @PostMapping("/sendUser") + public Result sendUser(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + String userId = jsonObject.getString("userId"); + String message = jsonObject.getString("message"); + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER); + obj.put(WebsocketConst.MSG_USER_ID, userId); + obj.put(WebsocketConst.MSG_ID, "M0001"); + obj.put(WebsocketConst.MSG_TXT, message); + webSocket.sendMessage(userId, obj.toJSONString()); + result.setResult("单发"); + return result; + } + +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/MsgParams.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/MsgParams.java new file mode 100644 index 0000000..0bee67d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/MsgParams.java @@ -0,0 +1,35 @@ +package com.ghb.base.modules.message.entity; + +import java.io.Serializable; + +import lombok.Data; + +/** + * 发送消息实体 + * @author: Ghb-boot + */ +@Data +public class MsgParams implements Serializable { + + private static final long serialVersionUID = 1L; + /** + * 消息类型 + */ + private String msgType; + + /** + * 消息接收方 + */ + private String receiver; + + /** + * 消息模板码 + */ + private String templateCode; + + /** + * 测试数据 + */ + private String testData; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/SysMessage.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/SysMessage.java new file mode 100644 index 0000000..1eb1caa --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/SysMessage.java @@ -0,0 +1,62 @@ +package com.ghb.base.modules.message.entity; + +import com.ghb.base.common.aspect.annotation.Dict; +import com.ghb.base.common.system.base.entity.GhbEntity; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @Description: 消息 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_sms") +public class SysMessage extends GhbEntity { + /**推送内容*/ + @Excel(name = "推送内容", width = 15) + private java.lang.String esContent; + /**推送所需参数Json格式*/ + @Excel(name = "推送所需参数Json格式", width = 15) + private java.lang.String esParam; + /**接收人*/ + @Excel(name = "接收人", width = 15) + private java.lang.String esReceiver; + /**推送失败原因*/ + @Excel(name = "推送失败原因", width = 15) + private java.lang.String esResult; + /**发送次数*/ + @Excel(name = "发送次数", width = 15) + private java.lang.Integer esSendNum; + /**推送状态 0未推送 1推送成功 2推送失败*/ + @Excel(name = "推送状态 0未推送 1推送成功 2推送失败", width = 15) + @Dict(dicCode = "msgSendStatus") + private java.lang.String esSendStatus; + /**推送时间*/ + @Excel(name = "推送时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date esSendTime; + /**消息标题*/ + @Excel(name = "消息标题", width = 15) + private java.lang.String esTitle; + /** + * 推送方式:参考枚举类MessageTypeEnum + */ + @Excel(name = "推送方式", width = 15) + @Dict(dicCode = "messageType") + private java.lang.String esType; + /**备注*/ + @Excel(name = "备注", width = 15) + private java.lang.String remark; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/SysMessageTemplate.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/SysMessageTemplate.java new file mode 100644 index 0000000..e8a2f4b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/entity/SysMessageTemplate.java @@ -0,0 +1,46 @@ +package com.ghb.base.modules.message.entity; + +import com.ghb.base.common.system.base.entity.GhbEntity; +import org.jeecgframework.poi.excel.annotation.Excel; + +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @Description: 消息模板 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("sys_sms_template") +public class SysMessageTemplate extends GhbEntity{ + /**模板CODE*/ + @Excel(name = "模板CODE", width = 15) + private java.lang.String templateCode; + /**模板标题*/ + @Excel(name = "模板标题", width = 30) + private java.lang.String templateName; + /**模板内容*/ + @Excel(name = "模板内容", width = 50) + private java.lang.String templateContent; + /**模板测试json*/ + @Excel(name = "模板测试json", width = 15) + private java.lang.String templateTestJson; + /**模板类型*/ + @Excel(name = "模板类型", width = 15) + private java.lang.String templateType; + /**模板分类*/ + @Excel(name = "模板类型(notice通知公告 other其他)", width = 15) + private java.lang.String templateCategory; + + /**已经应用/未应用 1是0否*/ + @Excel(name = "应用状态", width = 15) + private String useStatus; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/enums/RangeDateEnum.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/enums/RangeDateEnum.java new file mode 100644 index 0000000..e98435d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/enums/RangeDateEnum.java @@ -0,0 +1,135 @@ +package com.ghb.base.modules.message.enums; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.constant.enums.MessageTypeEnum; +import com.ghb.base.common.system.annotation.EnumDict; +import com.ghb.base.common.system.vo.DictModel; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * 用于消息数据查询【vue3】 + * 新版系统通知查询条件 + * @Author taoYan + * @Date 2022/8/19 20:41 + **/ +@Slf4j +@EnumDict("rangeDate") +public enum RangeDateEnum { + + JT("jt", "今天"), + ZT("zt", "昨天"), + QT("qt", "前天"), + BZ("bz","本周"), + SZ("sz", "上周"), + BY("by", "本月"), + SY("sy", "上月"), + SEVENDAYS("7day", "7日"), + ZDY("zdy", "自定义日期"); + + String key; + + String title; + + RangeDateEnum(String key, String title){ + this.key = key; + this.title = title; + } + + /** + * 获取字典数据 + * @return + */ + public static List getDictList(){ + List list = new ArrayList<>(); + DictModel dictModel = null; + for(RangeDateEnum e: RangeDateEnum.values()){ + dictModel = new DictModel(); + dictModel.setValue(e.key); + dictModel.setText(e.title); + list.add(dictModel); + } + return list; + } + + /** + * 根据key 获取范围时间值 + * @param key + * @return + */ + public static Date[] getRangeArray(String key){ + Calendar calendar1 = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + Date[] array = new Date[2]; + boolean flag = false; + if(JT.key.equals(key)){ + //今天 + } else if(ZT.key.equals(key)){ + //昨天 + calendar1.add(Calendar.DAY_OF_YEAR, -1); + calendar2.add(Calendar.DAY_OF_YEAR, -1); + } else if(QT.key.equals(key)){ + //前天 + calendar1.add(Calendar.DAY_OF_YEAR, -2); + calendar2.add(Calendar.DAY_OF_YEAR, -2); + } else if(BZ.key.equals(key)){ + //本周 + calendar1.set(Calendar.DAY_OF_WEEK, 2); + + calendar2.add(Calendar.WEEK_OF_MONTH,1); + calendar2.add(Calendar.DAY_OF_WEEK,-1); + } else if(SZ.key.equals(key)){ + //本周一减一周 + calendar1.set(Calendar.DAY_OF_WEEK, 2); + calendar1.add(Calendar.WEEK_OF_MONTH, -1); + + // 本周一减一天 + calendar2.set(Calendar.DAY_OF_WEEK, 2); + calendar2.add(Calendar.DAY_OF_WEEK,-1); + } else if(BY.key.equals(key)){ + //本月 + calendar1.set(Calendar.DAY_OF_MONTH, 1); + + calendar2.set(Calendar.DAY_OF_MONTH, 1); + calendar2.add(Calendar.MONTH, 1); + calendar2.add(Calendar.DAY_OF_MONTH, -1); + } else if(SY.key.equals(key)){ + //本月第一天减一月 + calendar1.set(Calendar.DAY_OF_MONTH, 1); + calendar1.add(Calendar.MONTH, -1); + + //本月第一天减一天 + calendar2.set(Calendar.DAY_OF_MONTH, 1); + calendar2.add(Calendar.DAY_OF_MONTH, -1); + } else if (SEVENDAYS.key.equals(key)){ + //七日第一天 + calendar1.setTime(new Date()); + calendar1.add(Calendar.DATE, -7); + }else{ + flag = true; + } + if(flag){ + return null; + } + // 开始时间00:00:00 结束时间23:59:59 + calendar1.set(Calendar.HOUR, 0); + calendar1.set(Calendar.MINUTE, 0); + calendar1.set(Calendar.SECOND, 0); + calendar1.set(Calendar.MILLISECOND, 0); + calendar2.set(Calendar.HOUR, 23); + calendar2.set(Calendar.MINUTE, 59); + calendar2.set(Calendar.SECOND, 59); + calendar2.set(Calendar.MILLISECOND, 999); + array[0] = calendar1.getTime(); + array[1] = calendar2.getTime(); + return array; + } + + public String getKey(){ + return this.key; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/ISendMsgHandle.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/ISendMsgHandle.java new file mode 100644 index 0000000..6815cb7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/ISendMsgHandle.java @@ -0,0 +1,26 @@ +package com.ghb.base.modules.message.handle; + +import com.ghb.base.common.api.dto.message.MessageDTO; + +/** + * @Description: 发送信息接口 + * @author: Ghb-boot + */ +public interface ISendMsgHandle { + + /** + * 发送信息 + * @param esReceiver 接受人 + * @param esTitle 标题 + * @param esContent 内容 + */ + void sendMsg(String esReceiver, String esTitle, String esContent); + + /** + * 发送信息 + * @param messageDTO + */ + default void sendMessage(MessageDTO messageDTO){ + + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/enums/SendMsgStatusEnum.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/enums/SendMsgStatusEnum.java new file mode 100644 index 0000000..d59dd6c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/enums/SendMsgStatusEnum.java @@ -0,0 +1,26 @@ +package com.ghb.base.modules.message.handle.enums; + +/** + * 推送状态枚举 + * @author: Ghb-boot + */ +public enum SendMsgStatusEnum { + +//推送状态 0未推送 1推送成功 2推送失败 + WAIT("0"), SUCCESS("1"), FAIL("2"); + + private String code; + + private SendMsgStatusEnum(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + public void setStatusCode(String code) { + this.code = code; + } + +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/enums/SendMsgTypeEnum.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/enums/SendMsgTypeEnum.java new file mode 100644 index 0000000..aca57a3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/enums/SendMsgTypeEnum.java @@ -0,0 +1,64 @@ +package com.ghb.base.modules.message.handle.enums; + +import com.ghb.base.common.util.oConvertUtils; + +/** + * 发送消息类型枚举 + * @author: Ghb-boot + */ +public enum SendMsgTypeEnum { + + /** + * 短信 + */ + SMS("1", "com.ghb.base.modules.message.handle.impl.SmsSendMsgHandle"), + /** + * 邮件 + */ + EMAIL("2", "com.ghb.base.modules.message.handle.impl.EmailSendMsgHandle"), + /** + * 微信 + */ + WX("3","com.ghb.base.modules.message.handle.impl.WxSendMsgHandle"), + /** + * 系统消息 + */ + SYSTEM_MESSAGE("4","com.ghb.base.modules.message.handle.impl.SystemSendMsgHandle"); + + private String type; + + private String implClass; + + private SendMsgTypeEnum(String type, String implClass) { + this.type = type; + this.implClass = implClass; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getImplClass() { + return implClass; + } + + public void setImplClass(String implClass) { + this.implClass = implClass; + } + + public static SendMsgTypeEnum getByType(String type) { + if (oConvertUtils.isEmpty(type)) { + return null; + } + for (SendMsgTypeEnum val : values()) { + if (val.getType().equals(type)) { + return val; + } + } + return null; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/DdSendMsgHandle.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/DdSendMsgHandle.java new file mode 100644 index 0000000..b9ff5ea --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/DdSendMsgHandle.java @@ -0,0 +1,37 @@ +package com.ghb.base.modules.message.handle.impl; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.modules.message.handle.ISendMsgHandle; +import com.ghb.base.modules.system.service.impl.ThirdAppDingtalkServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @Description: 发钉钉消息模板 + * @author: Ghb-boot + */ +@Slf4j +@Component("ddSendMsgHandle") +public class DdSendMsgHandle implements ISendMsgHandle { + + @Autowired + private ThirdAppDingtalkServiceImpl dingtalkService; + + @Override + public void sendMsg(String esReceiver, String esTitle, String esContent) { + log.info("发微信消息模板"); + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setToUser(esReceiver); + messageDTO.setTitle(esTitle); + messageDTO.setContent(esContent); + messageDTO.setToAll(false); + sendMessage(messageDTO); + } + + @Override + public void sendMessage(MessageDTO messageDTO) { + dingtalkService.sendMessage(messageDTO, true); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/EmailSendMsgHandle.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/EmailSendMsgHandle.java new file mode 100644 index 0000000..172ba03 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/EmailSendMsgHandle.java @@ -0,0 +1,277 @@ +package com.ghb.base.modules.message.handle.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.enums.MessageTypeEnum; +import com.ghb.base.common.system.util.JwtUtil; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.StaticConfig; +import com.ghb.base.modules.message.entity.SysMessage; +import com.ghb.base.modules.message.handle.ISendMsgHandle; +import com.ghb.base.modules.message.mapper.SysMessageMapper; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @Description: 邮箱发送信息 + * @author: Ghb-boot + */ +@Slf4j +@Component("emailSendMsgHandle") +public class EmailSendMsgHandle implements ISendMsgHandle { + static String emailFrom; + + public static void setEmailFrom(String emailFrom) { + EmailSendMsgHandle.emailFrom = emailFrom; + } + + @Autowired + SysUserMapper sysUserMapper; + + @Autowired + private RedisUtil redisUtil; + + @Autowired + private SysMessageMapper sysMessageMapper; + + /** + * 真实姓名变量 + */ + private static final String realNameExp = "{REALNAME}"; + /** + * 线程池用于异步发送消息 + */ + public static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, 1024, 60L, TimeUnit.SECONDS, new SynchronousQueue<>()); + + + @Override + public void sendMsg(String esReceiver, String esTitle, String esContent) { + JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender"); + MimeMessage message = mailSender.createMimeMessage(); + // 代码逻辑说明: 配置类数据获取 + if(oConvertUtils.isEmpty(emailFrom)){ + StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class); + setEmailFrom(staticConfig.getEmailFrom()); + } + cachedThreadPool.execute(()->{ + try { + log.info("============> 开始邮件发送,接收人:"+esReceiver); + MimeMessageHelper helper = new MimeMessageHelper(message, true); + // 设置发送方邮箱地址 + helper.setFrom(emailFrom); + helper.setTo(esReceiver); + helper.setSubject(esTitle); + helper.setText(esContent, true); + mailSender.send(message); + log.info("============> 邮件发送成功,接收人:"+esReceiver); + } catch (MessagingException e) { + log.error("============> 邮件发送失败,接收人:"+esReceiver, e.getMessage()); + } + }); + } + + @Override + public void sendMessage(MessageDTO messageDTO) { + String content = messageDTO.getContent(); + String title = messageDTO.getTitle(); + // 代码逻辑说明: 【QQYUN-8523】敲敲云发邮件通知,不稳定--- + boolean timeJobSendEmail = this.isTimeJobSendEmail(messageDTO.getToUser(), title, content); + if(timeJobSendEmail){ + return; + } + this.sendEmailMessage(messageDTO); + } + + /** + * 直接发送邮件 + * + * @param messageDTO + */ + public void sendEmailMessage(MessageDTO messageDTO) { + String[] arr = messageDTO.getToUser().split(","); + LambdaQueryWrapper query = new LambdaQueryWrapper().in(SysUser::getUsername, arr); + List list = sysUserMapper.selectList(query); + String content = messageDTO.getContent(); + String title = messageDTO.getTitle(); + for(SysUser user: list){ + String email = user.getEmail(); + if (ObjectUtils.isEmpty(email)) { + continue; + } + content=replaceContent(user,content); + log.info("邮件内容:"+ content); + sendMsg(email, title, content); + } + + // 代码逻辑说明: QQYUN-5557【简流】通知节点 发送邮箱 表单上有一个邮箱字段,流程中,邮件发送节点,邮件接收人 不可选择邮箱 + Set toEmailList = messageDTO.getToEmailList(); + if(toEmailList!=null && toEmailList.size()>0){ + for(String email: toEmailList){ + if (ObjectUtils.isEmpty(email)) { + continue; + } + log.info("邮件内容:"+ content); + sendMsg(email, title, content); + } + } + + //发送给抄送人 + sendMessageToCopyUser(messageDTO); + } + + /** + * 发送邮件给抄送人 + * @param messageDTO + */ + public void sendMessageToCopyUser(MessageDTO messageDTO) { + String copyToUser = messageDTO.getCopyToUser(); + if(ObjectUtils.isNotEmpty(copyToUser)) { + LambdaQueryWrapper query = new LambdaQueryWrapper().in(SysUser::getUsername, copyToUser.split(",")); + List list = sysUserMapper.selectList(query); + String content = messageDTO.getContent(); + String title = messageDTO.getTitle(); + + for (SysUser user : list) { + String email = user.getEmail(); + if (ObjectUtils.isEmpty(email)) { + continue; + } + content=replaceContent(user,content); + log.info("邮件内容:" + content); + + // 代码逻辑说明: QQYUN-5557【简流】通知节点 发送邮箱 表单上有一个邮箱字段,流程中,邮件发送节点,邮件接收人 不可选择邮箱 + sendEmail(email, content, title); + } + + Set ccEmailList = messageDTO.getCcEmailList(); + if(ccEmailList!=null && ccEmailList.size()>0){ + for(String email: ccEmailList){ + if (ObjectUtils.isEmpty(email)) { + continue; + } + log.info("邮件内容:"+ content); + sendEmail(email, content, title); + } + } + + } + } + + /** + * 发送邮件给抄送人调用 + * @param email + * @param content + * @param title + */ + private void sendEmail(String email, String content, String title){ + JavaMailSender mailSender = (JavaMailSender) SpringContextUtils.getBean("mailSender"); + MimeMessage message = mailSender.createMimeMessage(); + if (oConvertUtils.isEmpty(emailFrom)) { + StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class); + setEmailFrom(staticConfig.getEmailFrom()); + } + cachedThreadPool.execute(()->{ + try { + MimeMessageHelper helper = new MimeMessageHelper(message, true); + // 设置发送方邮箱地址 + helper.setFrom(emailFrom); + helper.setTo(email); + //设置抄送人 + helper.setCc(email); + helper.setSubject(title); + helper.setText(content, true); + mailSender.send(message); + log.info("============> 邮件发送成功,接收人:"+email); + } catch (MessagingException e) { + log.warn("============> 邮件发送失败,接收人:"+email, e.getMessage()); + } + }); + } + + + /** + * 替换邮件内容变量 + * @param user + * @param content + * @return + */ + private String replaceContent(SysUser user,String content){ + if (content.indexOf(realNameExp) > 0) { + content = content.replace("$"+realNameExp,user.getRealname()).replace(realNameExp, user.getRealname()); + } + if (content.indexOf(CommonConstant.LOGIN_TOKEN) > 0) { + String token = getToken(user); + try { + content = content.replace(CommonConstant.LOGIN_TOKEN, URLEncoder.encode(token, "UTF-8")); + } catch (UnsupportedEncodingException e) { + log.error("邮件消息token编码失败", e.getMessage()); + } + } + return content; + } + + /** + * 获取token + * @param user + * @return + */ + private String getToken(SysUser user) { + // 生成token + String token = JwtUtil.sign(user.getUsername(), user.getPassword(), CommonConstant.CLIENT_TYPE_PC); + redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); + // 设置超时时间 1个小时 + redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 1 / 1000); + return token; + } + + /** + * 是否定时发送邮箱 + * @param toUser + * @param title + * @param content + * @return + */ + private boolean isTimeJobSendEmail(String toUser, String title, String content) { + StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class); + Boolean timeJobSend = staticConfig.getTimeJobSend(); + if(null != timeJobSend && timeJobSend){ + this.addSysSmsSend(toUser,title,content); + return true; + } + return false; + } + + /** + * 保存到短信发送表 + */ + private void addSysSmsSend(String toUser, String title, String content) { + SysMessage sysMessage = new SysMessage(); + sysMessage.setEsTitle(title); + sysMessage.setEsContent(content); + sysMessage.setEsReceiver(toUser); + sysMessage.setEsSendStatus("0"); + sysMessage.setEsSendNum(0); + sysMessage.setEsType(MessageTypeEnum.YJ.getType()); + sysMessageMapper.insert(sysMessage); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/QywxSendMsgHandle.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/QywxSendMsgHandle.java new file mode 100644 index 0000000..5aaddf4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/QywxSendMsgHandle.java @@ -0,0 +1,37 @@ +package com.ghb.base.modules.message.handle.impl; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.modules.message.handle.ISendMsgHandle; +import com.ghb.base.modules.system.service.impl.ThirdAppWechatEnterpriseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @Description: 发企业微信消息模板 + * @author: Ghb-boot + */ +@Slf4j +@Component("qywxSendMsgHandle") +public class QywxSendMsgHandle implements ISendMsgHandle { + + @Autowired + private ThirdAppWechatEnterpriseServiceImpl wechatEnterpriseService; + + @Override + public void sendMsg(String esReceiver, String esTitle, String esContent) { + log.info("发微信消息模板"); + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setToUser(esReceiver); + messageDTO.setTitle(esTitle); + messageDTO.setContent(esContent); + messageDTO.setToAll(false); + sendMessage(messageDTO); + } + + @Override + public void sendMessage(MessageDTO messageDTO) { + wechatEnterpriseService.sendMessage(messageDTO, true); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/SmsSendMsgHandle.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/SmsSendMsgHandle.java new file mode 100644 index 0000000..efe3200 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/SmsSendMsgHandle.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.message.handle.impl; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.modules.message.handle.ISendMsgHandle; + +/** + * @Description: 短信发送 + * @author: Ghb-boot + */ +@Slf4j +public class SmsSendMsgHandle implements ISendMsgHandle { + + @Override + public void sendMsg(String esReceiver, String esTitle, String esContent) { + // TODO Auto-generated method stub + log.info("发短信"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/SystemSendMsgHandle.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/SystemSendMsgHandle.java new file mode 100644 index 0000000..120354c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/handle/impl/SystemSendMsgHandle.java @@ -0,0 +1,147 @@ +package com.ghb.base.modules.message.handle.impl; + +import com.alibaba.fastjson.JSONObject; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.WebsocketConst; +import com.ghb.base.common.constant.enums.NoticeTypeEnum; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.common.constant.enums.Vue3MessageHrefEnum; +import com.ghb.base.modules.message.handle.ISendMsgHandle; +import com.ghb.base.modules.message.websocket.WebSocket; +import com.ghb.base.modules.system.entity.SysAnnouncement; +import com.ghb.base.modules.system.entity.SysAnnouncementSend; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.mapper.SysAnnouncementMapper; +import com.ghb.base.modules.system.mapper.SysAnnouncementSendMapper; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.Date; +import java.util.Map; + +/** +* @Description: 发送系统消息 +* @Author: wangshuai +* @Date: 2022年3月22日 18:48:20 +*/ +@Component("systemSendMsgHandle") +public class SystemSendMsgHandle implements ISendMsgHandle { + + public static final String FROM_USER="system"; + + @Resource + private SysAnnouncementMapper sysAnnouncementMapper; + + @Resource + private SysUserMapper userMapper; + + @Resource + private SysAnnouncementSendMapper sysAnnouncementSendMapper; + + @Resource + private WebSocket webSocket; + + /** + * 该方法会发送3种消息:系统消息、企业微信 钉钉 + * @param esReceiver 发送人 + * @param esTitle 标题 + * @param esContent 内容 + */ + @Override + public void sendMsg(String esReceiver, String esTitle, String esContent) { + if(oConvertUtils.isEmpty(esReceiver)){ + throw new GhbBootException("被发送人不能为空"); + } + ISysBaseAPI sysBaseApi = SpringContextUtils.getBean(ISysBaseAPI.class); + MessageDTO messageDTO = new MessageDTO(FROM_USER,esReceiver,esTitle,esContent); + sysBaseApi.sendSysAnnouncement(messageDTO); + } + + /** + * 仅发送系统消息 + * @param messageDTO + */ + @Override + public void sendMessage(MessageDTO messageDTO) { + //原方法不支持 sysBaseApi.sendSysAnnouncement(messageDTO); 有企业微信消息逻辑, + String title = messageDTO.getTitle(); + String content = messageDTO.getContent(); + String fromUser = messageDTO.getFromUser(); + Map data = messageDTO.getData(); + String[] arr = messageDTO.getToUser().split(","); + for(String username: arr){ + // 代码逻辑说明: 【QQYUN-12162】OA项目改造,系统重消息拆分,目前消息都在一起 需按分类进行拆分--- + doSend(title, content, fromUser, username, data, messageDTO.getNoticeType()); + } + } + + private void doSend(String title, String msgContent, String fromUser, String toUser, Map data, String noticeType){ + SysAnnouncement announcement = new SysAnnouncement(); + if(data!=null){ + //摘要信息 + Object msgAbstract = data.get(CommonConstant.NOTICE_MSG_SUMMARY); + if(msgAbstract!=null){ + announcement.setMsgAbstract(msgAbstract.toString()); + } + // 任务节点ID + Object taskId = data.get(CommonConstant.NOTICE_MSG_BUS_ID); + if(taskId!=null){ + announcement.setBusId(taskId.toString()); + announcement.setBusType(Vue3MessageHrefEnum.BPM_TASK.getBusType()); + noticeType = NoticeTypeEnum.NOTICE_TYPE_FLOW.getValue(); + } + + // 流程内消息节点 发消息会传一个busType + Object busType = data.get(CommonConstant.NOTICE_MSG_BUS_TYPE); + if(busType!=null){ + announcement.setBusType(busType.toString()); + noticeType = NoticeTypeEnum.NOTICE_TYPE_FLOW.getValue(); + } + } + announcement.setTitile(title); + announcement.setMsgContent(msgContent); + announcement.setSender(fromUser); + announcement.setPriority(CommonConstant.PRIORITY_M); + announcement.setMsgType(CommonConstant.MSG_TYPE_UESR); + announcement.setSendStatus(CommonConstant.HAS_SEND); + announcement.setSendTime(new Date()); + //系统消息 + announcement.setMsgCategory("2"); + announcement.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0)); + if(oConvertUtils.isEmpty(noticeType)){ + noticeType = NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue(); + } + announcement.setNoticeType(noticeType); + announcement.setIzTop(CommonConstant.IZ_TOP_0); + sysAnnouncementMapper.insert(announcement); + // 2.插入用户通告阅读标记表记录 + String userId = toUser; + String[] userIds = userId.split(","); + String anntId = announcement.getId(); + for(int i=0;i queryWrapper = new QueryWrapper(); + queryWrapper.eq("es_send_status", SendMsgStatusEnum.WAIT.getCode()) + .or(i -> i.eq("es_send_status", SendMsgStatusEnum.FAIL.getCode()).lt("es_send_num", 6)); + List sysMessages = sysMessageService.list(queryWrapper); + System.out.println(sysMessages); + // 2.根据不同的类型走不通的发送实现类 + for (SysMessage sysMessage : sysMessages) { + // 代码逻辑说明: 模板消息发送测试调用方法修改 + Integer sendNum = sysMessage.getEsSendNum(); + try { + MessageDTO md = new MessageDTO(); + md.setTitle(sysMessage.getEsTitle()); + md.setContent(sysMessage.getEsContent()); + md.setToUser(sysMessage.getEsReceiver()); + md.setType(sysMessage.getEsType()); + md.setToAll(false); + // 代码逻辑说明: 【QQYUN-8523】敲敲云发邮件通知,不稳定--- + md.setIsTimeJob(true); + sysBaseAPI.sendTemplateMessage(md); + //发送消息成功 + sysMessage.setEsSendStatus(SendMsgStatusEnum.SUCCESS.getCode()); + } catch (Exception e) { + e.printStackTrace(); + // 发送消息出现异常 + sysMessage.setEsSendStatus(SendMsgStatusEnum.FAIL.getCode()); + } + sysMessage.setEsSendNum(++sendNum); + // 发送结果回写到数据库 + sysMessageService.updateById(sysMessage); + } + + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/SysMessageMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/SysMessageMapper.java new file mode 100644 index 0000000..21bbc55 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/SysMessageMapper.java @@ -0,0 +1,17 @@ +package com.ghb.base.modules.message.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.message.entity.SysMessage; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 消息 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +public interface SysMessageMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/SysMessageTemplateMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/SysMessageTemplateMapper.java new file mode 100644 index 0000000..68770ee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/SysMessageTemplateMapper.java @@ -0,0 +1,24 @@ +package com.ghb.base.modules.message.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.message.entity.SysMessageTemplate; + +import java.util.List; + +/** + * @Description: 消息模板 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +public interface SysMessageTemplateMapper extends BaseMapper { + + /** + * 通过模板CODE查询消息模板 + * @param code 模板CODE + * @return List + */ + @Select("SELECT * FROM SYS_SMS_TEMPLATE WHERE TEMPLATE_CODE = #{code}") + List selectByCode(String code); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/xml/SysMessageMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/xml/SysMessageMapper.xml new file mode 100644 index 0000000..df25164 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/xml/SysMessageMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/xml/SysMessageTemplateMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/xml/SysMessageTemplateMapper.xml new file mode 100644 index 0000000..18f2888 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/mapper/xml/SysMessageTemplateMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/ISysMessageService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/ISysMessageService.java new file mode 100644 index 0000000..a07c5cf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/ISysMessageService.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.message.service; + +import com.ghb.base.common.system.base.service.GhbService; +import com.ghb.base.modules.message.entity.SysMessage; + +/** + * @Description: 消息 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +public interface ISysMessageService extends GhbService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/ISysMessageTemplateService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/ISysMessageTemplateService.java new file mode 100644 index 0000000..036edb5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/ISysMessageTemplateService.java @@ -0,0 +1,22 @@ +package com.ghb.base.modules.message.service; + +import java.util.List; + +import com.ghb.base.common.system.base.service.GhbService; +import com.ghb.base.modules.message.entity.SysMessageTemplate; + +/** + * @Description: 消息模板 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +public interface ISysMessageTemplateService extends GhbService { + + /** + * 通过模板CODE查询消息模板 + * @param code 模板CODE + * @return + */ + List selectByCode(String code); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/impl/SysMessageServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/impl/SysMessageServiceImpl.java new file mode 100644 index 0000000..7ebdb72 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/impl/SysMessageServiceImpl.java @@ -0,0 +1,18 @@ +package com.ghb.base.modules.message.service.impl; + +import com.ghb.base.common.system.base.service.impl.GhbServiceImpl; +import com.ghb.base.modules.message.entity.SysMessage; +import com.ghb.base.modules.message.mapper.SysMessageMapper; +import com.ghb.base.modules.message.service.ISysMessageService; +import org.springframework.stereotype.Service; + +/** + * @Description: 消息 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +@Service +public class SysMessageServiceImpl extends GhbServiceImpl implements ISysMessageService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/impl/SysMessageTemplateServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/impl/SysMessageTemplateServiceImpl.java new file mode 100644 index 0000000..fbc287f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/service/impl/SysMessageTemplateServiceImpl.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.message.service.impl; + +import com.ghb.base.common.system.base.service.impl.GhbServiceImpl; +import com.ghb.base.modules.message.entity.SysMessageTemplate; +import com.ghb.base.modules.message.mapper.SysMessageTemplateMapper; +import com.ghb.base.modules.message.service.ISysMessageTemplateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.List; + +/** + * @Description: 消息模板 + * @Author: Ghb-boot + * @Date: 2019-04-09 + * @Version: V1.0 + */ +@Service +public class SysMessageTemplateServiceImpl extends GhbServiceImpl implements ISysMessageTemplateService { + + @Autowired + private SysMessageTemplateMapper sysMessageTemplateMapper; + + + @Override + public List selectByCode(String code) { + return sysMessageTemplateMapper.selectByCode(code); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/util/PushMsgUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/util/PushMsgUtil.java new file mode 100644 index 0000000..11b7b20 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/util/PushMsgUtil.java @@ -0,0 +1,81 @@ +package com.ghb.base.modules.message.util; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import com.ghb.base.modules.message.entity.SysMessage; +import com.ghb.base.modules.message.entity.SysMessageTemplate; +import com.ghb.base.modules.message.handle.enums.SendMsgStatusEnum; +import com.ghb.base.modules.message.service.ISysMessageService; +import com.ghb.base.modules.message.service.ISysMessageTemplateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.alibaba.fastjson.JSONObject; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 消息生成工具 + * @author: Ghb-boot + */ + +@Component +public class PushMsgUtil { + + @Autowired + private ISysMessageService sysMessageService; + + @Autowired + private ISysMessageTemplateService sysMessageTemplateService; + + @Autowired + private Configuration freemarkerConfig; + /** + * @param msgType 消息类型 1短信 2邮件 3微信 + * @param templateCode 消息模板码 + * @param map 消息参数 + * @param sentTo 接收消息方 + */ + public boolean sendMessage(String msgType, String templateCode, Map map, String sentTo) { + List sysSmsTemplates = sysMessageTemplateService.selectByCode(templateCode); + SysMessage sysMessage = new SysMessage(); + if (sysSmsTemplates.size() > 0) { + SysMessageTemplate sysSmsTemplate = sysSmsTemplates.get(0); + sysMessage.setEsType(msgType); + sysMessage.setEsReceiver(sentTo); + //模板标题 + String title = sysSmsTemplate.getTemplateName(); + //模板内容 + String content = sysSmsTemplate.getTemplateContent(); + StringWriter stringWriter = new StringWriter(); + Template template = null; + try { + template = new Template("SysMessageTemplate", content, freemarkerConfig); + template.process(map, stringWriter); + } catch (IOException e) { + e.printStackTrace(); + return false; + } catch (TemplateException e) { + e.printStackTrace(); + return false; + } + content = stringWriter.toString(); + sysMessage.setEsTitle(title); + sysMessage.setEsContent(content); + sysMessage.setEsParam(JSONObject.toJSONString(map)); + sysMessage.setEsSendTime(new Date()); + sysMessage.setEsSendStatus(SendMsgStatusEnum.WAIT.getCode()); + sysMessage.setEsSendNum(0); + if(sysMessageService.save(sysMessage)) { + return true; + } + } + return false; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/websocket/SocketHandler.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/websocket/SocketHandler.java new file mode 100644 index 0000000..8cc8bd1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/websocket/SocketHandler.java @@ -0,0 +1,40 @@ +package com.ghb.base.modules.message.websocket; + +import cn.hutool.core.util.ObjectUtil; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.common.constant.CommonSendStatus; +import org.jeecg.common.modules.redis.listener.JeecgRedisListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 监听消息(通过redis发布订阅,推送消息) + * 此方案:解决集群部署的问题,多实例节点(也就是发送消息端先发送消息到redis中,每个服务节点收到redis消息,再触发具体的ws推送) + * @author: Ghb-boot + */ +@Slf4j +@Component(WebSocket.REDIS_TOPIC_NAME) +public class SocketHandler implements JeecgRedisListener { + + @Autowired + private WebSocket webSocket; + + @Override + public void onMessage(BaseMap map) { + log.debug("【Redis发布订阅模式】redis Listener: {},参数:{}",WebSocket.REDIS_TOPIC_NAME, map.toString()); + + String userId = map.get("userId"); + String message = map.get("message"); + if (ObjectUtil.isNotEmpty(userId)) { + //pc端消息推送具体人 + webSocket.pushMessage(userId, message); + //app端消息推送具体人 + webSocket.pushMessage(userId+CommonSendStatus.APP_SESSION_SUFFIX, message); + } else { + //推送全部 + webSocket.pushMessage(message); + } + + } +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/websocket/WebSocket.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/websocket/WebSocket.java new file mode 100644 index 0000000..3385d5a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/message/websocket/WebSocket.java @@ -0,0 +1,191 @@ +package com.ghb.base.modules.message.websocket; + +import java.io.EOFException; +import java.nio.channels.ClosedChannelException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import jakarta.websocket.*; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; + +import com.alibaba.fastjson.JSONObject; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.common.constant.WebsocketConst; +import org.jeecg.common.modules.redis.client.JeecgRedisClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; + +/** + * @Author scott + * @Date 2019/11/29 9:41 + * @Description: 此注解相当于设置访问URL + */ +@Component +@Slf4j +@ServerEndpoint("/websocket/{userId}") +public class WebSocket { + + /**线程安全Map*/ + private static ConcurrentHashMap sessionPool = new ConcurrentHashMap<>(); + + /** + * Redis触发监听名字 + */ + public static final String REDIS_TOPIC_NAME = "socketHandler"; + + //避免初次调用出现空指针的情况 + private static JeecgRedisClient JeecgRedisClient; + @Autowired + private void setGhbRedisClient(JeecgRedisClient JeecgRedisClient){ + WebSocket.JeecgRedisClient = JeecgRedisClient; + } + + + //==========【websocket接受、推送消息等方法 —— 具体服务节点推送ws消息】======================================================================================== + @OnOpen + public void onOpen(Session session, @PathParam(value = "userId") String userId) { + try { + sessionPool.put(userId, session); + log.debug("【系统 WebSocket】有新的连接,总数为:" + sessionPool.size()); + } catch (Exception e) { + } + } + + @OnClose + public void onClose(@PathParam("userId") String userId) { + try { + sessionPool.remove(userId); + log.debug("【系统 WebSocket】连接断开,总数为:" + sessionPool.size()); + } catch (Exception e) { + log.error("【系统 WebSocket】连接断开异常", e); + } + } + + /** + * ws推送消息 + * + * @param userId + * @param message + */ + public void pushMessage(String userId, String message) { + for (Map.Entry item : sessionPool.entrySet()) { + //userId key值= {用户id + "_"+ 登录token的md5串} + //TODO vue2未改key新规则,暂时不影响逻辑 + if (item.getKey().contains(userId)) { + Session session = item.getValue(); + try { + // 代码逻辑说明: websocket报错 https://gitee.com/Ghb/Ghb-boot/issues/I4C0MU + synchronized (session){ + log.debug("【系统 WebSocket】推送单人消息:" + message); + session.getBasicRemote().sendText(message); + } + } catch (Exception e) { + log.error(e.getMessage(),e); + } + } + } + } + + /** + * ws遍历群发消息 + */ + public void pushMessage(String message) { + try { + for (Map.Entry item : sessionPool.entrySet()) { + try { + item.getValue().getAsyncRemote().sendText(message); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + log.debug("【系统 WebSocket】群发消息:" + message); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + + /** + * ws接受客户端消息 + */ + @OnMessage + public void onMessage(String message, @PathParam(value = "userId") String userId) { + if(!"ping".equals(message) && !WebsocketConst.CMD_CHECK.equals(message)){ + log.debug("【系统 WebSocket】收到客户端消息:" + message); + }else{ + log.debug("【系统 WebSocket】收到客户端消息:" + message); + // 代码逻辑说明: 【issues/1161】前端websocket因心跳导致监听不起作用--- + this.sendMessage(userId, "ping"); + } + +// //------------------------------------------------------------------------------ +// JSONObject obj = new JSONObject(); +// //业务类型 +// obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_CHECK); +// //消息内容 +// obj.put(WebsocketConst.MSG_TXT, "心跳响应"); +// this.pushMessage(userId, obj.toJSONString()); +// //------------------------------------------------------------------------------ + } + + /** + * 配置错误信息处理 + * + * @param session + * @param t + */ + @OnError + public void onError(Session session, Throwable t) { + // ClosedChannelException / EOFException 是应用关闭时 Tomcat 主动断开连接的正常现象,降级为 debug + Throwable cause = t.getCause() != null ? t.getCause() : t; + if (cause instanceof ClosedChannelException || cause instanceof EOFException) { + log.debug("【系统 WebSocket】连接已关闭(正常关闭): {}", t.getMessage()); + } else { + log.warn("【系统 WebSocket】消息出现错误", t); + } + } + //==========【系统 WebSocket接受、推送消息等方法 —— 具体服务节点推送ws消息】======================================================================================== + + + //==========【采用redis发布订阅模式——推送消息】======================================================================================== + /** + * 后台发送消息到redis + * + * @param message + */ + public void sendMessage(String message) { + //log.debug("【系统 WebSocket】广播消息:" + message); + BaseMap baseMap = new BaseMap(); + baseMap.put("userId", ""); + baseMap.put("message", message); + JeecgRedisClient.sendMessage(WebSocket.REDIS_TOPIC_NAME, baseMap); + } + + /** + * 此为单点消息 redis + * + * @param userId + * @param message + */ + public void sendMessage(String userId, String message) { + BaseMap baseMap = new BaseMap(); + baseMap.put("userId", userId); + baseMap.put("message", message); + JeecgRedisClient.sendMessage(WebSocket.REDIS_TOPIC_NAME, baseMap); + } + + /** + * 此为单点消息(多人) redis + * + * @param userIds + * @param message + */ + public void sendMessage(String[] userIds, String message) { + for (String userId : userIds) { + sendMessage(userId, message); + } + } + //=======【采用redis发布订阅模式——推送消息】========================================================================================== + +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/CustomActuatorConfig.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/CustomActuatorConfig.java new file mode 100644 index 0000000..c9efc2b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/CustomActuatorConfig.java @@ -0,0 +1,38 @@ +package com.ghb.base.modules.monitor.actuator; + +import com.ghb.base.modules.monitor.actuator.httptrace.CustomInMemoryHttpTraceRepository; +import org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesProperties; +import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自定义健康监控配置类 + * + * @Author: chenrui + * @Date: 2024/5/13 17:20 + */ +@Configuration +@EnableConfigurationProperties(HttpExchangesProperties.class) +@AutoConfigureBefore(HttpExchangesAutoConfiguration.class) +public class CustomActuatorConfig { + + /** + * 请求追踪 + * @return + * @author chenrui + * @date 2024/5/14 14:52 + */ + @Bean + @ConditionalOnProperty(prefix = "management.trace.http", name = "enabled", matchIfMissing = true) + @ConditionalOnMissingBean(HttpExchangeRepository.class) + public CustomInMemoryHttpTraceRepository traceRepository() { + return new CustomInMemoryHttpTraceRepository(); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/httptrace/CustomHttpTraceEndpoint.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/httptrace/CustomHttpTraceEndpoint.java new file mode 100644 index 0000000..02cd1cb --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/httptrace/CustomHttpTraceEndpoint.java @@ -0,0 +1,44 @@ +package com.ghb.base.modules.monitor.actuator.httptrace; + +import lombok.Getter; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.web.exchanges.HttpExchange; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import java.util.List; + +import static org.springframework.boot.actuate.endpoint.annotation.Selector.Match.ALL_REMAINING; + +/** + * @Description: ENDPOINT: 请求追踪(新),支持通过responseCode筛选 + * @Author: chenrui + * @Date: 2024/5/13 17:02 + */ +@Component +@Endpoint(id = "ghbhttptrace") +public class CustomHttpTraceEndpoint{ + private final CustomInMemoryHttpTraceRepository repository; + + public CustomHttpTraceEndpoint(CustomInMemoryHttpTraceRepository repository) { + Assert.notNull(repository, "Repository must not be null"); + this.repository = repository; + } + + @ReadOperation + public HttpTraceDescriptor traces(@Selector(match = ALL_REMAINING) String query) { + return new HttpTraceDescriptor(this.repository.findAll(query)); + } + + @Getter + public static final class HttpTraceDescriptor { + private final List traces; + + private HttpTraceDescriptor(List traces) { + this.traces = traces; + } + + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/httptrace/CustomInMemoryHttpTraceRepository.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/httptrace/CustomInMemoryHttpTraceRepository.java new file mode 100644 index 0000000..5eda992 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/httptrace/CustomInMemoryHttpTraceRepository.java @@ -0,0 +1,109 @@ +package com.ghb.base.modules.monitor.actuator.httptrace; + +import org.springframework.boot.actuate.web.exchanges.HttpExchange; +import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @Description: 自定义内存请求追踪存储 + * @Author: chenrui + * @Date: 2024/5/13 17:02 + */ +public class CustomInMemoryHttpTraceRepository extends InMemoryHttpExchangeRepository { + + @Override + public List findAll() { + return super.findAll(); + } + + /** + * for [issues/8309]系统监控>请求追踪,列表每刷新一下,总数据就减一#8309 + * @param trace + * @author chenrui + * @date 2025/6/4 19:38 + */ + @Override + public void add(HttpExchange trace) { + // 只有当请求不是OPTIONS方法,并且URI不包含httptrace时才记录数据 + if (!"OPTIONS".equals(trace.getRequest().getMethod()) && + !trace.getRequest().getUri().toString().contains("httptrace")) { + super.add(trace); + } + } + + public List findAll(String query) { + List allTrace = super.findAll(); + if (null != allTrace && !allTrace.isEmpty()) { + Stream stream = allTrace.stream(); + String[] params = query.split(","); + stream = filter(params, stream); + stream = sort(params, stream); + allTrace = stream.collect(Collectors.toList()); + } + return allTrace; + } + + private Stream sort(String[] params, Stream stream) { + if (params.length < 2) { + return stream; + } + String sortBy = params[1]; + String order; + if (params.length > 2) { + order = params[2]; + } else { + order = "desc"; + } + return stream.sorted((o1, o2) -> { + int i = 0; + if("timeTaken".equalsIgnoreCase(sortBy)) { + i = o1.getTimeTaken().compareTo(o2.getTimeTaken()); + }else if("timestamp".equalsIgnoreCase(sortBy)){ + i = o1.getTimestamp().compareTo(o2.getTimestamp()); + } + if("desc".equalsIgnoreCase(order)){ + i *=-1; + } + return i; + }); + } + + private static Stream filter(String[] params, Stream stream) { + if (params.length == 0) { + return stream; + } + String statusQuery = params[0]; + if (null != statusQuery && !statusQuery.isEmpty()) { + statusQuery = statusQuery.toLowerCase().trim(); + switch (statusQuery) { + case "error": + stream = stream.filter(httpTrace -> { + int status = httpTrace.getResponse().getStatus(); + return status >= 404 && status < 501; + }); + break; + case "warn": + stream = stream.filter(httpTrace -> { + int status = httpTrace.getResponse().getStatus(); + return status >= 201 && status < 404; + }); + break; + case "success": + stream = stream.filter(httpTrace -> { + int status = httpTrace.getResponse().getStatus(); + return status == 200; + }); + break; + case "all": + default: + break; + } + return stream; + } + return stream; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/undertow/CustomUndertowMetricsHandler.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/undertow/CustomUndertowMetricsHandler.java new file mode 100644 index 0000000..1d052e7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/actuator/undertow/CustomUndertowMetricsHandler.java @@ -0,0 +1,88 @@ +//package com.ghb.base.modules.monitor.actuator.undertow; +// +//import io.micrometer.core.instrument.MeterRegistry; +//import io.undertow.server.HttpHandler; +//import io.undertow.server.HttpServerExchange; +//import io.undertow.server.session.*; +//import org.springframework.stereotype.Component; +// +//import java.util.concurrent.atomic.AtomicInteger; +//import java.util.concurrent.atomic.LongAdder; +// +///** +// * 自定义undertow监控指标工具类 +// * for [QQYUN-11902]tomcat 替换undertow 这里的功能还没修改 +// * @author chenrui +// * @date 2025/4/8 19:06 +// */ +//@Component("GhbCustomUndertowMetricsHandler") +//public class CustomUndertowMetricsHandler { +// +// // 用于统计已创建的 session 数量 +// private final LongAdder sessionsCreated = new LongAdder(); +// +// // 用于统计已销毁的 session 数量 +// private final LongAdder sessionsExpired = new LongAdder(); +// +// // 当前活跃的 session 数量 +// private final AtomicInteger activeSessions = new AtomicInteger(); +// +// // 历史最大活跃 session 数 +// private final AtomicInteger maxActiveSessions = new AtomicInteger(); +// +// // Undertow 内存 session 管理器(用于创建与管理 session) +// private final InMemorySessionManager sessionManager = new InMemorySessionManager("undertow-session-manager"); +// +// // 使用 Cookie 存储 session ID +// private final SessionConfig sessionConfig = new SessionCookieConfig(); +// +// /** +// * 构造函数 +// * @param meterRegistry +// * @author chenrui +// * @date 2025/4/8 19:07 +// */ +// public CustomUndertowMetricsHandler(MeterRegistry meterRegistry) { +// // 注册 Micrometer 指标 +// meterRegistry.gauge("undertow.sessions.created", sessionsCreated, LongAdder::longValue); +// meterRegistry.gauge("undertow.sessions.expired", sessionsExpired, LongAdder::longValue); +// meterRegistry.gauge("undertow.sessions.active.current", activeSessions, AtomicInteger::get); +// meterRegistry.gauge("undertow.sessions.active.max", maxActiveSessions, AtomicInteger::get); +// +// // 添加 session 生命周期监听器,统计 session 创建与销毁 +// sessionManager.registerSessionListener(new SessionListener() { +// @Override +// public void sessionCreated(Session session, HttpServerExchange exchange) { +// sessionsCreated.increment(); +// int now = activeSessions.incrementAndGet(); +// maxActiveSessions.getAndUpdate(max -> Math.max(max, now)); +// } +// +// @Override +// public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) { +// sessionsExpired.increment(); +// activeSessions.decrementAndGet(); +// } +// }); +// } +// +// /** +// * 包装 Undertow 的 HttpHandler,实现 session 自动创建逻辑 +// * @param next +// * @return +// * @author chenrui +// * @date 2025/4/8 19:07 +// */ +// public HttpHandler wrap(HttpHandler next) { +// return exchange -> { +// // 获取当前 session,如果不存在则创建 +// Session session = sessionManager.getSession(exchange, sessionConfig); +// if (session == null) { +// sessionManager.createSession(exchange, sessionConfig); +// } +// +// // 执行下一个 Handler +// next.handleRequest(exchange); +// }; +// } +//} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/controller/ActuatorMemoryController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/controller/ActuatorMemoryController.java new file mode 100644 index 0000000..5ac5d37 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/controller/ActuatorMemoryController.java @@ -0,0 +1,55 @@ +package com.ghb.base.modules.monitor.controller; + +import cn.hutool.core.util.NumberUtil; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.util.HashMap; +import java.util.Map; + +/** + * @Description: 内存健康检查 + * @author: chenrui + */ +@Slf4j +@RestController +@RequestMapping("/sys/actuator/memory") +public class ActuatorMemoryController { + + + /** + * 内存详情 + * @return + */ + @GetMapping("/info") + public Result getRedisInfo() { + Runtime runtime = Runtime.getRuntime(); + Map result = new HashMap<>(); + result.put("memory.runtime.total", runtime.totalMemory()); + result.put("memory.runtime.used", runtime.freeMemory()); + result.put("memory.runtime.max", runtime.totalMemory() - runtime.freeMemory()); + result.put("memory.runtime.free", runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()); + result.put("memory.runtime.usage", NumberUtil.div(runtime.totalMemory() - runtime.freeMemory(), runtime.totalMemory())); + // 代码逻辑说明: [TV360X-1695]内存信息-立即更新 功能报错 #6635------------ + OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean(); + if (operatingSystemMXBean instanceof com.sun.management.OperatingSystemMXBean) { + com.sun.management.OperatingSystemMXBean opBean = (com.sun.management.OperatingSystemMXBean) operatingSystemMXBean; +// JSONObject operatingSystemJson = JSONObject.parseObject(JSONObject.toJSONString(operatingSystemMXBean)); + long totalPhysicalMemory = opBean.getTotalPhysicalMemorySize(); + long freePhysicalMemory = opBean.getFreePhysicalMemorySize(); + long usedPhysicalMemory = totalPhysicalMemory - freePhysicalMemory; + result.put("memory.physical.total", totalPhysicalMemory); + result.put("memory.physical.used", freePhysicalMemory); + result.put("memory.physical.free", usedPhysicalMemory); + result.put("memory.physical.usage", NumberUtil.div(usedPhysicalMemory, totalPhysicalMemory)); + } + return Result.ok(result); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/controller/ActuatorRedisController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/controller/ActuatorRedisController.java new file mode 100644 index 0000000..8cfb3af --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/controller/ActuatorRedisController.java @@ -0,0 +1,134 @@ +package com.ghb.base.modules.monitor.controller; + +import com.alibaba.fastjson.JSONArray; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.monitor.domain.RedisInfo; +import com.ghb.base.modules.monitor.service.RedisService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Description: ActuatorRedisController + * @author: Ghb-boot + */ +@Slf4j +@RestController +@RequestMapping("/sys/actuator/redis") +public class ActuatorRedisController { + + @Autowired + private RedisService redisService; + + /** + * Redis详细信息 + * @return + * @throws Exception + */ + @GetMapping("/info") + public Result getRedisInfo() throws Exception { + List infoList = this.redisService.getRedisInfo(); + //log.info(infoList.toString()); + return Result.ok(infoList); + } + + /** + * Redis历史性能指标查询(过去一小时) + * @return + * @throws Exception + * @author chenrui + * @date 2024/5/14 14:56 + */ + @GetMapping(value = "/metrics/history") + public Result getMetricsHistory() throws Exception { + Map>> metricsHistory = this.redisService.getMetricsHistory(); + return Result.OK(metricsHistory); + } + + @GetMapping("/keysSize") + public Map getKeysSize() throws Exception { + return redisService.getKeysSize(); + } + + /** + * 获取redis key数量 for 报表 + * @return + * @throws Exception + */ + @GetMapping("/keysSizeForReport") + public Map getKeysSizeReport() throws Exception { + return redisService.getMapForReport("1"); + } + /** + * 获取redis 内存 for 报表 + * + * @return + * @throws Exception + */ + @GetMapping("/memoryForReport") + public Map memoryForReport() throws Exception { + return redisService.getMapForReport("2"); + } + /** + * 获取redis 全部信息 for 报表 + * @return + * @throws Exception + */ + @GetMapping("/infoForReport") + public Map infoForReport() throws Exception { + return redisService.getMapForReport("3"); + } + + @GetMapping("/memoryInfo") + public Map getMemoryInfo() throws Exception { + return redisService.getMemoryInfo(); + } + + /** + * @功能:获取磁盘信息 + * @param request + * @param response + * @return + */ + @GetMapping("/queryDiskInfo") + public Result>> queryDiskInfo(HttpServletRequest request, HttpServletResponse response){ + Result>> res = new Result<>(); + try { + // 当前文件系统类 + FileSystemView fsv = FileSystemView.getFileSystemView(); + // 列出所有windows 磁盘 + File[] fs = File.listRoots(); + log.info("查询磁盘信息:"+fs.length+"个"); + List> list = new ArrayList<>(); + + for (int i = 0; i < fs.length; i++) { + if(fs[i].getTotalSpace()==0) { + continue; + } + Map map = new HashMap(5); + map.put("name", fsv.getSystemDisplayName(fs[i])); + map.put("max", fs[i].getTotalSpace()); + map.put("rest", fs[i].getFreeSpace()); + map.put("restPPT", (fs[i].getTotalSpace()-fs[i].getFreeSpace())*100/fs[i].getTotalSpace()); + list.add(map); + log.info(map.toString()); + } + res.setResult(list); + res.success("查询成功"); + } catch (Exception e) { + res.error500("查询失败"+e.getMessage()); + } + return res; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/domain/RedisInfo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/domain/RedisInfo.java new file mode 100644 index 0000000..96bad53 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/domain/RedisInfo.java @@ -0,0 +1,141 @@ +package com.ghb.base.modules.monitor.domain; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Description: redis信息 + * @author: Ghb-boot + */ +public class RedisInfo { + + private static Map map = new HashMap(5); + + static { + map.put("redis_version", "Redis 服务器版本"); + map.put("redis_git_sha1", "Git SHA1"); + map.put("redis_git_dirty", "Git dirty flag"); + map.put("os", "Redis 服务器的宿主操作系统"); + map.put("arch_bits", " 架构(32 或 64 位)"); + map.put("multiplexing_api", "Redis 所使用的事件处理机制"); + map.put("gcc_version", "编译 Redis 时所使用的 GCC 版本"); + map.put("process_id", "服务器进程的 PID"); + map.put("run_id", "Redis 服务器的随机标识符(用于 Sentinel 和集群)"); + map.put("tcp_port", "TCP/IP 监听端口"); + map.put("uptime_in_seconds", "自 Redis 服务器启动以来,经过的秒数"); + map.put("uptime_in_days", "自 Redis 服务器启动以来,经过的天数"); + map.put("lru_clock", " 以分钟为单位进行自增的时钟,用于 LRU 管理"); + map.put("connected_clients", "已连接客户端的数量(不包括通过从属服务器连接的客户端)"); + map.put("client_longest_output_list", "当前连接的客户端当中,最长的输出列表"); + map.put("client_longest_input_buf", "当前连接的客户端当中,最大输入缓存"); + map.put("blocked_clients", "正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量"); + map.put("used_memory", "由 Redis 分配器分配的内存总量,以字节(byte)为单位"); + map.put("used_memory_human", "以人类可读的格式返回 Redis 分配的内存总量"); + map.put("used_memory_rss", "从操作系统的角度,返回 Redis 已分配的内存总量(俗称常驻集大小)。这个值和 top 、 ps 等命令的输出一致"); + map.put("used_memory_peak", " Redis 的内存消耗峰值(以字节为单位)"); + map.put("used_memory_peak_human", "以人类可读的格式返回 Redis 的内存消耗峰值"); + map.put("used_memory_lua", "Lua 引擎所使用的内存大小(以字节为单位)"); + map.put("mem_fragmentation_ratio", "sed_memory_rss 和 used_memory 之间的比率"); + map.put("mem_allocator", "在编译时指定的, Redis 所使用的内存分配器。可以是 libc 、 jemalloc 或者 tcmalloc"); + + map.put("redis_build_id", "redis_build_id"); + map.put("redis_mode", "运行模式,单机(standalone)或者集群(cluster)"); + map.put("atomicvar_api", "atomicvar_api"); + map.put("hz", "redis内部调度(进行关闭timeout的客户端,删除过期key等等)频率,程序规定serverCron每秒运行10次。"); + map.put("executable", "server脚本目录"); + map.put("config_file", "配置文件目录"); + map.put("client_biggest_input_buf", "当前连接的客户端当中,最大输入缓存,用client list命令观察qbuf和qbuf-free两个字段最大值"); + map.put("used_memory_rss_human", "以人类可读的方式返回 Redis 已分配的内存总量"); + map.put("used_memory_peak_perc", "内存使用率峰值"); + map.put("total_system_memory", "系统总内存"); + map.put("total_system_memory_human", "以人类可读的方式返回系统总内存"); + map.put("used_memory_lua_human", "以人类可读的方式返回Lua 引擎所使用的内存大小"); + map.put("maxmemory", "最大内存限制,0表示无限制"); + map.put("maxmemory_human", "以人类可读的方式返回最大限制内存"); + map.put("maxmemory_policy", "超过内存限制后的处理策略"); + map.put("loading", "服务器是否正在载入持久化文件"); + map.put("rdb_changes_since_last_save", "离最近一次成功生成rdb文件,写入命令的个数,即有多少个写入命令没有持久化"); + map.put("rdb_bgsave_in_progress", "服务器是否正在创建rdb文件"); + map.put("rdb_last_save_time", "离最近一次成功创建rdb文件的时间戳。当前时间戳 - rdb_last_save_time=多少秒未成功生成rdb文件"); + map.put("rdb_last_bgsave_status", "最近一次rdb持久化是否成功"); + map.put("rdb_last_bgsave_time_sec", "最近一次成功生成rdb文件耗时秒数"); + map.put("rdb_current_bgsave_time_sec", "如果服务器正在创建rdb文件,那么这个域记录的就是当前的创建操作已经耗费的秒数"); + map.put("aof_enabled", "是否开启了aof"); + map.put("aof_rewrite_in_progress", "标识aof的rewrite操作是否在进行中"); + map.put("aof_rewrite_scheduled", "rewrite任务计划,当客户端发送bgrewriteaof指令,如果当前rewrite子进程正在执行,那么将客户端请求的bgrewriteaof变为计划任务,待aof子进程结束后执行rewrite "); + + map.put("aof_last_rewrite_time_sec", "最近一次aof rewrite耗费的时长"); + map.put("aof_current_rewrite_time_sec", "如果rewrite操作正在进行,则记录所使用的时间,单位秒"); + map.put("aof_last_bgrewrite_status", "上次bgrewrite aof操作的状态"); + map.put("aof_last_write_status", "上次aof写入状态"); + + map.put("total_commands_processed", "redis处理的命令数"); + map.put("total_connections_received", "新创建连接个数,如果新创建连接过多,过度地创建和销毁连接对性能有影响,说明短连接严重或连接池使用有问题,需调研代码的连接设置"); + map.put("instantaneous_ops_per_sec", "redis当前的qps,redis内部较实时的每秒执行的命令数"); + map.put("total_net_input_bytes", "redis网络入口流量字节数"); + map.put("total_net_output_bytes", "redis网络出口流量字节数"); + + map.put("instantaneous_input_kbps", "redis网络入口kps"); + map.put("instantaneous_output_kbps", "redis网络出口kps"); + map.put("rejected_connections", "拒绝的连接个数,redis连接个数达到maxclients限制,拒绝新连接的个数"); + map.put("sync_full", "主从完全同步成功次数"); + + map.put("sync_partial_ok", "主从部分同步成功次数"); + map.put("sync_partial_err", "主从部分同步失败次数"); + map.put("expired_keys", "运行以来过期的key的数量"); + map.put("evicted_keys", "运行以来剔除(超过了maxmemory后)的key的数量"); + map.put("keyspace_hits", "命中次数"); + map.put("keyspace_misses", "没命中次数"); + map.put("pubsub_channels", "当前使用中的频道数量"); + map.put("pubsub_patterns", "当前使用的模式的数量"); + map.put("latest_fork_usec", "最近一次fork操作阻塞redis进程的耗时数,单位微秒"); + map.put("role", "实例的角色,是master or slave"); + map.put("connected_slaves", "连接的slave实例个数"); + map.put("master_repl_offset", "主从同步偏移量,此值如果和上面的offset相同说明主从一致没延迟"); + map.put("repl_backlog_active", "复制积压缓冲区是否开启"); + map.put("repl_backlog_size", "复制积压缓冲大小"); + map.put("repl_backlog_first_byte_offset", "复制缓冲区里偏移量的大小"); + map.put("repl_backlog_histlen", "此值等于 master_repl_offset - repl_backlog_first_byte_offset,该值不会超过repl_backlog_size的大小"); + map.put("used_cpu_sys", "将所有redis主进程在核心态所占用的CPU时求和累计起来"); + map.put("used_cpu_user", "将所有redis主进程在用户态所占用的CPU时求和累计起来"); + map.put("used_cpu_sys_children", "将后台进程在核心态所占用的CPU时求和累计起来"); + map.put("used_cpu_user_children", "将后台进程在用户态所占用的CPU时求和累计起来"); + map.put("cluster_enabled", "实例是否启用集群模式"); + map.put("db0", "db0的key的数量,以及带有生存期的key的数,平均存活时间"); + + } + + private String key; + private String value; + private String description; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + this.description = map.get(this.key); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String toString() { + return "RedisInfo{" + "key='" + key + '\'' + ", value='" + value + '\'' + ", desctiption='" + description + '\'' + '}'; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/exception/RedisConnectException.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/exception/RedisConnectException.java new file mode 100644 index 0000000..94621f9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/exception/RedisConnectException.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.monitor.exception; + +/** + * Redis 连接异常 + * @author: Ghb-boot + */ +public class RedisConnectException extends Exception { + + private static final long serialVersionUID = 1639374111871115063L; + + public RedisConnectException(String message) { + super(message); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/RedisService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/RedisService.java new file mode 100644 index 0000000..7ef15ea --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/RedisService.java @@ -0,0 +1,55 @@ +package com.ghb.base.modules.monitor.service; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.alibaba.fastjson.JSONArray; +import com.ghb.base.modules.monitor.domain.RedisInfo; +import com.ghb.base.modules.monitor.exception.RedisConnectException; + +/** + * @Description: redis信息service接口 + * @author: Ghb-boot + */ +public interface RedisService { + + /** + * 获取 redis 的详细信息 + * + * @return List + * @throws RedisConnectException + */ + List getRedisInfo() throws RedisConnectException; + + /** + * 获取 redis key 数量 + * + * @return Map + * @throws RedisConnectException + */ + Map getKeysSize() throws RedisConnectException; + + /** + * 获取 redis 内存信息 + * + * @return Map + * @throws RedisConnectException + */ + Map getMemoryInfo() throws RedisConnectException; + /** + * 获取 报表需要个redis信息 + * @param type + * @return Map + * @throws RedisConnectException + */ + Map getMapForReport(String type) throws RedisConnectException ; + + /** + * 获取历史性能指标 + * @return + * @author chenrui + * @date 2024/5/14 14:57 + */ + Map>> getMetricsHistory(); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/impl/MailHealthIndicator.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/impl/MailHealthIndicator.java new file mode 100644 index 0000000..5b121f4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/impl/MailHealthIndicator.java @@ -0,0 +1,29 @@ +package com.ghb.base.modules.monitor.service.impl; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +/** + * 功能说明:自定义邮件检测 + * + * @author: 李波 + * @email: 503378406@qq.com + * @date: 2019-06-29 + */ +@Component +public class MailHealthIndicator implements HealthIndicator { + + + @Override public Health health() { + int errorCode = check(); + if (errorCode != 0) { + return Health.down().withDetail("Error Code", errorCode) .build(); + } + return Health.up().build(); + } + int check(){ + //可以实现自定义的数据库检测逻辑 + return 0; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/impl/RedisServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/impl/RedisServiceImpl.java new file mode 100644 index 0000000..f6a7ecf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/monitor/service/impl/RedisServiceImpl.java @@ -0,0 +1,172 @@ +package com.ghb.base.modules.monitor.service.impl; + +import java.util.*; + +import jakarta.annotation.Resource; + +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Maps; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.monitor.domain.RedisInfo; +import com.ghb.base.modules.monitor.exception.RedisConnectException; +import com.ghb.base.modules.monitor.service.RedisService; +import org.springframework.cglib.beans.BeanMap; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +/** + * Redis 监控信息获取 + * + * @Author MrBird + */ +@Service("redisService") +@Slf4j +public class RedisServiceImpl implements RedisService { + + @Resource + private RedisConnectionFactory redisConnectionFactory; + + /** + * redis信息 + */ + private static final String REDIS_MESSAGE = "3"; + + /** + * redis性能信息记录 + */ + private static final Map>> REDIS_METRICS = new HashMap<>(2); + + /** + * Redis详细信息 + */ + @Override + public List getRedisInfo() throws RedisConnectException { + Properties info = redisConnectionFactory.getConnection().info(); + List infoList = new ArrayList<>(); + RedisInfo redisInfo = null; + for (Map.Entry entry : info.entrySet()) { + redisInfo = new RedisInfo(); + redisInfo.setKey(oConvertUtils.getString(entry.getKey())); + redisInfo.setValue(oConvertUtils.getString(entry.getValue())); + infoList.add(redisInfo); + } + return infoList; + } + + @Override + public Map getKeysSize() throws RedisConnectException { + Long dbSize = redisConnectionFactory.getConnection().dbSize(); + Map map = new HashMap(5); + map.put("create_time", System.currentTimeMillis()); + map.put("dbSize", dbSize); + + log.debug("--getKeysSize--: " + map.toString()); + return map; + } + + @Override + public Map getMemoryInfo() throws RedisConnectException { + Map map = null; + Properties info = redisConnectionFactory.getConnection().info(); + for (Map.Entry entry : info.entrySet()) { + String key = oConvertUtils.getString(entry.getKey()); + if ("used_memory".equals(key)) { + map = new HashMap(5); + map.put("used_memory", entry.getValue()); + map.put("create_time", System.currentTimeMillis()); + } + } + log.debug("--getMemoryInfo--: " + map.toString()); + return map; + } + + /** + * 查询redis信息for报表 + * @param type 1redis key数量 2 占用内存 3redis信息 + * @return + * @throws RedisConnectException + */ + @Override + public Map getMapForReport(String type) throws RedisConnectException { + Map mapJson=new HashMap(5); + JSONArray json = new JSONArray(); + if(REDIS_MESSAGE.equals(type)){ + List redisInfo = getRedisInfo(); + for(RedisInfo info:redisInfo){ + Map map= Maps.newHashMap(); + BeanMap beanMap = BeanMap.create(info); + for (Object key : beanMap.keySet()) { + map.put(key+"", beanMap.get(key)); + } + json.add(map); + } + mapJson.put("data",json); + return mapJson; + } + int length = 5; + for(int i = 0; i < length; i++){ + JSONObject jo = new JSONObject(); + Map map; + if("1".equals(type)){ + map= getKeysSize(); + jo.put("value",map.get("dbSize")); + }else{ + map = getMemoryInfo(); + Integer usedMemory = Integer.valueOf(map.get("used_memory").toString()); + jo.put("value",usedMemory/1000); + } + String createTime = DateUtil.formatTime(DateUtil.date((Long) map.get("create_time")-(4-i)*1000)); + jo.put("name",createTime); + json.add(jo); + } + mapJson.put("data",json); + return mapJson; + } + + /** + * 获取历史性能指标 + * @return + * @author chenrui + * @date 2024/5/14 14:57 + */ + @Override + public Map>> getMetricsHistory() { + return REDIS_METRICS; + } + + /** + * 记录近一小时redis监控数据
+ * 60s一次,,记录存储keysize和内存 + * @throws RedisConnectException + * @author chenrui + * @date 2024/5/14 14:09 + */ + @Scheduled(fixedRate = 60000) + public void recordCustomMetric() throws RedisConnectException { + List> list= new ArrayList<>(); + if(REDIS_METRICS.containsKey("dbSize")){ + list = REDIS_METRICS.get("dbSize"); + }else{ + REDIS_METRICS.put("dbSize",list); + } + if(list.size()>60){ + list.remove(0); + } + list.add(getKeysSize()); + list= new ArrayList<>(); + if(REDIS_METRICS.containsKey("memory")){ + list = REDIS_METRICS.get("memory"); + }else{ + REDIS_METRICS.put("memory",list); + } + if(list.size()>60){ + list.remove(0); + } + list.add(getMemoryInfo()); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/aop/LogRecordAspect.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/aop/LogRecordAspect.java new file mode 100644 index 0000000..c2bef12 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/aop/LogRecordAspect.java @@ -0,0 +1,46 @@ +//package com.ghb.base.modules.ngalain.aop; +// +//import jakarta.servlet.http.HttpServletRequest; +// +//import org.aspectj.lang.ProceedingJoinPoint; +//import org.aspectj.lang.annotation.Around; +//import org.aspectj.lang.annotation.Aspect; +//import org.aspectj.lang.annotation.Pointcut; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.context.request.RequestAttributes; +//import org.springframework.web.context.request.RequestContextHolder; +//import org.springframework.web.context.request.ServletRequestAttributes; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory;; +// +// +//// 暂时注释掉,提高系统性能 +////@Aspect //定义一个切面 +////@Configuration +//public class LogRecordAspect { +//private static final Logger logger = LoggerFactory.getLogger(LogRecordAspect.class); +// +// // 定义切点Pointcut +// @Pointcut("execution(public * com.ghb.base.modules.*.*.*Controller.*(..))") +// public void excudeService() { +// } +// +// @Around("excudeService()") +// public Object doAround(ProceedingJoinPoint pjp) throws Throwable { +// RequestAttributes ra = RequestContextHolder.getRequestAttributes(); +// ServletRequestAttributes sra = (ServletRequestAttributes) ra; +// HttpServletRequest request = sra.getRequest(); +// +// String url = request.getRequestURL().toString(); +// String method = request.getMethod(); +// String uri = request.getRequestURI(); +// String queryString = request.getQueryString(); +// logger.info("请求开始, 各个参数, url: {}, method: {}, uri: {}, params: {}", url, method, uri, queryString); +// +// // result的值就是被拦截方法的返回值 +// Object result = pjp.proceed(); +// +// logger.info("请求结束,controller的返回值是 " + result); +// return result; +// } +//} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/controller/NgAlainController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/controller/NgAlainController.java new file mode 100644 index 0000000..1b95c87 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/controller/NgAlainController.java @@ -0,0 +1,86 @@ +//package com.ghb.base.modules.ngalain.controller; +// +//import java.util.ArrayList; +//import java.util.List; +//import java.util.Map; +// +//import jakarta.servlet.http.HttpServletRequest; +// +//import org.apache.shiro.SecurityUtils; +//import com.ghb.base.common.api.vo.Result; +//import com.ghb.base.common.system.vo.DictModel; +//import com.ghb.base.common.system.vo.LoginUser; +//import com.ghb.base.modules.ngalain.service.NgAlainService; +//import com.ghb.base.modules.system.service.ISysDictService; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.web.bind.annotation.PathVariable; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RequestMethod; +//import org.springframework.web.bind.annotation.ResponseBody; +//import org.springframework.web.bind.annotation.RestController; +// +//import com.alibaba.fastjson.JSONObject; +// +//import lombok.extern.slf4j.Slf4j; +// +//@Slf4j +//@RestController +//@RequestMapping("/sys/ng-alain") +//public class NgAlainController { +// @Autowired +// private NgAlainService ngAlainService; +// @Autowired +// private ISysDictService sysDictService; +// +// @RequestMapping(value = "/getAppData") +// @ResponseBody +// public JSONObject getAppData(HttpServletRequest request) throws Exception { +// String token=request.getHeader("X-Access-Token"); +// JSONObject j = new JSONObject(); +// LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); +// JSONObject userObjcet = new JSONObject(); +// userObjcet.put("name", user.getUsername()); +// userObjcet.put("avatar", user.getAvatar()); +// userObjcet.put("email", user.getEmail()); +// userObjcet.put("token", token); +// j.put("user", userObjcet); +// j.put("menu",ngAlainService.getMenu(user.getUsername())); +// JSONObject app = new JSONObject(); +// app.put("name", "Ghb-boot-angular"); +// app.put("description", "Ghb+ng-alain整合版本"); +// j.put("app", app); +// return j; +// } +// +// @RequestMapping(value = "/getDictItems/{dictCode}", method = RequestMethod.GET) +// public Object getDictItems(@PathVariable String dictCode) { +// log.info(" dictCode : "+ dictCode); +// Result> result = new Result>(); +// List ls = null; +// try { +// ls = sysDictService.queryDictItemsByCode(dictCode); +// result.setSuccess(true); +// result.setResult(ls); +// } catch (Exception e) { +// log.error(e.getMessage(),e); +// result.error500("操作失败"); +// return result; +// } +// List dictlist=new ArrayList<>(); +// for (DictModel l : ls) { +// JSONObject dict=new JSONObject(); +// try { +// dict.put("value",Integer.parseInt(l.getValue())); +// } catch (NumberFormatException e) { +// dict.put("value",l.getValue()); +// } +// dict.put("label",l.getText()); +// dictlist.add(dict); +// } +// return dictlist; +// } +// @RequestMapping(value = "/getDictItemsByTable/{table}/{key}/{value}", method = RequestMethod.GET) +// public Object getDictItemsByTable(@PathVariable String table,@PathVariable String key,@PathVariable String value) { +// return this.ngAlainService.getDictByTable(table,key,value); +// } +//} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/service/NgAlainService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/service/NgAlainService.java new file mode 100644 index 0000000..baf44ce --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/service/NgAlainService.java @@ -0,0 +1,37 @@ +//package com.ghb.base.modules.ngalain.service; +// +//import com.alibaba.fastjson.JSONArray; +// +//import java.util.List; +//import java.util.Map; +// +///** +// * @Description: NgAlainService接口 +// * @author: Ghb-boot +// */ +//public interface NgAlainService { +// /** +// * 菜单 +// * @param id +// * @return JSONArray +// * @throws Exception +// */ +// public JSONArray getMenu(String id) throws Exception; +// +// /** +// * Ghb菜单 +// * @param id +// * @return JSONArray +// * @throws Exception +// */ +// public JSONArray getGhbMenu(String id) throws Exception; +// +// /** +// * 获取字典值 +// * @param table +// * @param key +// * @param value +// * @return List> +// */ +// public List> getDictByTable(String table, String key, String value); +//} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/service/impl/NgAlainServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/service/impl/NgAlainServiceImpl.java new file mode 100644 index 0000000..927654b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/ngalain/service/impl/NgAlainServiceImpl.java @@ -0,0 +1,187 @@ +//package com.ghb.base.modules.ngalain.service.impl; +// +//import com.alibaba.fastjson.JSONArray; +//import com.alibaba.fastjson.JSONObject; +//import com.ghb.base.common.constant.CommonConstant; +//import com.ghb.base.common.constant.SymbolConstant; +//import com.ghb.base.common.util.oConvertUtils; +//import com.ghb.base.modules.ngalain.service.NgAlainService; +//import com.ghb.base.modules.system.entity.SysPermission; +//import com.ghb.base.modules.system.mapper.SysDictMapper; +//import com.ghb.base.modules.system.service.ISysPermissionService; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.stereotype.Service; +//import org.springframework.transaction.annotation.Transactional; +// +//import java.util.Base64; +//import java.util.List; +//import java.util.Map; +// +///** +// * @Description: NgAlainServiceImpl 实现类 +// * @author: Ghb-boot +// */ +//@Service("ngAlainService") +//public class NgAlainServiceImpl implements NgAlainService { +// @Autowired +// private ISysPermissionService sysPermissionService; +// @Autowired +// private SysDictMapper mapper; +// @Override +// public JSONArray getMenu(String id) throws Exception { +// return getGhbMenu(id); +// } +// @Override +// public JSONArray getGhbMenu(String id) throws Exception { +// List metaList = sysPermissionService.queryByUser(id); +// JSONArray jsonArray = new JSONArray(); +// getPermissionJsonArray(jsonArray, metaList, null); +// JSONArray menulist= parseNgAlain(jsonArray); +// JSONObject GhbMenu = new JSONObject(); +// GhbMenu.put("text", "Ghb菜单"); +// GhbMenu.put("group",true); +// GhbMenu.put("children", menulist); +// JSONArray GhbMenuList=new JSONArray(); +// GhbMenuList.add(GhbMenu); +// return GhbMenuList; +// } +// +// @Override +// public List> getDictByTable(String table, String key, String value) { +// return this.mapper.getDictByTableNgAlain(table,key,value); +// } +// +// private JSONArray parseNgAlain(JSONArray jsonArray) { +// JSONArray menulist=new JSONArray(); +// for (Object object : jsonArray) { +// JSONObject jsonObject= (JSONObject) object; +// String path= (String) jsonObject.get("path"); +// JSONObject meta= (JSONObject) jsonObject.get("meta"); +// JSONObject menu=new JSONObject(); +// menu.put("text",meta.get("title")); +// menu.put("reuse",true); +// if (jsonObject.get("children")!=null){ +// JSONArray child= parseNgAlain((JSONArray) jsonObject.get("children")); +// menu.put("children",child); +// JSONObject icon=new JSONObject(); +// icon.put("type", "icon"); +// icon.put("value", meta.get("icon")); +// menu.put("icon",icon); +// }else { +// menu.put("link",path); +// } +// menulist.add(menu); +// } +// return menulist; +// } +// +// /** +// * 获取菜单JSON数组 +// * @param jsonArray +// * @param metaList +// * @param parentJson +// */ +// private void getPermissionJsonArray(JSONArray jsonArray,List metaList,JSONObject parentJson) { +// for (SysPermission permission : metaList) { +// if(permission.getMenuType()==null) { +// continue; +// } +// String tempPid = permission.getParentId(); +// JSONObject json = getPermissionJsonObject(permission); +// if(parentJson==null && oConvertUtils.isEmpty(tempPid)) { +// jsonArray.add(json); +// if(!permission.isLeaf()) { +// getPermissionJsonArray(jsonArray, metaList, json); +// } +// }else if(parentJson!=null && oConvertUtils.isNotEmpty(tempPid) && tempPid.equals(parentJson.getString("id"))){ +// if(permission.getMenuType()==0) { +// JSONObject metaJson = parentJson.getJSONObject("meta"); +// if(metaJson.containsKey("permissionList")) { +// metaJson.getJSONArray("permissionList").add(json); +// }else { +// JSONArray permissionList = new JSONArray(); +// permissionList.add(json); +// metaJson.put("permissionList", permissionList); +// } +// +// }else if(permission.getMenuType()==1) { +// if(parentJson.containsKey("children")) { +// parentJson.getJSONArray("children").add(json); +// }else { +// JSONArray children = new JSONArray(); +// children.add(json); +// parentJson.put("children", children); +// } +// +// if(!permission.isLeaf()) { +// getPermissionJsonArray(jsonArray, metaList, json); +// } +// } +// } +// +// +// } +// } +// private JSONObject getPermissionJsonObject(SysPermission permission) { +// JSONObject json = new JSONObject(); +// //类型(0:一级菜单 1:子菜单 2:按钮) +// if(CommonConstant.MENU_TYPE_2.equals(permission.getMenuType())) { +// json.put("action", permission.getPerms()); +// json.put("describe", permission.getName()); +// }else if(CommonConstant.MENU_TYPE_0.equals(permission.getMenuType()) || CommonConstant.MENU_TYPE_1.equals(permission.getMenuType())) { +// json.put("id", permission.getId()); +// boolean flag = permission.getUrl()!=null&&(permission.getUrl().startsWith(CommonConstant.HTTP_PROTOCOL)||permission.getUrl().startsWith(CommonConstant.HTTPS_PROTOCOL)); +// if(flag) { +// String url= new String(Base64.getUrlEncoder().encode(permission.getUrl().getBytes())); +// json.put("path", "/sys/link/" +url.replaceAll("=","")); +// }else { +// json.put("path", permission.getUrl()); +// } +// +// //重要规则:路由name (通过URL生成路由name,路由name供前端开发,页面跳转使用) +// json.put("name", urlToRouteName(permission.getUrl())); +// +// //是否隐藏路由,默认都是显示的 +// if(permission.isHidden()) { +// json.put("hidden",true); +// } +// //聚合路由 +// if(permission.isAlwaysShow()) { +// json.put("alwaysShow",true); +// } +// json.put("component", permission.getComponent()); +// JSONObject meta = new JSONObject(); +// meta.put("title", permission.getName()); +// if(oConvertUtils.isEmpty(permission.getParentId())) { +// //一级菜单跳转地址 +// json.put("redirect",permission.getRedirect()); +// meta.put("icon", oConvertUtils.getString(permission.getIcon(), "")); +// }else { +// meta.put("icon", oConvertUtils.getString(permission.getIcon(), "")); +// } +// if(flag) { +// meta.put("url", permission.getUrl()); +// } +// json.put("meta", meta); +// } +// +// return json; +// } +// /** +// * 通过URL生成路由name(去掉URL前缀斜杠,替换内容中的斜杠‘/’为-) +// * 举例: URL = /isystem/role +// * RouteName = isystem-role +// * @return +// */ +// private String urlToRouteName(String url) { +// if(oConvertUtils.isNotEmpty(url)) { +// if(url.startsWith(SymbolConstant.SINGLE_SLASH)) { +// url = url.substring(1); +// } +// url = url.replace("/", "-"); +// return url; +// }else { +// return null; +// } +// } +//} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiAuthController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiAuthController.java new file mode 100644 index 0000000..8cef71a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiAuthController.java @@ -0,0 +1,112 @@ +package com.ghb.base.modules.openapi.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.openapi.entity.OpenApiAuth; +import com.ghb.base.modules.openapi.generator.AKSKGenerator; +import com.ghb.base.modules.openapi.service.OpenApiAuthService; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; + +/** + * @date 2024/12/10 9:54 + */ +@RestController +@RequestMapping("/openapi/auth") +public class OpenApiAuthController extends GhbController { + + /** + * 分页列表查询 + * + * @param openApiAuth + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/list") + public Result queryPageList(OpenApiAuth openApiAuth, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(openApiAuth, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = service.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 添加 + * + * @param openApiAuth + * @return + */ + @PostMapping(value = "/add") + public Result add(@RequestBody OpenApiAuth openApiAuth) { + service.save(openApiAuth); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param openApiAuth + * @return + */ + @PutMapping(value = "/edit") + public Result edit(@RequestBody OpenApiAuth openApiAuth) { + service.updateById(openApiAuth); + return Result.ok("修改成功!"); + + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + service.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + + this.service.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + OpenApiAuth openApiAuth = service.getById(id); + return Result.ok(openApiAuth); + } + + /** + * 生成AKSK + * @return + */ + @GetMapping("genAKSK") + public Result genAKSK() { + return Result.ok(AKSKGenerator.genAKSKPair()); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiController.java new file mode 100644 index 0000000..cc6a97d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiController.java @@ -0,0 +1,487 @@ +package com.ghb.base.modules.openapi.controller; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.Lists; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.exception.GhbBootBizTipException; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.util.CommonUtils; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.openapi.entity.OpenApi; +import com.ghb.base.modules.openapi.entity.OpenApiAuth; +import com.ghb.base.modules.openapi.entity.OpenApiHeader; +import com.ghb.base.modules.openapi.entity.OpenApiParam; +import com.ghb.base.modules.openapi.generator.PathGenerator; +import com.ghb.base.modules.openapi.service.OpenApiAuthService; +import com.ghb.base.modules.openapi.service.OpenApiService; +import com.ghb.base.modules.openapi.swagger.*; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.service.ISysUserService; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.URI; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @date 2024/12/10 9:11 + */ +@RestController +@RequestMapping("/openapi") +public class OpenApiController extends GhbController { + + @Autowired + private RestTemplate restTemplate; + @Autowired + private RedisUtil redisUtil; + @Autowired + private ISysUserService sysUserService; + @Autowired + private OpenApiAuthService openApiAuthService; + + /** + * 分页列表查询 + * + * @param openApi + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/list") + public Result queryPageList(OpenApi openApi, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(openApi, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = service.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 添加 + * + * @param openApi + * @return + */ + @RequiresRoles({"admin"}) + @PostMapping(value = "/add") + public Result add(@RequestBody OpenApi openApi) { + if (openApi == null) { + return Result.error("请求参数不能为空"); + } + validOriginUrl(openApi.getOriginUrl()); + service.save(openApi); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param openApi + * @return + */ + @RequiresRoles({"admin"}) + @PutMapping(value = "/edit") + public Result edit(@RequestBody OpenApi openApi) { + if (openApi == null) { + return Result.error("请求参数不能为空"); + } + validOriginUrl(openApi.getOriginUrl()); + service.updateById(openApi); + return Result.ok("修改成功!"); + + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @RequiresRoles({"admin"}) + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + service.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @RequiresRoles({"admin"}) + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + + this.service.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + OpenApi OpenApi = service.getById(id); + return Result.ok(OpenApi); + } + + /** + * 接口调用 + * @param path + * @return + */ + @RequestMapping(value = "/call/{path}", method = {RequestMethod.GET,RequestMethod.POST}) + public Result call(@PathVariable String path, @RequestBody(required = false) String json, HttpServletRequest request) { + OpenApi openApi = service.findByPath(path); + if (Objects.isNull(openApi)) { + Map result = new HashMap<>(); + result.put("code", 404); + result.put("data", null); + return Result.error("失败", result); + } + HttpHeaders httpHeaders = new HttpHeaders(); + if (StrUtil.isNotEmpty(openApi.getHeadersJson())) { + List headers = JSON.parseArray(openApi.getHeadersJson(),OpenApiHeader.class); + if (headers.size()>0) { + for (OpenApiHeader header : headers) { + httpHeaders.put(header.getHeaderKey(), Lists.newArrayList(request.getHeader(header.getHeaderKey()))); + } + } + } + + String url = openApi.getOriginUrl(); + // 校验原始接口路径是否合法 + validOriginUrl(url); + String method = openApi.getRequestMethod(); + String appkey = request.getHeader("appkey"); + OpenApiAuth openApiAuth = openApiAuthService.getByAppkey(appkey); + SysUser systemUser = sysUserService.getUserByName(openApiAuth.getCreateBy()); + String token = this.getToken(systemUser.getUsername(), systemUser.getPassword()); + httpHeaders.put("X-Access-Token", Lists.newArrayList(token)); + httpHeaders.put("Content-Type",Lists.newArrayList("application/json")); + HttpEntity httpEntity = new HttpEntity<>(json, httpHeaders); + //update-begin---author:scott ---date:20260429 for:【issues/9590】微服务nginx部署openApi接口访问不到----------- + // originUrl 支持两种形式: + // 1) 相对路径(如 /house/houseTest/list):拼接当前请求的 baseUrl; + // 使用 CommonUtils.getBaseUrl(request)(而非 RestUtil.getBaseUrl()), + // 可读取 X-Gateway-Base-Path 请求头,兼容微服务网关下的真实 base path + // 2) 完整URL(http(s)://host:port/path):直接使用,适用于微服务模式下接口部署在其他微服务模块(如 erp 7003)的场景 + String lowerUrl = url.toLowerCase(); + if (!lowerUrl.startsWith("http://") && !lowerUrl.startsWith("https://")) { + url = CommonUtils.getBaseUrl(request) + url; + } + //update-end---author:scott ---date:20260429 for:【issues/9590】微服务nginx部署openApi接口访问不到----------- + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url); + if (HttpMethod.GET.matches(method) + || HttpMethod.DELETE.matches(method) + || HttpMethod.OPTIONS.matches(method) + || HttpMethod.TRACE.matches(method)) { + //拼接参数 + if (!request.getParameterMap().isEmpty()) { + if (StrUtil.isNotEmpty(openApi.getParamsJson())) { + List params = JSON.parseArray(openApi.getParamsJson(),OpenApiParam.class); + if (params.size()>0) { + Map openApiParamMap = params.stream().collect(Collectors.toMap(p -> p.getParamKey(), p -> p, (e, r) -> e)); + request.getParameterMap().forEach((k, v) -> { + OpenApiParam openApiParam = openApiParamMap.get(k); + if (Objects.nonNull(openApiParam)) { + if(v==null&&StrUtil.isNotEmpty(openApiParam.getDefaultValue())){ + builder.queryParam(openApiParam.getParamKey(), openApiParam.getDefaultValue()); + } + if (v!=null){ + builder.queryParam(openApiParam.getParamKey(), v); + } + } + }); + } + } + + } + } + URI targetUrl = builder.build().encode().toUri(); + return restTemplate.exchange(targetUrl.toString(), Objects.requireNonNull(HttpMethod.valueOf(method)), httpEntity, Result.class, request.getParameterMap()).getBody(); + } + + /** + * 生成接口访问令牌 Token + * + * @param USERNAME + * @param PASSWORD + * @return + */ + private String getToken(String USERNAME, String PASSWORD) { + String token = JwtUtil.sign(USERNAME, PASSWORD, CommonConstant.CLIENT_TYPE_PC); + redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); + redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, 60); + return token; + } + + /** + * 校验原始接口路径是否合法: + * - 相对路径:必须以 / 开头,不允许 // 和 .. 防止路径穿越 + * - 完整URL:仅允许 http/https 协议,禁止 file/ftp/gopher/jar/netdoc 等其它协议(用于微服务模式跨模块调用) + */ + private void validOriginUrl(String originUrl) { + if (oConvertUtils.isEmpty(originUrl)) { + throw new GhbBootBizTipException("原始接口路径不能为空"); + } + String decoded; + try { + decoded = java.net.URLDecoder.decode(originUrl, "UTF-8"); + // 二次解码,防止 %252f 这类双重编码绕过 + decoded = java.net.URLDecoder.decode(decoded, "UTF-8"); + } catch (Exception e) { + throw new GhbBootBizTipException("原始接口路径包含非法字符"); + } + //update-begin---author:scott ---date:20260429 for:【issues/9590】微服务nginx部署openApi接口访问不到----------- + // 微服务部署时,OpenAPI 配置的接口可能位于其他微服务模块(如 erp 7003),允许 originUrl 直接配置完整 http(s) URL + String lower = decoded.toLowerCase(); + boolean isFullHttpUrl = lower.startsWith("http://") || lower.startsWith("https://"); + if (!isFullHttpUrl) { + if (!decoded.startsWith("/")) { + throw new GhbBootBizTipException("原始接口路径必须以 / 开头,或填写完整的 http(s) URL"); + } + if (decoded.startsWith("//") || decoded.startsWith("/\\")) { + throw new GhbBootBizTipException("原始接口路径不能以 // 或 /\\ 开头"); + } + if (lower.contains("://") || lower.startsWith("file:") || lower.startsWith("ftp:") || lower.startsWith("gopher:") + || lower.startsWith("jar:") || lower.startsWith("netdoc:")) { + throw new GhbBootBizTipException("原始接口路径仅支持相对路径或 http(s) 完整URL"); + } + } else { + // 即便是完整URL,也禁止其它危险协议(防止 http://x@file:/... 之类的绕过场景) + String afterScheme = lower.substring(lower.indexOf("://") + 3); + if (afterScheme.contains("file:") || afterScheme.contains("ftp:") || afterScheme.contains("gopher:") + || afterScheme.contains("jar:") || afterScheme.contains("netdoc:")) { + throw new GhbBootBizTipException("原始接口路径不允许嵌套 file/ftp/gopher/jar/netdoc 等协议"); + } + } + if (decoded.contains("..")) { + throw new GhbBootBizTipException("原始接口路径不能包含 .."); + } + //update-end---author:scott ---date:20260429 for:【issues/9590】微服务nginx部署openApi接口访问不到----------- + } + + @GetMapping("/json") + public SwaggerModel swaggerModel() { + + SwaggerModel swaggerModel = new SwaggerModel(); + swaggerModel.setSwagger("2.0"); + swaggerModel.setInfo(swaggerInfo()); + swaggerModel.setHost("Ghb.com"); + swaggerModel.setBasePath("/Ghb-boot"); + swaggerModel.setSchemes(Lists.newArrayList("http", "https")); + + SwaggerTag swaggerTag = new SwaggerTag(); + swaggerTag.setName("openapi"); + swaggerModel.setTags(Lists.newArrayList(swaggerTag)); + + pathsAndDefinitions(swaggerModel); + + return swaggerModel; + } + + private void pathsAndDefinitions(SwaggerModel swaggerModel) { + Map> paths = new HashMap<>(); + Map definitions = new HashMap<>(); + List openapis = service.list(); + for (OpenApi openApi : openapis) { + Map operations = new HashMap<>(); + SwaggerOperation operation = new SwaggerOperation(); + operation.setTags(Lists.newArrayList("openapi")); + operation.setSummary(openApi.getName()); + operation.setDescription(openApi.getName()); + operation.setOperationId(openApi.getRequestUrl()+"Using"+openApi.getRequestMethod()); + operation.setProduces(Lists.newArrayList("application/json")); + parameters(operation, openApi); + + // body入参 + if (StringUtils.hasText(openApi.getBody())) { + SwaggerDefinition definition = new SwaggerDefinition(); + definition.setType("object"); + Map definitionProperties = new HashMap<>(); + definition.setProperties(definitionProperties); + if (openApi.getBody()!=null){ + JSONObject jsonObject = JSONObject.parseObject(openApi.getBody()); + if (jsonObject.size()>0){ + for (Map.Entry properties : jsonObject.entrySet()) { + SwaggerDefinitionProperties swaggerDefinitionProperties = new SwaggerDefinitionProperties(); + swaggerDefinitionProperties.setType("string"); + swaggerDefinitionProperties.setDescription(properties.getValue()+""); + definitionProperties.put(properties.getKey(), swaggerDefinitionProperties); + } + } + } + // body的definition构建完成 + definitions.put(openApi.getRequestUrl()+"Using"+openApi.getRequestMethod()+"body", definition); + + SwaggerOperationParameter bodyParameter = new SwaggerOperationParameter(); + bodyParameter.setDescription(openApi.getName() + " body"); + bodyParameter.setIn("body"); + bodyParameter.setName(openApi.getName() + " body"); + bodyParameter.setRequired(true); + + Map bodySchema = new HashMap<>(); + bodySchema.put("$ref", "#/definitions/" + openApi.getRequestUrl()+"Using"+openApi.getRequestMethod()+"body"); + bodyParameter.setSchema(bodySchema); + + // 构建参数构建完成 + operation.getParameters().add(bodyParameter); + + } + + // 响应 + Map responses = new HashMap<>(); + SwaggerOperationResponse resp200 = new SwaggerOperationResponse(); + resp200.setDescription("OK"); + Map respSchema = new HashMap<>(); + respSchema.put("$ref", "#/definitions/OpenApiResult"); + resp200.setSchema(respSchema); + + responses.put("200", resp200); + + Map emptySchema = new HashMap<>(); + SwaggerOperationResponse resp201 = new SwaggerOperationResponse(); + resp201.setDescription("Created"); + resp201.setSchema(emptySchema); + responses.put("201", resp201); + SwaggerOperationResponse resp401 = new SwaggerOperationResponse(); + resp401.setDescription("Unauthorized"); + resp401.setSchema(emptySchema); + responses.put("401", resp401); + SwaggerOperationResponse resp403 = new SwaggerOperationResponse(); + resp403.setDescription("Forbidden"); + resp403.setSchema(emptySchema); + responses.put("403", resp403); + SwaggerOperationResponse resp404 = new SwaggerOperationResponse(); + resp404.setDescription("Not Found"); + resp404.setSchema(emptySchema); + responses.put("404", resp404); + + // 构建响应definition + SwaggerDefinition respDefinition = new SwaggerDefinition(); + respDefinition.setType("object"); + + Map definitionProperties = new HashMap<>(); + respDefinition.setProperties(definitionProperties); + + SwaggerDefinitionProperties codeProperties = new SwaggerDefinitionProperties(); + codeProperties.setType("integer"); + codeProperties.setDescription("返回代码"); + definitionProperties.put("code", codeProperties); + SwaggerDefinitionProperties messageProperties = new SwaggerDefinitionProperties(); + messageProperties.setType("string"); + messageProperties.setDescription("返回处理消息"); + definitionProperties.put("message", messageProperties); + SwaggerDefinitionProperties resultProperties = new SwaggerDefinitionProperties(); + resultProperties.setType("object"); + resultProperties.setDescription("返回数据对象"); + definitionProperties.put("result", resultProperties); + SwaggerDefinitionProperties successProperties = new SwaggerDefinitionProperties(); + successProperties.setType("boolean"); + successProperties.setDescription("成功标志"); + definitionProperties.put("success", successProperties); + SwaggerDefinitionProperties timestampProperties = new SwaggerDefinitionProperties(); + timestampProperties.setType("integer"); + timestampProperties.setDescription("时间戳"); + definitionProperties.put("timestamp", timestampProperties); + + definitions.put("OpenApiResult", respDefinition); + + + operation.setResponses(responses); + operations.put(openApi.getRequestMethod().toLowerCase(), operation); + paths.put("/openapi/call/"+openApi.getRequestUrl(), operations); + } + + swaggerModel.setDefinitions(definitions); + swaggerModel.setPaths(paths); + + } + + private void parameters(SwaggerOperation operation, OpenApi openApi) { + List parameters = new ArrayList<>(); + if (openApi.getParamsJson()!=null) { + List openApiParams = JSON.parseArray(openApi.getParamsJson(), OpenApiParam.class); + for (OpenApiParam openApiParam : openApiParams) { + SwaggerOperationParameter parameter = new SwaggerOperationParameter(); + parameter.setIn("path"); + parameter.setName(openApiParam.getParamKey()); + parameter.setRequired(openApiParam.getRequired() == 1); + parameter.setDescription(openApiParam.getNote()); + parameters.add(parameter); + } + } + if (openApi.getHeadersJson()!=null) { + List openApiHeaders = JSON.parseArray(openApi.getHeadersJson(), OpenApiHeader.class); + for (OpenApiHeader openApiHeader : openApiHeaders) { + SwaggerOperationParameter parameter = new SwaggerOperationParameter(); + parameter.setIn("header"); + parameter.setName(openApiHeader.getHeaderKey()); + parameter.setRequired(openApiHeader.getRequired() == 1); + parameter.setDescription(openApiHeader.getNote()); + parameters.add(parameter); + } + } + operation.setParameters(parameters); + } + + private SwaggerInfo swaggerInfo() { + SwaggerInfo info = new SwaggerInfo(); + + info.setDescription("OpenAPI 接口列表"); + info.setVersion("3.9.2"); + info.setTitle("OpenAPI 接口列表"); + info.setTermsOfService("https://Ghb.com"); + + SwaggerInfoContact contact = new SwaggerInfoContact(); + contact.setName("Ghb@qq.com"); + + info.setContact(contact); + + SwaggerInfoLicense license = new SwaggerInfoLicense(); + license.setName("Apache 2.0"); + license.setUrl("http://www.apache.org/licenses/LICENSE-2.0.html"); + + info.setLicense(license); + + return info; + } + + /** + * 生成接口路径 + * @return + */ + @GetMapping("genPath") + public Result genPath() { + Result r = new Result(); + r.setSuccess(true); + r.setCode(CommonConstant.SC_OK_200); + r.setResult(PathGenerator.genPath()); + return r; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiIndexController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiIndexController.java new file mode 100644 index 0000000..a1a7820 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiIndexController.java @@ -0,0 +1,26 @@ +package com.ghb.base.modules.openapi.controller; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.config.shiro.IgnoreAuth; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * @date 2024/12/20 14:04 + */ +@RestController +@RequestMapping("/openapi/demo") +public class OpenApiIndexController { + + @GetMapping("index") + @IgnoreAuth + public Result> index() { + Map result = new HashMap<>(); + result.put("first", "Hello World"); + return Result.ok(result); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiLogController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiLogController.java new file mode 100644 index 0000000..c02dd4d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiLogController.java @@ -0,0 +1,102 @@ +package com.ghb.base.modules.openapi.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.openapi.entity.OpenApiLog; +import com.ghb.base.modules.openapi.service.OpenApiLogService; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; + +/** + * @date 2024/12/10 9:57 + */ +@RestController +@RequestMapping("/openapi/record") +public class OpenApiLogController extends GhbController { + + /** + * 分页列表查询 + * + * @param OpenApiLog + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/list") + public Result queryPageList(OpenApiLog OpenApiLog, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(OpenApiLog, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = service.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 添加 + * + * @param OpenApiLog + * @return + */ + @PostMapping(value = "/add") + public Result add(@RequestBody OpenApiLog OpenApiLog) { + service.save(OpenApiLog); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param OpenApiLog + * @return + */ + @PutMapping(value = "/edit") + public Result edit(@RequestBody OpenApiLog OpenApiLog) { + service.updateById(OpenApiLog); + return Result.ok("修改成功!"); + + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + service.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + + this.service.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + OpenApiLog OpenApiLog = service.getById(id); + return Result.ok(OpenApiLog); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiPermissionController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiPermissionController.java new file mode 100644 index 0000000..36b59a9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/controller/OpenApiPermissionController.java @@ -0,0 +1,22 @@ +package com.ghb.base.modules.openapi.controller; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.modules.openapi.entity.OpenApiPermission; +import com.ghb.base.modules.openapi.service.OpenApiPermissionService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/openapi/permission") +public class OpenApiPermissionController extends GhbController { + + @PostMapping("add") + public Result add(@RequestBody OpenApiPermission openApiPermission) { + service.add(openApiPermission); + return Result.ok("保存成功"); + } + @GetMapping("/getOpenApi") + public Result getOpenApi( String apiAuthId) { + return service.getOpenApi(apiAuthId); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApi.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApi.java new file mode 100644 index 0000000..de5e45e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApi.java @@ -0,0 +1,111 @@ +package com.ghb.base.modules.openapi.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +/** + * 接口表 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class OpenApi implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 接口名称 + */ + private String name; + + /** + * 请求方式,如POST、GET + */ + private String requestMethod; + + /** + * 对外开放的相对接口路径 + */ + private String requestUrl; + + /** + * IP 白名单 + */ + private String whiteList; + + //update-begin---author:scott ---date:20260417 for:【PR/9083】OpenAPI新增白名单备注字段----------- + /** + * 白名单备注说明 + */ + private String comment; + //update-end---author:scott ---date:20260417 for:【PR/9083】OpenAPI新增白名单备注字段----------- + /** + * 请求头json + */ + private String headersJson; + /** + * 请求参数json + */ + private String paramsJson; + + + /** + * 目前仅支持json + */ + private String body; + + /** + * 原始接口路径 + */ + private String originUrl; + + /** + * 状态(1:正常 2:废弃 ) + */ + private Integer status; + + /** + * 删除状态(0,正常,1已删除) + */ + @TableLogic + private Integer delFlag; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + /** + * 历史已选接口 + */ + @TableField(exist = false) + private String ifCheckBox = "0"; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiAuth.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiAuth.java new file mode 100644 index 0000000..9b978cd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiAuth.java @@ -0,0 +1,70 @@ +package com.ghb.base.modules.openapi.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; + +import java.io.Serializable; +import java.util.Date; + +/** + * 权限表 + * @date 2024/12/10 9:38 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class OpenApiAuth implements Serializable { + + private static final long serialVersionUID = -5933153354153738498L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 受权名称 + */ + private String name; + + /** + * access key + */ + private String ak; + + /** + * secret key + */ + private String sk; + + /** + * 系统用户ID + */ + @Dict(dictTable = "sys_user",dicCode = "id",dicText = "username") + private String systemUserId; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiHeader.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiHeader.java new file mode 100644 index 0000000..ac9ada2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiHeader.java @@ -0,0 +1,39 @@ +package com.ghb.base.modules.openapi.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 请求头表 + * @date 2024/12/10 14:37 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class OpenApiHeader implements Serializable { + private static final long serialVersionUID = 5032708503120184683L; + + + /** + * key + */ + private String headerKey; + + /** + * 是否必填(0:否,1:是) + */ + private Integer required; + + /** + * 默认值 + */ + private String defaultValue; + + /** + * 说明 + */ + private String note; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiLog.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiLog.java new file mode 100644 index 0000000..175376a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiLog.java @@ -0,0 +1,52 @@ +package com.ghb.base.modules.openapi.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +/** + * 调用记录表 + * @date 2024/12/10 9:41 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class OpenApiLog implements Serializable { + private static final long serialVersionUID = -5870384488947863579L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 接口ID + */ + private String apiId; + + /** + * 调用ID + */ + private String callAuthId; + + /** + * 调用时间 + */ + private Date callTime; + + /** + * 耗时 + */ + private Long usedTime; + + /** + * 响应时间 + */ + private Date responseTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiParam.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiParam.java new file mode 100644 index 0000000..716ae98 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiParam.java @@ -0,0 +1,38 @@ +package com.ghb.base.modules.openapi.entity; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * query部分参数表 + * @date 2024/12/10 14:37 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class OpenApiParam implements Serializable { + private static final long serialVersionUID = -6174831468578022357L; + + /** + * key + */ + private String paramKey; + + /** + * 是否必填(0:否,1:是) + */ + private Integer required; + + /** + * 默认值 + */ + private String defaultValue; + + /** + * 说明 + */ + private String note; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiPermission.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiPermission.java new file mode 100644 index 0000000..c22b50e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/entity/OpenApiPermission.java @@ -0,0 +1,55 @@ +package com.ghb.base.modules.openapi.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; + +/** + * + * @date 2024/12/19 17:41 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class OpenApiPermission implements Serializable { + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 接口ID + */ + private String apiId; + + /** + * 认证ID + */ + private String apiAuthId; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/filter/ApiAuthFilter.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/filter/ApiAuthFilter.java new file mode 100644 index 0000000..9a08b7b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/filter/ApiAuthFilter.java @@ -0,0 +1,292 @@ +package com.ghb.base.modules.openapi.filter; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.IpUtils; +import com.ghb.base.modules.openapi.entity.OpenApi; +import com.ghb.base.modules.openapi.entity.OpenApiAuth; +import com.ghb.base.modules.openapi.entity.OpenApiLog; +import com.ghb.base.modules.openapi.entity.OpenApiPermission; +import com.ghb.base.modules.openapi.service.OpenApiAuthService; +import com.ghb.base.modules.openapi.service.OpenApiLogService; +import com.ghb.base.modules.openapi.service.OpenApiPermissionService; +import com.ghb.base.modules.openapi.service.OpenApiService; +import org.springframework.util.StringUtils; +import org.springframework.web.context.WebApplicationContext; + +import java.io.IOException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @date 2024/12/19 16:55 + */ +@Slf4j +public class ApiAuthFilter implements Filter { + + private OpenApiLogService openApiLogService; + private OpenApiAuthService openApiAuthService; + private OpenApiPermissionService openApiPermissionService; + private OpenApiService openApiService; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + long startTime = System.currentTimeMillis(); + Date callTime = new Date(); + + HttpServletRequest request = (HttpServletRequest)servletRequest; + String ip = IpUtils.getIpAddr(request); + + String appkey = request.getHeader("appkey"); + String signature = request.getHeader("signature"); + String timestamp = request.getHeader("timestamp"); + + OpenApi openApi = findOpenApi(request); + + // IP 白名单核验 + checkWhiteList(openApi, ip); + + // 签名核验 + checkSignValid(appkey, signature, timestamp); + + OpenApiAuth openApiAuth = openApiAuthService.getByAppkey(appkey); + // 认证信息核验 + checkSignature(appkey, signature, timestamp, openApiAuth); + // 业务核验 + checkPermission(openApi, openApiAuth); + + filterChain.doFilter(servletRequest, servletResponse); + long endTime = System.currentTimeMillis(); + + OpenApiLog openApiLog = new OpenApiLog(); + openApiLog.setApiId(openApi.getId()); + openApiLog.setCallAuthId(openApiAuth.getId()); + openApiLog.setCallTime(callTime); + openApiLog.setUsedTime(endTime - startTime); + openApiLog.setResponseTime(new Date()); + openApiLogService.save(openApiLog); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + ServletContext servletContext = filterConfig.getServletContext(); + WebApplicationContext applicationContext = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + this.openApiService = applicationContext.getBean(OpenApiService.class); + this.openApiLogService = applicationContext.getBean(OpenApiLogService.class); + this.openApiAuthService = applicationContext.getBean(OpenApiAuthService.class); + this.openApiPermissionService = applicationContext.getBean(OpenApiPermissionService.class); + } + + //update-begin---author:scott ---date:20260416 for:【PR/9083】OpenAPI白名单增强,支持CIDR网段和通配符匹配----------- + /** + * IP 白名单核验,支持精确IP、CIDR网段(如192.168.1.0/24)、通配符(如10.2.3.*) + * @param openApi + * @param ip + */ + protected void checkWhiteList(OpenApi openApi, String ip) { + if (!StringUtils.hasText(openApi.getWhiteList())) { + return; + } + + List whiteList = Arrays.stream(openApi.getWhiteList().split("[,\\n]")) + .map(String::trim) + .filter(StringUtils::hasText) + .collect(Collectors.toList()); + + for (String item : whiteList) { + if (isIpMatch(ip, item)) { + return; + } + } + throw new GhbBootException("IP[" + ip + "]不在白名单中,禁止访问"); + } + + /** + * IP匹配:支持精确匹配、CIDR网段匹配、通配符匹配 + * @param ip 客户端IP + * @param pattern 白名单条目(IP/CIDR/通配符) + * @return 是否匹配 + */ + private boolean isIpMatch(String ip, String pattern) { + if (!ip.contains(".") || !pattern.contains(".")) { + return ip.equals(pattern); + } + if (pattern.contains("/")) { + return isCidrMatch(ip, pattern); + } + if (pattern.contains("*")) { + return isWildcardMatch(ip, pattern); + } + return ip.equals(pattern); + } + + /** + * CIDR网段匹配(仅IPv4),如 192.168.1.0/24 + */ + private boolean isCidrMatch(String ip, String cidr) { + String[] parts = cidr.split("/"); + if (parts.length != 2) { + return false; + } + try { + long ipLong = ipToLong(ip); + long cidrLong = ipToLong(parts[0]); + int prefixLength = Integer.parseInt(parts[1]); + if (prefixLength < 0 || prefixLength > 32) { + return false; + } + long mask = prefixLength == 0 ? 0 : (-1L << (32 - prefixLength)); + return (ipLong & mask) == (cidrLong & mask); + } catch (Exception e) { + log.warn("CIDR匹配解析失败: cidr={}, ip={}", cidr, ip); + return false; + } + } + + /** + * 通配符匹配,如 10.2.3.* + */ + private boolean isWildcardMatch(String ip, String pattern) { + String[] ipParts = ip.split("\\."); + String[] patternParts = pattern.split("\\."); + if (ipParts.length != 4 || patternParts.length != 4) { + return false; + } + for (int i = 0; i < 4; i++) { + if ("*".equals(patternParts[i])) { + continue; + } + if (!ipParts[i].equals(patternParts[i])) { + return false; + } + } + return true; + } + + /** + * IPv4地址转long + */ + private long ipToLong(String ip) { + String[] parts = ip.split("\\."); + if (parts.length != 4) { + throw new IllegalArgumentException("非法IPv4地址: " + ip); + } + long result = 0; + for (int i = 0; i < 4; i++) { + result = (result << 8) | (Integer.parseInt(parts[i]) & 0xFF); + } + return result; + } + //update-end---author:scott ---date:20260416 for:【PR/9083】OpenAPI白名单增强,支持CIDR网段和通配符匹配----------- + + /** + * 签名验证 + * @param appkey + * @param signature + * @param timestamp + * @return + */ + protected void checkSignValid(String appkey, String signature, String timestamp) { + if (!StringUtils.hasText(appkey)) { + throw new GhbBootException("appkey为空"); + } + if (!StringUtils.hasText(signature)) { + throw new GhbBootException("signature为空"); + } + if (!StringUtils.hasText(timestamp)) { + throw new GhbBootException("timastamp时间戳为空"); + } + if (!timestamp.matches("[0-9]*")) { + throw new GhbBootException("timastamp时间戳不合法"); + } + if (System.currentTimeMillis() - Long.parseLong(timestamp) > 5 * 60 * 1000) { + throw new GhbBootException("signature签名已过期(超过五分钟)"); + } + } + + /** + * 认证信息核验 + * @param appKey + * @param signature + * @param timestamp + * @param openApiAuth + * @return + * @throws Exception + */ + protected void checkSignature(String appKey, String signature, String timestamp, OpenApiAuth openApiAuth) { + if(openApiAuth==null){ + throw new GhbBootException("不存在认证信息"); + } + + if(!appKey.equals(openApiAuth.getAk())){ + throw new GhbBootException("appkey错误"); + } + + if (!signature.equals(md5(appKey + openApiAuth.getSk() + timestamp))) { + throw new GhbBootException("signature签名错误"); + } + } + + protected void checkPermission(OpenApi openApi, OpenApiAuth openApiAuth) { + List permissionList = openApiPermissionService.findByAuthId(openApiAuth.getId()); + + boolean hasPermission = false; + for (OpenApiPermission permission : permissionList) { + if (permission.getApiId().equals(openApi.getId())) { + hasPermission = true; + break; + } + } + + if (!hasPermission) { + throw new GhbBootException("该appKey未授权当前接口"); + } + } + + /** + * @return String 返回类型 + * @Title: MD5 + * @Description: 【MD5加密】 + */ + protected static String md5(String sourceStr) { + String result = ""; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(sourceStr.getBytes("utf-8")); + byte[] hash = md.digest(); + int i; + StringBuffer buf = new StringBuffer(32); + for (int offset = 0; offset < hash.length; offset++) { + i = hash[offset]; + if (i < 0) { + i += 256; + } + if (i < 16) { + buf.append("0"); + } + buf.append(Integer.toHexString(i)); + } + result = buf.toString(); + } catch (Exception e) { + log.error("sign签名错误", e); + } + return result; + } + + protected OpenApi findOpenApi(HttpServletRequest request) { + String uri = request.getRequestURI(); + String path = uri.substring(uri.lastIndexOf("/") + 1); + return openApiService.findByPath(path); + } + + public static void main(String[] args) { + long timestamp = System.currentTimeMillis(); + System.out.println("timestamp:" + timestamp); + System.out.println("signature:" + md5("ak-eAU25mrMxhtaZsyS" + "rjxMqB6YyUXpSHAz4DCIz8vZ5aozQQiV" + timestamp)); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/filter/ApiFilterConfig.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/filter/ApiFilterConfig.java new file mode 100644 index 0000000..e98ff5f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/filter/ApiFilterConfig.java @@ -0,0 +1,25 @@ +package com.ghb.base.modules.openapi.filter; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @date 2024/12/19 17:09 + */ +@Configuration +public class ApiFilterConfig { + + /** + * + * @Description: 【注册api加密过滤器】 + */ + @Bean + public FilterRegistrationBean authFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new ApiAuthFilter()); + registration.setName("apiAuthFilter"); + registration.addUrlPatterns("/openapi/call/*"); + return registration; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/generator/AKSKGenerator.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/generator/AKSKGenerator.java new file mode 100644 index 0000000..f5b687e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/generator/AKSKGenerator.java @@ -0,0 +1,36 @@ +package com.ghb.base.modules.openapi.generator; + +import java.security.SecureRandom; + +/** + * AK/SK生成器 + */ +public class AKSKGenerator { + private static final String CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static final int AK_LENGTH = 16; // Adjust as per requirements + private static final int SK_LENGTH = 32; + + public static String[] genAKSKPair() { + return new String[]{genAK(), genSK()}; + } + + public static String genAK() { + return "ak-" + generateRandomString(AK_LENGTH); + } + + public static String genSK() { + return generateRandomString(SK_LENGTH); + } + + + private static String generateRandomString(int length) { + SecureRandom random = new SecureRandom(); + StringBuilder sb = new StringBuilder(length); + + for (int i = 0; i < length; i++) { + sb.append(CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length()))); + } + + return sb.toString(); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/generator/PathGenerator.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/generator/PathGenerator.java new file mode 100644 index 0000000..08ca248 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/generator/PathGenerator.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.openapi.generator; + +import lombok.experimental.UtilityClass; + +import java.util.Random; + +/** + * @date 2024/12/10 10:00 + */ +@UtilityClass +public class PathGenerator { + + // Base62字符集 + private static final String BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + /** + * 生成随机路径 + * @return + */ + public static String genPath() { + StringBuilder result = new StringBuilder(); + Random random = new Random(); + for (int i=0; i<8; i++) { + result.append(BASE62.charAt(random.nextInt(62))); + } + return result.toString(); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiAuthMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiAuthMapper.java new file mode 100644 index 0000000..070dbc2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiAuthMapper.java @@ -0,0 +1,12 @@ +package com.ghb.base.modules.openapi.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import com.ghb.base.modules.openapi.entity.OpenApiAuth; + +/** + * @date 2024/12/10 9:49 + */ +@Mapper +public interface OpenApiAuthMapper extends BaseMapper { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiLogMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiLogMapper.java new file mode 100644 index 0000000..efb71b5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiLogMapper.java @@ -0,0 +1,12 @@ +package com.ghb.base.modules.openapi.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import com.ghb.base.modules.openapi.entity.OpenApiLog; + +/** + * @date 2024/12/10 9:50 + */ +@Mapper +public interface OpenApiLogMapper extends BaseMapper { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiMapper.java new file mode 100644 index 0000000..2e2fb10 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiMapper.java @@ -0,0 +1,9 @@ +package com.ghb.base.modules.openapi.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import com.ghb.base.modules.openapi.entity.OpenApi; + +@Mapper +public interface OpenApiMapper extends BaseMapper { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiPermissionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiPermissionMapper.java new file mode 100644 index 0000000..2e7476f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/mapper/OpenApiPermissionMapper.java @@ -0,0 +1,12 @@ +package com.ghb.base.modules.openapi.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import com.ghb.base.modules.openapi.entity.OpenApiPermission; + +/** + * @date 2024/12/19 17:43 + */ +@Mapper +public interface OpenApiPermissionMapper extends BaseMapper { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiAuthService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiAuthService.java new file mode 100644 index 0000000..e968baf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiAuthService.java @@ -0,0 +1,11 @@ +package com.ghb.base.modules.openapi.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.openapi.entity.OpenApiAuth; + +/** + * @date 2024/12/10 9:50 + */ +public interface OpenApiAuthService extends IService { + OpenApiAuth getByAppkey(String appkey); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiLogService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiLogService.java new file mode 100644 index 0000000..1342fd7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiLogService.java @@ -0,0 +1,10 @@ +package com.ghb.base.modules.openapi.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.openapi.entity.OpenApiLog; + +/** + * @date 2024/12/10 9:51 + */ +public interface OpenApiLogService extends IService { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiPermissionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiPermissionService.java new file mode 100644 index 0000000..04c3a9d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiPermissionService.java @@ -0,0 +1,18 @@ +package com.ghb.base.modules.openapi.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.openapi.entity.OpenApiPermission; + +import java.util.List; + +/** + * @date 2024/12/19 17:44 + */ +public interface OpenApiPermissionService extends IService { + List findByAuthId(String authId); + + Result getOpenApi(String apiAuthId); + + void add(OpenApiPermission openApiPermission); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiService.java new file mode 100644 index 0000000..b55e7d6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/OpenApiService.java @@ -0,0 +1,8 @@ +package com.ghb.base.modules.openapi.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.openapi.entity.OpenApi; + +public interface OpenApiService extends IService { + OpenApi findByPath(String path); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiAuthServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiAuthServiceImpl.java new file mode 100644 index 0000000..a938005 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiAuthServiceImpl.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.openapi.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.modules.openapi.entity.OpenApiAuth; +import com.ghb.base.modules.openapi.mapper.OpenApiAuthMapper; +import com.ghb.base.modules.openapi.service.OpenApiAuthService; +import org.springframework.stereotype.Service; + +/** + * @date 2024/12/10 9:51 + */ +@Service +public class OpenApiAuthServiceImpl extends ServiceImpl implements OpenApiAuthService { + @Override + public OpenApiAuth getByAppkey(String appkey) { + return baseMapper.selectOne(Wrappers.lambdaUpdate(OpenApiAuth.class).eq(OpenApiAuth::getAk, appkey), false); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiLogServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiLogServiceImpl.java new file mode 100644 index 0000000..5e9bc96 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiLogServiceImpl.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.openapi.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.modules.openapi.entity.OpenApiLog; +import com.ghb.base.modules.openapi.mapper.OpenApiLogMapper; +import com.ghb.base.modules.openapi.service.OpenApiLogService; +import org.springframework.stereotype.Service; + +/** + * @date 2024/12/10 9:53 + */ +@Service +public class OpenApiLogServiceImpl extends ServiceImpl implements OpenApiLogService { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiPermissionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiPermissionServiceImpl.java new file mode 100644 index 0000000..109b5b6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiPermissionServiceImpl.java @@ -0,0 +1,67 @@ +package com.ghb.base.modules.openapi.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jakarta.annotation.Resource; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.openapi.entity.OpenApi; +import com.ghb.base.modules.openapi.entity.OpenApiPermission; +import com.ghb.base.modules.openapi.mapper.OpenApiPermissionMapper; +import com.ghb.base.modules.openapi.service.OpenApiPermissionService; +import com.ghb.base.modules.openapi.service.OpenApiService; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @date 2024/12/19 17:44 + */ +@Service +public class OpenApiPermissionServiceImpl extends ServiceImpl implements OpenApiPermissionService { + @Resource + private OpenApiService openApiService; + @Override + public List findByAuthId(String authId) { + return baseMapper.selectList(Wrappers.lambdaQuery(OpenApiPermission.class).eq(OpenApiPermission::getApiAuthId, authId)); + } + + @Override + public Result getOpenApi(String apiAuthId) { + List openApis = openApiService.list(); + if (CollectionUtil.isEmpty(openApis)) { + return Result.error("接口不存在"); + } + List openApiPermissions = baseMapper.selectList(Wrappers.lambdaQuery().eq(OpenApiPermission::getApiAuthId, apiAuthId)); + if (CollectionUtil.isNotEmpty(openApiPermissions)) { + Map openApiMap = openApis.stream().collect(Collectors.toMap(OpenApi::getId, o -> o)); + for (OpenApiPermission openApiPermission : openApiPermissions) { + OpenApi openApi = openApiMap.get(openApiPermission.getApiId()); + if (openApi!=null) { + openApi.setIfCheckBox("1"); + } + } + } + return Result.ok(openApis); + } + + @Override + public void add(OpenApiPermission openApiPermission) { + this.remove(Wrappers.lambdaQuery().eq(OpenApiPermission::getApiAuthId, openApiPermission.getApiAuthId())); + List list = Arrays.asList(openApiPermission.getApiId().split(",")); + if (CollectionUtil.isNotEmpty(list)) { + list.forEach(l->{ + if (StrUtil.isNotEmpty(l)){ + OpenApiPermission saveApiPermission = new OpenApiPermission(); + saveApiPermission.setApiId(l); + saveApiPermission.setApiAuthId(openApiPermission.getApiAuthId()); + this.save(saveApiPermission); + } + }); + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiServiceImpl.java new file mode 100644 index 0000000..9625f91 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/service/impl/OpenApiServiceImpl.java @@ -0,0 +1,16 @@ +package com.ghb.base.modules.openapi.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.modules.openapi.entity.OpenApi; +import com.ghb.base.modules.openapi.mapper.OpenApiMapper; +import com.ghb.base.modules.openapi.service.OpenApiService; +import org.springframework.stereotype.Service; + +@Service +public class OpenApiServiceImpl extends ServiceImpl implements OpenApiService { + @Override + public OpenApi findByPath(String path) { + return baseMapper.selectOne(Wrappers.lambdaQuery(OpenApi.class).eq(OpenApi::getRequestUrl, path), false); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerDefinition.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerDefinition.java new file mode 100644 index 0000000..1d730fb --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerDefinition.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +import java.util.Map; + +/** + * @date 2025/1/26 11:17 + */ +@Data +public class SwaggerDefinition { + private String type; + private Map properties; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerDefinitionProperties.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerDefinitionProperties.java new file mode 100644 index 0000000..ae08fd9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerDefinitionProperties.java @@ -0,0 +1,13 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +/** + * @date 2025/1/26 13:54 + */ +@Data +public class SwaggerDefinitionProperties { + private String type; + private String example; + private String description; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfo.java new file mode 100644 index 0000000..b84ddb8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfo.java @@ -0,0 +1,16 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +/** + * @date 2025/1/26 11:05 + */ +@Data +public class SwaggerInfo { + private String description; + private String version; + private String title; + private String termsOfService; + private SwaggerInfoContact contact; + private SwaggerInfoLicense license; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfoContact.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfoContact.java new file mode 100644 index 0000000..250b5a1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfoContact.java @@ -0,0 +1,11 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +/** + * @date 2025/1/26 11:08 + */ +@Data +public class SwaggerInfoContact { + private String name; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfoLicense.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfoLicense.java new file mode 100644 index 0000000..b4b2332 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerInfoLicense.java @@ -0,0 +1,12 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +/** + * @date 2025/1/26 11:09 + */ +@Data +public class SwaggerInfoLicense { + private String name; + private String url; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerModel.java new file mode 100644 index 0000000..8d1508d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerModel.java @@ -0,0 +1,21 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * @date 2025/1/26 11:05 + */ +@Data +public class SwaggerModel { + private String swagger; + private SwaggerInfo info; + private String host; + private String basePath; + private List tags; + private List schemes; + private Map> paths; + private Map definitions; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperation.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperation.java new file mode 100644 index 0000000..8524040 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperation.java @@ -0,0 +1,20 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * @date 2025/1/26 11:16 + */ +@Data +public class SwaggerOperation { + private List tags; + private String summary; + private String description; + private String operationId; + private List produces; + private List parameters; + private Map responses; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperationParameter.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperationParameter.java new file mode 100644 index 0000000..729f792 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperationParameter.java @@ -0,0 +1,17 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +import java.util.Map; + +/** + * @date 2025/1/26 11:43 + */ +@Data +public class SwaggerOperationParameter { + private String name; + private String in; + private String description; + private Boolean required; + private Map schema; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperationResponse.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperationResponse.java new file mode 100644 index 0000000..6f2f746 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerOperationResponse.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +import java.util.Map; + +/** + * @date 2025/1/26 11:47 + */ +@Data +public class SwaggerOperationResponse { + private String description; + private Map schema; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerSchema.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerSchema.java new file mode 100644 index 0000000..c8cc4bc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerSchema.java @@ -0,0 +1,16 @@ +package com.ghb.base.modules.openapi.swagger; + +/** + * @date 2025/1/26 11:51 + */ +public class SwaggerSchema { + private String $ref; + + public String get$ref() { + return $ref; + } + + public void set$ref(String $ref) { + this.$ref = $ref; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerTag.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerTag.java new file mode 100644 index 0000000..f72df4c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/openapi/swagger/SwaggerTag.java @@ -0,0 +1,11 @@ +package com.ghb.base.modules.openapi.swagger; + +import lombok.Data; + +/** + * @date 2025/1/26 11:15 + */ +@Data +public class SwaggerTag { + private String name; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/controller/OssFileController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/controller/OssFileController.java new file mode 100644 index 0000000..7ec70b0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/controller/OssFileController.java @@ -0,0 +1,99 @@ +package com.ghb.base.modules.oss.controller; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.oss.entity.OssFile; +import com.ghb.base.modules.oss.service.IOssFileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + +/** + * 云存储示例 DEMO + * @author: Ghb-boot + */ +@Slf4j +@Controller +@RequestMapping("/sys/oss/file") +public class OssFileController { + + @Autowired + private IOssFileService ossFileService; + + @ResponseBody + @RequiresPermissions("system:ossFile:list") + @GetMapping("/list") + public Result> queryPageList(OssFile file, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + Result> result = new Result<>(); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(file, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = ossFileService.page(page, queryWrapper); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + @ResponseBody + @PostMapping("/upload") + //@RequiresRoles("admin") + @RequiresPermissions("system:ossFile:upload") + public Result upload(@RequestParam("file") MultipartFile multipartFile) { + Result result = new Result(); + try { + ossFileService.upload(multipartFile); + result.success("上传成功!"); + } + catch (Exception ex) { + log.info(ex.getMessage(), ex); + result.error500("上传失败"); + } + return result; + } + + @ResponseBody + @RequiresPermissions("system:ossFile:delete") + @DeleteMapping("/delete") + public Result delete(@RequestParam(name = "id") String id) { + Result result = new Result(); + OssFile file = ossFileService.getById(id); + if (file == null) { + result.error500("未找到对应实体"); + }else { + boolean ok = ossFileService.delete(file); + result.success("删除成功!"); + } + return result; + } + + /** + * 通过id查询. + */ + @ResponseBody + @GetMapping("/queryById") + public Result queryById(@RequestParam(name = "id") String id) { + Result result = new Result<>(); + OssFile file = ossFileService.getById(id); + if (file == null) { + result.error500("未找到对应实体"); + } + else { + result.setResult(file); + result.setSuccess(true); + } + return result; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/entity/OssFile.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/entity/OssFile.java new file mode 100644 index 0000000..91810c9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/entity/OssFile.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.oss.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.system.base.entity.GhbEntity; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * @Description: oss云存储实体类 + * @author: Ghb-boot + */ +@Data +@TableName("oss_file") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class OssFile extends GhbEntity { + + private static final long serialVersionUID = 1L; + + @Excel(name = "文件名称") + private String fileName; + + @Excel(name = "文件地址") + private String url; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/mapper/OssFileMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/mapper/OssFileMapper.java new file mode 100644 index 0000000..c718b3e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/mapper/OssFileMapper.java @@ -0,0 +1,12 @@ +package com.ghb.base.modules.oss.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.oss.entity.OssFile; + +/** + * @Description: oss云存储Mapper + * @author: Ghb-boot + */ +public interface OssFileMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/service/IOssFileService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/service/IOssFileService.java new file mode 100644 index 0000000..782e677 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/service/IOssFileService.java @@ -0,0 +1,29 @@ +package com.ghb.base.modules.oss.service; + +import java.io.IOException; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.oss.entity.OssFile; +import org.springframework.web.multipart.MultipartFile; + +/** + * @Description: OOS云存储service接口 + * @author: Ghb-boot + */ +public interface IOssFileService extends IService { + + /** + * oss文件上传 + * @param multipartFile + * @throws IOException + */ + void upload(MultipartFile multipartFile) throws Exception; + + /** + * oss文件删除 + * @param ossFile OSSFile对象 + * @return + */ + boolean delete(OssFile ossFile); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/service/impl/OssFileServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/service/impl/OssFileServiceImpl.java new file mode 100644 index 0000000..bfa615e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/oss/service/impl/OssFileServiceImpl.java @@ -0,0 +1,51 @@ +package com.ghb.base.modules.oss.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.CommonUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.common.util.oss.OssBootUtil; +import com.ghb.base.modules.oss.entity.OssFile; +import com.ghb.base.modules.oss.mapper.OssFileMapper; +import com.ghb.base.modules.oss.service.IOssFileService; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +/** + * @Description: OSS云存储实现类 + * @author: Ghb-boot + */ +@Service("ossFileService") +public class OssFileServiceImpl extends ServiceImpl implements IOssFileService { + + @Override + public void upload(MultipartFile multipartFile) throws Exception { + String fileName = multipartFile.getOriginalFilename(); + fileName = CommonUtils.getFileName(fileName); + OssFile ossFile = new OssFile(); + ossFile.setFileName(fileName); + String url = OssBootUtil.upload(multipartFile,"upload/test"); + if(oConvertUtils.isEmpty(url)){ + throw new GhbBootException("上传文件失败! "); + } + // 返回阿里云原生域名前缀URL + ossFile.setUrl(OssBootUtil.getOriginalUrl(url)); + this.save(ossFile); + } + + @Override + public boolean delete(OssFile ossFile) { + try { + this.removeById(ossFile.getId()); + OssBootUtil.deleteUrl(ossFile.getUrl()); + } + catch (Exception ex) { + log.error(ex.getMessage(),ex); + return false; + } + return true; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/controller/QuartzJobController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/controller/QuartzJobController.java new file mode 100644 index 0000000..4261335 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/controller/QuartzJobController.java @@ -0,0 +1,297 @@ +package com.ghb.base.modules.quartz.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.ImportExcelUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.quartz.entity.QuartzJob; +import com.ghb.base.modules.quartz.service.IQuartzJobService; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @Description: 定时任务在线管理 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version:V1.0 + */ +@RestController +@RequestMapping("/sys/quartzJob") +@Slf4j +@Tag(name = "定时任务接口") +public class QuartzJobController { + @Autowired + private IQuartzJobService quartzJobService; + @Autowired + private Scheduler scheduler; + + /** + * 分页列表查询 + * + * @param quartzJob + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result queryPageList(QuartzJob quartzJob, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(quartzJob, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = quartzJobService.page(page, queryWrapper); + return Result.ok(pageList); + + } + + /** + * 添加定时任务 + * + * @param quartzJob + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:quartzJob:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + public Result add(@RequestBody QuartzJob quartzJob) { + quartzJobService.saveAndScheduleJob(quartzJob); + return Result.ok("创建定时任务成功"); + } + + /** + * 更新定时任务 + * + * @param quartzJob + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:quartzJob:edit") + @RequestMapping(value = "/edit", method ={RequestMethod.PUT, RequestMethod.POST}) + public Result eidt(@RequestBody QuartzJob quartzJob) { + try { + quartzJobService.editAndScheduleJob(quartzJob); + } catch (SchedulerException e) { + log.error(e.getMessage(),e); + return Result.error("更新定时任务失败!"); + } + return Result.ok("更新定时任务成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:quartzJob:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + public Result delete(@RequestParam(name = "id", required = true) String id) { + QuartzJob quartzJob = quartzJobService.getById(id); + if (quartzJob == null) { + return Result.error("未找到对应实体"); + } + quartzJobService.deleteAndStopJob(quartzJob); + return Result.ok("删除成功!"); + + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:quartzJob:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + if (ids == null || "".equals(ids.trim())) { + return Result.error("参数不识别!"); + } + for (String id : Arrays.asList(ids.split(SymbolConstant.COMMA))) { + QuartzJob job = quartzJobService.getById(id); + quartzJobService.deleteAndStopJob(job); + } + return Result.ok("删除定时任务成功!"); + } + + /** + * 暂停定时任务 + * + * @param id + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:quartzJob:pause") + @GetMapping(value = "/pause") + @Operation(summary = "停止定时任务") + public Result pauseJob(@RequestParam(name = "id") String id) { + QuartzJob job = quartzJobService.getById(id); + if (job == null) { + return Result.error("定时任务不存在!"); + } + quartzJobService.pause(job); + return Result.ok("停止定时任务成功"); + } + + /** + * 启动定时任务 + * + * @param id + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:quartzJob:resume") + @GetMapping(value = "/resume") + @Operation(summary = "启动定时任务") + public Result resumeJob(@RequestParam(name = "id") String id) { + QuartzJob job = quartzJobService.getById(id); + if (job == null) { + return Result.error("定时任务不存在!"); + } + quartzJobService.resumeJob(job); + //scheduler.resumeJob(JobKey.jobKey(job.getJobClassName().trim())); + return Result.ok("启动定时任务成功"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @RequestMapping(value = "/queryById", method = RequestMethod.GET) + public Result queryById(@RequestParam(name = "id", required = true) String id) { + QuartzJob quartzJob = quartzJobService.getById(id); + return Result.ok(quartzJob); + } + + /** + * 导出excel + * + * @param request + * @param quartzJob + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, QuartzJob quartzJob) { + // Step.1 组装查询条件 + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(quartzJob, request.getParameterMap()); + // 过滤选中数据 + String selections = request.getParameter("selections"); + if (oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + queryWrapper.in("id",selectionList); + } + // Step.2 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + List pageList = quartzJobService.list(queryWrapper); + // 导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "定时任务列表"); + mv.addObject(NormalExcelConstants.CLASS, QuartzJob.class); + //获取当前登录用户 + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("定时任务列表数据", "导出人:"+user.getRealname(), "导出信息")); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) throws IOException { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + // 错误信息 + List errorMessage = new ArrayList<>(); + int successLines = 0, errorLines = 0; + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List listQuartzJobs = ExcelImportUtil.importExcel(file.getInputStream(), QuartzJob.class, params); + //add-begin-author:taoyan date:20210909 for:导入定时任务,并不会被启动和调度,需要手动点击启动,才会加入调度任务中 #2986 + for(QuartzJob job: listQuartzJobs){ + job.setStatus(CommonConstant.STATUS_DISABLE); + } + List list = ImportExcelUtil.importDateSave(listQuartzJobs, IQuartzJobService.class, errorMessage,CommonConstant.SQL_INDEX_UNIQ_JOB_CLASS_NAME); + //add-end-author:taoyan date:20210909 for:导入定时任务,并不会被启动和调度,需要手动点击启动,才会加入调度任务中 #2986 + errorLines+=list.size(); + successLines+=(listQuartzJobs.size()-errorLines); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("文件导入失败!"); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return ImportExcelUtil.imporReturnRes(errorLines,successLines,errorMessage); + } + + /** + * 立即执行 + * @param id + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:quartzJob:execute") + @GetMapping("/execute") + public Result execute(@RequestParam(name = "id", required = true) String id) { + QuartzJob quartzJob = quartzJobService.getById(id); + if (quartzJob == null) { + return Result.error("未找到对应实体"); + } + try { + quartzJobService.execute(quartzJob); + } catch (Exception e) { + //e.printStackTrace(); + log.info("定时任务 立即执行失败>>"+e.getMessage()); + return Result.error("执行失败!"); + } + return Result.ok("执行成功!"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/entity/QuartzJob.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/entity/QuartzJob.java new file mode 100644 index 0000000..0bf642b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/entity/QuartzJob.java @@ -0,0 +1,62 @@ +package com.ghb.base.modules.quartz.entity; + +import java.io.Serializable; + +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; + +/** + * @Description: 定时任务在线管理 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version: V1.0 + */ +@Data +@TableName("sys_quartz_job") +public class QuartzJob implements Serializable { + private static final long serialVersionUID = 1L; + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + private java.lang.String id; + /**创建人*/ + private java.lang.String createBy; + /**创建时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date createTime; + /**删除状态*/ + private java.lang.Integer delFlag; + /**修改人*/ + private java.lang.String updateBy; + /**修改时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date updateTime; + /**任务类名*/ + @Excel(name="任务类名",width=40) + private java.lang.String jobClassName; + /**cron表达式*/ + @Excel(name="cron表达式",width=30) + private java.lang.String cronExpression; + /**参数*/ + @Excel(name="参数",width=15) + private java.lang.String parameter; + /**描述*/ + @Excel(name="描述",width=40) + private java.lang.String description; + /**状态 0正常 -1停止*/ + @Excel(name="状态",width=15,dicCode="quartz_status") + @Dict(dicCode = "quartz_status") + private java.lang.Integer status; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/AsyncJob.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/AsyncJob.java new file mode 100644 index 0000000..d0bb8bd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/AsyncJob.java @@ -0,0 +1,35 @@ +package com.ghb.base.modules.quartz.job; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.util.DateUtils; +import org.quartz.*; + +/** + * @Description: 同步定时任务测试 + * + * 此处的同步是指 当定时任务的执行时间大于任务的时间间隔时 + * 会等待第一个任务执行完成才会走第二个任务 + * + * + * @author: taoyan + * @date: 2020年06月19日 + */ +@PersistJobDataAfterExecution +@DisallowConcurrentExecution +@Slf4j +public class AsyncJob implements Job { + + @Override + public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + log.info(" --- 同步任务调度开始 --- "); + try { + //此处模拟任务执行时间 5秒 任务表达式配置为每秒执行一次:0/1 * * * * ? * + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + //测试发现 每5秒执行一次 + log.info(" --- 执行完毕,时间:"+DateUtils.now()+"---"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/SampleJob.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/SampleJob.java new file mode 100644 index 0000000..2935a03 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/SampleJob.java @@ -0,0 +1,23 @@ +package com.ghb.base.modules.quartz.job; + +import com.ghb.base.common.util.DateUtils; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import lombok.extern.slf4j.Slf4j; + +/** + * 示例不带参定时任务 + * + * @Author Scott + */ +@Slf4j +public class SampleJob implements Job { + + @Override + public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + log.info(" Job Execution key:"+jobExecutionContext.getJobDetail().getKey()); + log.info(String.format(" Ghb-Boot 普通定时任务 SampleJob ! 时间:" + DateUtils.getTimestamp())); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/SampleParamJob.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/SampleParamJob.java new file mode 100644 index 0000000..d8a17e7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/job/SampleParamJob.java @@ -0,0 +1,32 @@ +package com.ghb.base.modules.quartz.job; + +import com.ghb.base.common.util.DateUtils; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import lombok.extern.slf4j.Slf4j; + +/** + * 示例带参定时任务 + * + * @Author Scott + */ +@Slf4j +public class SampleParamJob implements Job { + + /** + * 若参数变量名修改 QuartzJobController中也需对应修改 + */ + private String parameter; + + public void setParameter(String parameter) { + this.parameter = parameter; + } + + @Override + public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + log.info(" Job Execution key:"+jobExecutionContext.getJobDetail().getKey()); + log.info( String.format("welcome %s! Ghb-Boot 带参数定时任务 SampleParamJob ! 时间:" + DateUtils.now(), this.parameter)); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/mapper/QuartzJobMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/mapper/QuartzJobMapper.java new file mode 100644 index 0000000..030fb05 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/mapper/QuartzJobMapper.java @@ -0,0 +1,25 @@ +package com.ghb.base.modules.quartz.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.quartz.entity.QuartzJob; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 定时任务在线管理 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version: V1.0 + */ +public interface QuartzJobMapper extends BaseMapper { + + /** + * 根据jobClassName查询 + * @param jobClassName 任务类名 + * @return + */ + public List findByJobClassName(@Param("jobClassName") String jobClassName); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/mapper/xml/QuartzJobMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/mapper/xml/QuartzJobMapper.xml new file mode 100644 index 0000000..da07651 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/mapper/xml/QuartzJobMapper.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/service/IQuartzJobService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/service/IQuartzJobService.java new file mode 100644 index 0000000..9802e6b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/service/IQuartzJobService.java @@ -0,0 +1,67 @@ +package com.ghb.base.modules.quartz.service; + +import java.util.List; + +import com.ghb.base.modules.quartz.entity.QuartzJob; +import org.quartz.SchedulerException; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 定时任务在线管理 + * @Author: Ghb-boot + * @Date: 2019-04-28 + * @Version: V1.1 + */ +public interface IQuartzJobService extends IService { + + /** + * 通过类名寻找定时任务 + * @param jobClassName 类名 + * @return List + */ + List findByJobClassName(String jobClassName); + + /** + * 保存定时任务 + * @param quartzJob + * @return boolean + */ + boolean saveAndScheduleJob(QuartzJob quartzJob); + + /** + * 编辑定时任务 + * @param quartzJob + * @return boolean + * @throws SchedulerException + */ + boolean editAndScheduleJob(QuartzJob quartzJob) throws SchedulerException; + + /** + * 删除定时任务 + * @param quartzJob + * @return boolean + */ + boolean deleteAndStopJob(QuartzJob quartzJob); + + /** + * 恢复定时任务 + * @param quartzJob + * @return + */ + boolean resumeJob(QuartzJob quartzJob); + + /** + * 执行定时任务 + * @param quartzJob + * @throws Exception + */ + void execute(QuartzJob quartzJob) throws Exception; + + /** + * 暂停任务 + * @param quartzJob + * @throws SchedulerException + */ + void pause(QuartzJob quartzJob); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/service/impl/QuartzJobServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/service/impl/QuartzJobServiceImpl.java new file mode 100644 index 0000000..0091349 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/quartz/service/impl/QuartzJobServiceImpl.java @@ -0,0 +1,195 @@ +package com.ghb.base.modules.quartz.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.DateUtils; +import com.ghb.base.modules.quartz.entity.QuartzJob; +import com.ghb.base.modules.quartz.mapper.QuartzJobMapper; +import com.ghb.base.modules.quartz.service.IQuartzJobService; +import org.quartz.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; +import java.util.List; + +/** + * @Description: 定时任务在线管理 + * @Author: Ghb-boot + * @Date: 2019-04-28 + * @Version: V1.1 + */ +@Slf4j +@Service +public class QuartzJobServiceImpl extends ServiceImpl implements IQuartzJobService { + @Autowired + private QuartzJobMapper quartzJobMapper; + @Autowired + private Scheduler scheduler; + + /** + * 立即执行的任务分组 + */ + private static final String JOB_TEST_GROUP = "test_group"; + + @Override + public List findByJobClassName(String jobClassName) { + return quartzJobMapper.findByJobClassName(jobClassName); + } + + /** + * 保存&启动定时任务 + */ + @Override + @Transactional(rollbackFor = GhbBootException.class) + public boolean saveAndScheduleJob(QuartzJob quartzJob) { + // DB设置修改 + quartzJob.setDelFlag(CommonConstant.DEL_FLAG_0); + boolean success = this.save(quartzJob); + if (success) { + if (CommonConstant.STATUS_NORMAL.equals(quartzJob.getStatus())) { + // 定时器添加 + this.schedulerAdd(quartzJob.getId(), quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter()); + } + } + return success; + } + + /** + * 恢复定时任务 + */ + @Override + @Transactional(rollbackFor = GhbBootException.class) + public boolean resumeJob(QuartzJob quartzJob) { + schedulerDelete(quartzJob.getId()); + schedulerAdd(quartzJob.getId(), quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter()); + quartzJob.setStatus(CommonConstant.STATUS_NORMAL); + return this.updateById(quartzJob); + } + + /** + * 编辑&启停定时任务 + * @throws SchedulerException + */ + @Override + @Transactional(rollbackFor = GhbBootException.class) + public boolean editAndScheduleJob(QuartzJob quartzJob) throws SchedulerException { + if (CommonConstant.STATUS_NORMAL.equals(quartzJob.getStatus())) { + schedulerDelete(quartzJob.getId()); + schedulerAdd(quartzJob.getId(), quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter()); + }else{ + scheduler.pauseJob(JobKey.jobKey(quartzJob.getId())); + } + return this.updateById(quartzJob); + } + + /** + * 删除&停止删除定时任务 + */ + @Override + @Transactional(rollbackFor = GhbBootException.class) + public boolean deleteAndStopJob(QuartzJob job) { + schedulerDelete(job.getId()); + boolean ok = this.removeById(job.getId()); + return ok; + } + + @Override + public void execute(QuartzJob quartzJob) throws Exception { + String jobName = quartzJob.getJobClassName().trim(); + Date startDate = new Date(); + String ymd = DateUtils.date2Str(startDate,DateUtils.yyyymmddhhmmss.get()); + String identity = jobName + ymd; + //3秒后执行 只执行一次 + // 代码逻辑说明: 定时任务立即执行,延迟3秒改成0.1秒------- + startDate.setTime(startDate.getTime() + 100L); + // 定义一个Trigger + SimpleTrigger trigger = (SimpleTrigger)TriggerBuilder.newTrigger() + .withIdentity(identity, JOB_TEST_GROUP) + .startAt(startDate) + .build(); + // 构建job信息 + JobDetail jobDetail = JobBuilder.newJob(getClass(jobName).getClass()).withIdentity(identity).usingJobData("parameter", quartzJob.getParameter()).build(); + // 将trigger和 jobDetail 加入这个调度 + scheduler.scheduleJob(jobDetail, trigger); + // 启动scheduler + scheduler.start(); + } + + @Override + @Transactional(rollbackFor = GhbBootException.class) + public void pause(QuartzJob quartzJob){ + schedulerDelete(quartzJob.getId()); + quartzJob.setStatus(CommonConstant.STATUS_DISABLE); + this.updateById(quartzJob); + } + + /** + * 添加定时任务 + * + * @param jobClassName + * @param cronExpression + * @param parameter + */ + private void schedulerAdd(String id, String jobClassName, String cronExpression, String parameter) { + try { + // 启动调度器 + scheduler.start(); + + // 构建job信息 + JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(id).usingJobData("parameter", parameter).build(); + + // 表达式调度构建器(即任务执行的时间) + CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(id).withSchedule(scheduleBuilder).build(); + + scheduler.scheduleJob(jobDetail, trigger); + } catch (SchedulerException e) { + throw new GhbBootException("创建定时任务失败", e); + } catch (RuntimeException e) { + throw new GhbBootException(e.getMessage(), e); + }catch (Exception e) { + throw new GhbBootException("后台找不到该类名:" + jobClassName, e); + } + } + + /** + * 删除定时任务 + * + * @param id + */ + private void schedulerDelete(String id) { + try { + scheduler.pauseTrigger(TriggerKey.triggerKey(id)); + scheduler.unscheduleJob(TriggerKey.triggerKey(id)); + scheduler.deleteJob(JobKey.jobKey(id)); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new GhbBootException("删除定时任务失败"); + } + } + + /** + * 安全加载Job类:仅允许 com.ghb.base. 包下的类,且必须实现 org.quartz.Job 接口 + */ + private static Job getClass(String classname) throws Exception { + // 包名白名单校验,防止任意类实例化导致RCE + if (classname == null || !classname.startsWith("com.ghb.base.")) { + throw new IllegalArgumentException("非法的任务类名:" + classname + ",仅允许 com.ghb.base 包下的Job类"); + } + //update-begin---author:scott ---date:20260416 for:【PR#9538】Class.forName使用上下文类加载器,增强部署兼容性----------- + Class clazz = Class.forName(classname, true, Thread.currentThread().getContextClassLoader()); + //update-end---author:scott ---date:20260416 for:【PR#9538】Class.forName使用上下文类加载器,增强部署兼容性----------- + // 校验是否实现了 org.quartz.Job 接口 + if (!Job.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException("非法的任务类:" + classname + ",必须实现 org.quartz.Job 接口"); + } + return (Job) clazz.getDeclaredConstructor().newInstance(); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/cache/AuthStateRedisCache.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/cache/AuthStateRedisCache.java new file mode 100644 index 0000000..bf40b49 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/cache/AuthStateRedisCache.java @@ -0,0 +1,69 @@ +package com.ghb.base.modules.system.cache; + +import jakarta.annotation.PostConstruct; +import me.zhyd.oauth.cache.AuthCacheConfig; +import me.zhyd.oauth.cache.AuthStateCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.concurrent.TimeUnit; + + +public class AuthStateRedisCache implements AuthStateCache { + + @Autowired + private RedisTemplate redisTemplate; + + private ValueOperations valueOperations; + + @PostConstruct + public void init() { + valueOperations = redisTemplate.opsForValue(); + } + + /** + * 存入缓存,默认3分钟 + * + * @param key 缓存key + * @param value 缓存内容 + */ + @Override + public void cache(String key, String value) { + valueOperations.set(key, value, AuthCacheConfig.timeout, TimeUnit.MILLISECONDS); + } + + /** + * 存入缓存 + * + * @param key 缓存key + * @param value 缓存内容 + * @param timeout 指定缓存过期时间(毫秒) + */ + @Override + public void cache(String key, String value, long timeout) { + valueOperations.set(key, value, timeout, TimeUnit.MILLISECONDS); + } + + /** + * 获取缓存内容 + * + * @param key 缓存key + * @return 缓存内容 + */ + @Override + public String get(String key) { + return valueOperations.get(key); + } + + /** + * 是否存在key,如果对应key的value值已过期,也返回false + * + * @param key 缓存key + * @return true:存在key,并且value没过期;false:key不存在或者已过期 + */ + @Override + public boolean containsKey(String key) { + return redisTemplate.hasKey(key); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/config/AuthStateConfiguration.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/config/AuthStateConfiguration.java new file mode 100644 index 0000000..7466a95 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/config/AuthStateConfiguration.java @@ -0,0 +1,15 @@ +package com.ghb.base.modules.system.config; + +import me.zhyd.oauth.cache.AuthStateCache; +import com.ghb.base.modules.system.cache.AuthStateRedisCache; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AuthStateConfiguration { + + @Bean + public AuthStateCache authStateCache() { + return new AuthStateRedisCache(); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/config/json/app3-version.json b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/config/json/app3-version.json new file mode 100644 index 0000000..f6ccbe0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/config/json/app3-version.json @@ -0,0 +1,13 @@ +{ + "id": "E0CC280", + "appTitle": null, + "appLogo": null, + "carouselImgJson": null, + "routeImgJson": null, + "appVersion": "1.0.0", + "versionNum": 100, + "downloadUrl": "https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/app/JeecgUniapp3_0617.apk", + "wgtUrl": "", + "webDownloadUrl": "https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/app/jeecgboot-setup-3.8.3.exe", + "updateNote": "1. 优化用户体验\n2. 修复已知bug\n" +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/constant/DefIndexConst.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/constant/DefIndexConst.java new file mode 100644 index 0000000..99a55ab --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/constant/DefIndexConst.java @@ -0,0 +1,35 @@ +package com.ghb.base.modules.system.constant; + +/** + * 默认首页常量 + */ +public interface DefIndexConst { + + /** + * 默认首页的roleCode + */ + String DEF_INDEX_ALL = "DEF_INDEX_ALL"; + + /** + * 默认首页的缓存key + */ + String CACHE_KEY = "sys:cache:def_index"; + /** + * 缓存默认首页的类型前缀 + */ + String CACHE_TYPE = "sys:cache:home_type::"; + /** + * 默认首页类型 + */ + String HOME_TYPE_SYSTEM = "system"; + String HOME_TYPE_PERSONAL = "personal"; + String HOME_TYPE_MENU = "menuHome"; + + /** + * 默认首页的初始值 + */ + String DEF_INDEX_NAME = "首页"; + String DEF_INDEX_URL = "/dashboard/analysis"; + String DEF_INDEX_COMPONENT = "dashboard/Analysis"; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/CommonController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/CommonController.java new file mode 100644 index 0000000..c735c46 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/CommonController.java @@ -0,0 +1,346 @@ +package com.ghb.base.modules.system.controller; + +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.FileTypeEnum; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.CommonUtils; +import com.ghb.base.common.util.filter.SsrfFileTypeFilter; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.util.HttpFileToMultipartFileUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; + +/** + *

+ * 用户表 前端控制器 + *

+ * + * @Author scott + * @since 2018-12-20 + */ +@Slf4j +@RestController +@RequestMapping("/sys/common") +public class CommonController { + + @Value(value = "${ghb.path.upload}") + private String uploadpath; + + /** + * 本地:local minio:minio 阿里:alioss + */ + @Value(value="${ghb.uploadType}") + private String uploadType; + + /** + * @Author 政辉 + * @return + */ + @GetMapping("/403") + public Result noauth() { + return Result.error("没有权限,请联系管理员分配权限!"); + } + + /** + * 文件上传统一方法 + * @param request + * @param response + * @return + */ + @PostMapping(value = "/upload") + public Result upload(HttpServletRequest request, HttpServletResponse response) throws Exception { + Result result = new Result<>(); + String savePath = ""; + String bizPath = request.getParameter("biz"); + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + MultipartFile file = multipartRequest.getFile("file"); + + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(file, bizPath); + + if (oConvertUtils.isEmpty(bizPath)) { + bizPath = CommonConstant.UPLOAD_TYPE_OSS.equals(uploadType) ? "upload" : ""; + } + if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){ + savePath = this.uploadLocal(file,bizPath); + }else{ + savePath = CommonUtils.upload(file, bizPath, uploadType); + } + if(oConvertUtils.isNotEmpty(savePath)){ + + //添加到文件表 + String orgName = file.getOriginalFilename(); + // 获取文件名 + orgName = CommonUtils.getFileName(orgName); + String type = orgName.substring(orgName.lastIndexOf(SymbolConstant.SPOT)); + FileTypeEnum fileType = FileTypeEnum.getByType(type); + result.setMessage(savePath); + result.setSuccess(true); + }else { + result.setMessage("上传失败!"); + result.setSuccess(false); + } + return result; + } + + /** + * 本地文件上传 + * @param mf 文件 + * @param bizPath 自定义路径 + * @return + */ + private String uploadLocal(MultipartFile mf,String bizPath){ + try { + String ctxPath = uploadpath; + String fileName = null; + File file = new File(ctxPath + File.separator + bizPath + File.separator ); + if (!file.exists()) { + // 创建文件根目录 + file.mkdirs(); + } + // 获取文件名 + String orgName = mf.getOriginalFilename(); + orgName = CommonUtils.getFileName(orgName); + if(orgName.indexOf(SymbolConstant.SPOT)!=-1){ + fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf(".")); + }else{ + fileName = orgName+ "_" + System.currentTimeMillis(); + } + String savePath = file.getPath() + File.separator + fileName; + File savefile = new File(savePath); + FileCopyUtils.copy(mf.getBytes(), savefile); + String dbpath = null; + if(oConvertUtils.isNotEmpty(bizPath)){ + dbpath = bizPath + File.separator + fileName; + }else{ + dbpath = fileName; + } + if (dbpath.contains(SymbolConstant.DOUBLE_BACKSLASH)) { + dbpath = dbpath.replace(SymbolConstant.DOUBLE_BACKSLASH, SymbolConstant.SINGLE_SLASH); + } + return dbpath; + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return ""; + } + +// @PostMapping(value = "/upload2") +// public Result upload2(HttpServletRequest request, HttpServletResponse response) { +// Result result = new Result<>(); +// try { +// String ctxPath = uploadpath; +// String fileName = null; +// String bizPath = "files"; +// String tempBizPath = request.getParameter("biz"); +// if(oConvertUtils.isNotEmpty(tempBizPath)){ +// bizPath = tempBizPath; +// } +// String nowday = new SimpleDateFormat("yyyyMMdd").format(new Date()); +// File file = new File(ctxPath + File.separator + bizPath + File.separator + nowday); +// if (!file.exists()) { +// file.mkdirs();// 创建文件根目录 +// } +// MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; +// MultipartFile mf = multipartRequest.getFile("file");// 获取上传文件对象 +// String orgName = mf.getOriginalFilename();// 获取文件名 +// fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.indexOf(".")); +// String savePath = file.getPath() + File.separator + fileName; +// File savefile = new File(savePath); +// FileCopyUtils.copy(mf.getBytes(), savefile); +// String dbpath = bizPath + File.separator + nowday + File.separator + fileName; +// if (dbpath.contains("\\")) { +// dbpath = dbpath.replace("\\", "/"); +// } +// result.setMessage(dbpath); +// result.setSuccess(true); +// } catch (IOException e) { +// result.setSuccess(false); +// result.setMessage(e.getMessage()); +// log.error(e.getMessage(), e); +// } +// return result; +// } + + /** + * 预览图片&下载文件 + * 请求地址:http://localhost:8080/common/static/{user/20190119/e1fe9925bc315c60addea1b98eb1cb1349547719_1547866868179.jpg} + * + * @param request + * @param response + */ + @GetMapping(value = "/static/**") + public void view(HttpServletRequest request, HttpServletResponse response) { + // ISO-8859-1 ==> UTF-8 进行编码转换 + String imgPath = extractPathFromPattern(request); + if(oConvertUtils.isEmpty(imgPath) || CommonConstant.STRING_NULL.equals(imgPath)){ + return; + } + + try { + imgPath = imgPath.replace("..", "").replace("../",""); + if (imgPath.endsWith(SymbolConstant.COMMA)) { + imgPath = imgPath.substring(0, imgPath.length() - 1); + } + // 代码逻辑说明: 检查下载文件类型-------------- + SsrfFileTypeFilter.checkDownloadFileType(imgPath); + + String filePath = uploadpath + File.separator + imgPath; + File file = new File(filePath); + if(!file.exists()){ + response.setStatus(404); + log.warn("文件["+imgPath+"]不存在.."); + return; + //throw new RuntimeException(); + } + // 设置强制下载不打开 + response.setContentType("application/force-download"); + response.addHeader("Content-Disposition", "attachment;fileName=" + new String(file.getName().getBytes("UTF-8"),"iso-8859-1")); + + // 结合 StreamingResponseBody 的流式写法 + try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + OutputStream outputStream = response.getOutputStream()) { + byte[] buf = new byte[8192]; + int len; + while ((len = inputStream.read(buf)) != -1) { + outputStream.write(buf, 0, len); + } + outputStream.flush(); + } + } catch (IOException e) { + log.error("预览文件失败" + e.getMessage()); + response.setStatus(404); + e.printStackTrace(); + } + + } + +// /** +// * 下载文件 +// * 请求地址:http://localhost:8080/common/download/{user/20190119/e1fe9925bc315c60addea1b98eb1cb1349547719_1547866868179.jpg} +// * +// * @param request +// * @param response +// * @throws Exception +// */ +// @GetMapping(value = "/download/**") +// public void download(HttpServletRequest request, HttpServletResponse response) throws Exception { +// // ISO-8859-1 ==> UTF-8 进行编码转换 +// String filePath = extractPathFromPattern(request); +// // 其余处理略 +// InputStream inputStream = null; +// OutputStream outputStream = null; +// try { +// filePath = filePath.replace("..", ""); +// if (filePath.endsWith(",")) { +// filePath = filePath.substring(0, filePath.length() - 1); +// } +// String localPath = uploadpath; +// String downloadFilePath = localPath + File.separator + filePath; +// File file = new File(downloadFilePath); +// if (file.exists()) { +// response.setContentType("application/force-download");// 设置强制下载不打开             +// response.addHeader("Content-Disposition", "attachment;fileName=" + new String(file.getName().getBytes("UTF-8"),"iso-8859-1")); +// inputStream = new BufferedInputStream(new FileInputStream(file)); +// outputStream = response.getOutputStream(); +// byte[] buf = new byte[1024]; +// int len; +// while ((len = inputStream.read(buf)) > 0) { +// outputStream.write(buf, 0, len); +// } +// response.flushBuffer(); +// } +// +// } catch (Exception e) { +// log.info("文件下载失败" + e.getMessage()); +// // e.printStackTrace(); +// } finally { +// if (inputStream != null) { +// try { +// inputStream.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// if (outputStream != null) { +// try { +// outputStream.close(); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } +// +// } + + /** + * @功能:pdf预览Iframe + * @param modelAndView + * @return + */ + @RequestMapping("/pdf/pdfPreviewIframe") + public ModelAndView pdfPreviewIframe(ModelAndView modelAndView) { + modelAndView.setViewName("pdfPreviewIframe"); + return modelAndView; + } + + /** + * 把指定URL后的字符串全部截断当成参数 + * 这么做是为了防止URL中包含中文或者特殊字符(/等)时,匹配不了的问题 + * @param request + * @return + */ + private static String extractPathFromPattern(final HttpServletRequest request) { + String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + return new AntPathMatcher().extractPathWithinPattern(bestMatchPattern, path); + } + + /** + * 根据网路图片地址上传到服务器 + * @param jsonObject + * @param request + * @return + */ + @PostMapping("/uploadImgByHttp") + public Result uploadImgByHttp(@RequestBody JSONObject jsonObject, HttpServletRequest request){ + String fileUrl = oConvertUtils.getString(jsonObject.get("fileUrl")); + String filename = oConvertUtils.getString(jsonObject.get("filename")); + String bizPath = oConvertUtils.getString(jsonObject.get("bizPath")); + try { + String savePath = ""; + MultipartFile file = HttpFileToMultipartFileUtil.httpFileToMultipartFile(fileUrl, filename); + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(file, bizPath); + if (oConvertUtils.isEmpty(bizPath)) { + bizPath = CommonConstant.UPLOAD_TYPE_OSS.equals(uploadType) ? "upload" : ""; + } + if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){ + savePath = this.uploadLocal(file,bizPath); + }else{ + savePath = CommonUtils.upload(file, bizPath, uploadType); + } + return Result.OK(savePath); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error(e.getMessage()); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/DuplicateCheckController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/DuplicateCheckController.java new file mode 100644 index 0000000..99523bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/DuplicateCheckController.java @@ -0,0 +1,64 @@ +package com.ghb.base.modules.system.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.system.model.DuplicateCheckVo; +import com.ghb.base.modules.system.service.ISysDictService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * @Title: DuplicateCheckAction + * @Description: 重复校验工具 + * @Author 张代浩 + * @Date 2019-03-25 + * @Version V1.0 + */ +@Slf4j +@RestController +@RequestMapping("/sys/duplicate") +@Tag(name="重复校验") +public class DuplicateCheckController { + + @Autowired + ISysDictService sysDictService; + + /** + * 校验数据是否在系统中是否存在 + * + * @return + */ + @RequestMapping(value = "/check", method = RequestMethod.GET) + @Operation(summary="重复校验接口") + public Result doDuplicateCheck(DuplicateCheckVo duplicateCheckVo, HttpServletRequest request) { + log.debug("----duplicate check------:"+ duplicateCheckVo.toString()); + + // 1.填值为空,直接返回 + if(StringUtils.isEmpty(duplicateCheckVo.getFieldVal())){ + Result rs = new Result(); + rs.setCode(500); + rs.setSuccess(true); + rs.setMessage("数据为空,不作处理!"); + return rs; + } + + // 2.返回结果 + if (sysDictService.duplicateCheckData(duplicateCheckVo)) { + // 该值可用 + return Result.ok("该值可用!"); + } else { + // 该值不可用 + log.info("该值不可用,系统中已存在!"); + return Result.error("该值不可用,系统中已存在!"); + } + } + + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/LoginController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/LoginController.java new file mode 100644 index 0000000..ed47aca --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/LoginController.java @@ -0,0 +1,953 @@ +package com.ghb.base.modules.system.controller; +import org.jeecg.common.util.RedisUtil; + +import cn.hutool.core.util.RandomUtil; +import com.alibaba.fastjson.JSONObject; +import com.aliyuncs.exceptions.ClientException; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.DySmsEnum; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.*; +import com.ghb.base.common.util.encryption.AesEncryptUtil; +import com.ghb.base.common.util.encryption.EncryptedString; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.shiro.IgnoreAuth; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.constant.DefIndexConst; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysRoleIndex; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.model.SysLoginModel; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.service.impl.SysBaseApiImpl; +import com.ghb.base.modules.system.util.RandImageUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +/** + * @Author scott + * @since 2018-12-17 + */ +@RestController +@RequestMapping("/sys") +@Tag(name="用户登录") +@Slf4j +public class LoginController { + @Autowired + private ISysUserService sysUserService; + @Autowired + private ISysPermissionService sysPermissionService; + @Autowired + private SysBaseApiImpl sysBaseApi; + @Autowired + private ISysLogService logService; + @Autowired + private RedisUtil redisUtil; + @Autowired + private ISysDepartService sysDepartService; + @Autowired + private ISysDictService sysDictService; + @Resource + private BaseCommonService baseCommonService; + @Autowired + private GhbBaseConfig GhbBaseConfig; + + private final String BASE_CHECK_CODES = "qwertyuiplkjhgfdsazxcvbnmQWERTYUPLKJHGFDSAZXCVBNM1234567890"; + /** + * 线程池用于异步发送纪要 + */ + public static ExecutorService cachedThreadPool = new ShiroThreadPoolExecutor(0, 1024, 60L, TimeUnit.SECONDS, new SynchronousQueue<>()); + + + + @Operation(summary="登录接口") + @RequestMapping(value = "/login", method = RequestMethod.POST) + public Result login(@RequestBody SysLoginModel sysLoginModel, HttpServletRequest request){ + Result result = new Result<>(); + String username = sysLoginModel.getUsername(); + // 密码加密传输(尝试 AES解密,失败视为明文) + String password = AesEncryptUtil.resolvePassword(sysLoginModel.getPassword()); + log.debug("登录密码,原始密码:{},解密密码:{}" , sysLoginModel.getPassword(), password); + + //step.1 登录失败超出次数5次锁定用户10分钟 + if(isLoginFailOvertimes(username)){ + return result.error500("该用户登录失败次数过多,请于10分钟后再次登录!"); + } + + // step.2 验证码check + String realKey = validateCaptcha(sysLoginModel, result); + if (realKey == null) { + return result; + } + + // step.3 校验用户是否存在且有效 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysUser::getUsername,username); + SysUser sysUser = sysUserService.getOne(queryWrapper); + result = sysUserService.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + return result; + } + + // step.4 校验用户名或密码是否正确 + String userpassword = PasswordUtil.encrypt(username, password, sysUser.getSalt()); + String syspassword = sysUser.getPassword(); + if (!syspassword.equals(userpassword)) { + addLoginFailOvertimes(username); + result.error500("用户名或密码错误"); + return result; + } + + // step.5 登录成功获取用户信息 + String loginOrgCode = sysLoginModel.getLoginOrgCode(); + sysUser.setLoginOrgCode(loginOrgCode); + userInfo(sysUser, result, request, CommonConstant.CLIENT_TYPE_PC); + + // step.6 登录成功删除验证码 + redisUtil.del(realKey); + redisUtil.del(CommonConstant.LOGIN_FAIL + username); + + // step.7 记录用户登录日志 + LoginUser loginUser = new LoginUser(); + BeanUtils.copyProperties(sysUser, loginUser); + baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser); + return result; + } + + + /** + * 【vue3专用】获取用户信息 + */ + @GetMapping("/user/getUserInfo") + public Result getUserInfo(HttpServletRequest request){ + long start = System.currentTimeMillis(); + Result result = new Result(); + String username = JwtUtil.getUserNameByToken(request); + if(oConvertUtils.isNotEmpty(username)) { + // 根据用户名查询用户信息 + SysUser sysUser = sysUserService.getUserByName(username); + JSONObject obj=new JSONObject(); + log.debug("1 获取用户信息耗时(用户基础信息)" + (System.currentTimeMillis() - start) + "毫秒"); + + // 代码逻辑说明: vue3前端,支持自定义首页----------- + String vue3Version = request.getHeader(CommonConstant.VERSION); + SysRoleIndex roleIndex = sysUserService.getDynamicIndexByUserRole(username, vue3Version); + if (oConvertUtils.isNotEmpty(vue3Version) && roleIndex != null && oConvertUtils.isNotEmpty(roleIndex.getUrl())) { + String homePath = roleIndex.getUrl(); + if (!homePath.startsWith(SymbolConstant.SINGLE_SLASH)) { + homePath = SymbolConstant.SINGLE_SLASH + homePath; + } + sysUser.setHomePath(homePath); + } + log.debug("2 获取用户信息耗时 (首页面配置)" + (System.currentTimeMillis() - start) + "毫秒"); + + obj.put("userInfo",sysUser); + obj.put("sysAllDictItems", sysDictService.queryAllDictItems()); + log.debug("3 获取用户信息耗时 (字典数据)" + (System.currentTimeMillis() - start) + "毫秒"); + + result.setResult(obj); + result.success(""); + } + log.debug("end 获取用户信息耗时 " + (System.currentTimeMillis() - start) + "毫秒"); + return result; + + } + + /** + * 退出登录 + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/logout") + public Result logout(HttpServletRequest request,HttpServletResponse response) { + //用户退出逻辑 + String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); + if(oConvertUtils.isEmpty(token)) { + return Result.error("退出登录失败!"); + } + String username = JwtUtil.getUsername(token); + LoginUser sysUser = sysBaseApi.getUserByName(username); + if(sysUser!=null) { + //update-begin---author:zhangdaihao ---date:2026-04-15 for:【issue/9517】校验token签名,防止伪造token强制他人下线(DoS)----------- + if (!JwtUtil.verify(token, username, sysUser.getPassword())) { + return Result.error("Token无效!"); + } + //update-end---author:zhangdaihao ---date:2026-04-15 for:【issue/9517】校验token签名,防止伪造token强制他人下线(DoS)----------- + asyncClearLogoutCache(token, sysUser); // 异步清理 + SecurityUtils.getSubject().logout(); + return Result.ok("退出登录成功!"); + }else { + return Result.error("Token无效!"); + } + } + + /** + * 清理用户缓存 + * + * @param token + * @param sysUser + */ + private void asyncClearLogoutCache(String token, LoginUser sysUser) { + cachedThreadPool.execute(()->{ + //清空用户登录Token缓存 + redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token); + //清空用户登录Shiro权限缓存 + redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId()); + //清空用户的缓存信息(包括部门信息),例如sys:cache:user:: + redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername())); + //清空是否允许同一账号多地同时登录缓存(PC端和APP端) + redisUtil.del(CommonConstant.PREFIX_USER_TOKEN_PC + sysUser.getUsername()); + redisUtil.del(CommonConstant.PREFIX_USER_TOKEN_APP + sysUser.getUsername()); + redisUtil.del(CommonConstant.PREFIX_USER_TOKEN_PHONE + sysUser.getUsername()); + + // 清空用户的默认首页缓存 + redisUtil.del(DefIndexConst.CACHE_TYPE + sysUser.getUsername()); + baseCommonService.addLog("用户名: "+sysUser.getRealname()+",退出成功!", CommonConstant.LOG_TYPE_1, null, sysUser); + log.debug("【退出成功操作】异步处理,退出后,清理用户缓存: "+sysUser.getRealname()); + }); + } + + /** + * 获取访问量 + * @return + */ + @GetMapping("loginfo") + public Result loginfo() { + Result result = new Result(); + JSONObject obj = new JSONObject(); + // 获取一天的开始和结束时间 + Calendar calendar = new GregorianCalendar(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date dayStart = calendar.getTime(); + calendar.add(Calendar.DATE, 1); + Date dayEnd = calendar.getTime(); + // 获取系统访问记录 + Long totalVisitCount = logService.findTotalVisitCount(); + obj.put("totalVisitCount", totalVisitCount); + Long todayVisitCount = logService.findTodayVisitCount(dayStart,dayEnd); + obj.put("todayVisitCount", todayVisitCount); + Long todayIp = logService.findTodayIp(dayStart,dayEnd); + obj.put("todayIp", todayIp); + result.setResult(obj); + result.success("登录成功"); + return result; + } + + /** + * 获取访问量 + * @return + */ + @GetMapping("/visitInfo") + public Result>> visitInfo() { + Result>> result = new Result>>(); + Calendar calendar = new GregorianCalendar(); + calendar.set(Calendar.HOUR_OF_DAY,0); + calendar.set(Calendar.MINUTE,0); + calendar.set(Calendar.SECOND,0); + calendar.set(Calendar.MILLISECOND,0); + calendar.add(Calendar.DAY_OF_MONTH, 1); + Date dayEnd = calendar.getTime(); + calendar.add(Calendar.DAY_OF_MONTH, -7); + Date dayStart = calendar.getTime(); + List> list = logService.findVisitCount(dayStart, dayEnd); + result.setResult(oConvertUtils.toLowerCasePageList(list)); + return result; + } + + + /** + * 登陆成功选择用户当前部门 + * @param user + * @return + */ + @RequestMapping(value = "/selectDepart", method = RequestMethod.PUT) + public Result selectDepart(@RequestBody SysUser user) { + Result result = new Result(); + String username = user.getUsername(); + if(oConvertUtils.isEmpty(username)) { + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + username = sysUser.getUsername(); + } + + //获取登录部门 + String orgCode= user.getOrgCode(); + //获取登录租户 + Integer tenantId = user.getLoginTenantId(); + //设置用户登录部门和登录租户 + this.sysUserService.updateUserDepart(username, orgCode,tenantId); + SysUser sysUser = sysUserService.getUserByName(username); + JSONObject obj = new JSONObject(); + obj.put("userInfo", sysUser); + result.setResult(obj); + return result; + } + + /** + * 短信登录接口 + * + * @param jsonObject + * @return + */ + @PostMapping(value = "/sms") + public Result sms(@RequestBody JSONObject jsonObject,HttpServletRequest request) { + Result result = new Result(); + String clientIp = IpUtils.getIpAddr(request); + String mobile = jsonObject.get("mobile").toString(); + //手机号模式 登录模式: "2" 注册模式: "1" + String smsmode=jsonObject.get("smsmode").toString(); + log.info("-------- IP:{}, 手机号:{},获取绑定验证码", clientIp, mobile); + + if(oConvertUtils.isEmpty(mobile)){ + result.setMessage("手机号不允许为空!"); + result.setSuccess(false); + return result; + } + + // VUEN-2245【漏洞】发现新漏洞待处理20220906 + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+mobile; + Object object = redisUtil.get(redisKey); + + if (object != null) { + result.setMessage("验证码10分钟内,仍然有效!"); + result.setSuccess(false); + return result; + } + + //------------------------------------------------------------------------------------- + //增加 check防止恶意刷短信接口 + if(!DySmsLimit.canSendSms(clientIp)){ + log.warn("--------[警告] IP地址:{}, 短信接口请求太多-------", clientIp); + result.setMessage("短信接口请求太多,请稍后再试!"); + result.setCode(CommonConstant.PHONE_SMS_FAIL_CODE); + result.setSuccess(false); + return result; + } + //------------------------------------------------------------------------------------- + + //随机数 + String captcha = RandomUtil.randomNumbers(6); + JSONObject obj = new JSONObject(); + obj.put("code", captcha); + try { + boolean b = false; + //注册模板 + if (CommonConstant.SMS_TPL_TYPE_1.equals(smsmode)) { + SysUser sysUser = sysUserService.getUserByPhone(mobile); + if(sysUser!=null) { + result.error500(" 手机号已经注册,请直接登录!"); + baseCommonService.addLog("手机号已经注册,请直接登录!", CommonConstant.LOG_TYPE_1, null); + return result; + } + b = DySmsHelper.sendSms(mobile, obj, DySmsEnum.REGISTER_TEMPLATE_CODE); + }else { + //登录模式,校验用户有效性 + SysUser sysUser = sysUserService.getUserByPhone(mobile); + result = sysUserService.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + String message = result.getMessage(); + String userNotExist="该用户不存在,请注册"; + if(userNotExist.equals(message)){ + result.error500("该用户不存在或未绑定手机号"); + } + return result; + } + + /** + * smsmode 短信模板方式 0 .登录模板、1.注册模板、2.忘记密码模板 + */ + if (CommonConstant.SMS_TPL_TYPE_0.equals(smsmode)) { + //登录模板 + b = DySmsHelper.sendSms(mobile, obj, DySmsEnum.LOGIN_TEMPLATE_CODE); + } else if(CommonConstant.SMS_TPL_TYPE_2.equals(smsmode)) { + //忘记密码模板 + b = DySmsHelper.sendSms(mobile, obj, DySmsEnum.FORGET_PASSWORD_TEMPLATE_CODE); + // 代码逻辑说明: 【issues/8567】严重:修改密码存在水平越权问题。--- + if(b){ + String username = sysUser.getUsername(); + obj.put("username",username); + redisUtil.set(redisKey, obj.toJSONString(), 600); + result.setSuccess(true); + return result; + } + } + } + + if (b == false) { + result.setMessage("短信验证码发送失败,请稍后重试"); + result.setSuccess(false); + return result; + } + + //验证码10分钟内有效 + redisUtil.set(redisKey, captcha, 600); + result.setSuccess(true); + + } catch (ClientException e) { + e.printStackTrace(); + result.error500(" 短信接口未配置,请联系管理员!"); + return result; + } + return result; + } + + + /** + * 手机号登录接口 + * + * @param jsonObject + * @return + */ + @Operation(summary="手机号登录接口") + @PostMapping("/phoneLogin") + public Result phoneLogin(@RequestBody JSONObject jsonObject, HttpServletRequest request) { + Result result = new Result(); + String phone = jsonObject.getString("mobile"); + // 平台用户登录失败锁定用户 + if(isLoginFailOvertimes(phone)){ + return result.error500("该用户登录失败次数过多,请于10分钟后再次登录!"); + } + + //校验用户有效性 + SysUser sysUser = sysUserService.getUserByPhone(phone); + result = sysUserService.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + return result; + } + + String smscode = jsonObject.getString("captcha"); + + // 代码逻辑说明: VUEN-2245 【漏洞】发现新漏洞待处理20220906 + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; + Object code = redisUtil.get(redisKey); + + if (!smscode.equals(code)) { + addLoginFailOvertimes(phone); + return Result.error("手机验证码错误"); + } + //用户信息 + String loginOrgCode = jsonObject.getString("loginOrgCode"); + sysUser.setLoginOrgCode(loginOrgCode); + userInfo(sysUser, result, request, CommonConstant.CLIENT_TYPE_PHONE); + //添加日志 + baseCommonService.addLog("用户名: " + sysUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null); + redisUtil.removeAll(redisKey); + return result; + } + + + /** + * 用户信息 + * + * @param sysUser + * @param result + * @return + */ + private Result userInfo(SysUser sysUser, Result result, HttpServletRequest request, String clientType) { + String username = sysUser.getUsername(); + String syspassword = sysUser.getPassword(); + JSONObject obj = new JSONObject(new LinkedHashMap<>()); + + //1.生成token,并设置超时时间 + String token = JwtUtil.sign(username, syspassword, clientType); + redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); + // 根据客户端类型设置对应的过期时间 + long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType) + ? JwtUtil.APP_EXPIRE_TIME * 2 / 1000 + : JwtUtil.EXPIRE_TIME * 2 / 1000; + redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime); + obj.put("token", token); + + // 是否允许同一账号多地同时登录,踢掉之前的登录 + handleSingleSignOn(username, token, clientType); + + //2.设置登录租户 + Result loginTenantError = sysUserService.setLoginTenant(sysUser, obj, username,result); + if (loginTenantError != null) { + return loginTenantError; + } + + //3.设置登录用户信息 + obj.put("userInfo", sysUser); + + //4.设置登录部门 + List departs = sysDepartService.queryUserDeparts(sysUser.getId()); + obj.put("departs", departs); + if (departs == null || departs.size() == 0) { + obj.put("multi_depart", 0); + sysUserService.updateUserDepart(username, null, null); + } else if (departs.size() == 1) { + sysUserService.updateUserDepart(username, departs.get(0).getOrgCode(),null); + obj.put("multi_depart", 1); + } else { + //查询当前是否有登录部门 + SysUser sysUserById = sysUserService.getById(sysUser.getId()); + //【部门切换】支持登录页面选择部门 + String loginOrgCode = sysUser.getLoginOrgCode(); + + // 判断上次登录部门orgCode是否在departs中 + boolean orgCodeInDeparts = departs.stream().anyMatch(d -> sysUserById.getOrgCode() != null && sysUserById.getOrgCode().equalsIgnoreCase(d.getOrgCode())); + if (!orgCodeInDeparts) { + sysUserById.setOrgCode(null); + } + + // 如果未设置登录部门,则将登录部门设置为用户选择的 loginOrgCode(优先),否则设置为默认的第一个部门 + if(oConvertUtils.isEmpty(sysUserById.getOrgCode())){ + String orgCode = oConvertUtils.isNotEmpty(loginOrgCode) ? loginOrgCode : departs.get(0).getOrgCode(); + sysUserService.updateUserDepart(username, orgCode, null); + } else { + // 已设置登录部门,若用户本次登录选择了不同的部门,则优先使用用户选择的 loginOrgCode 更新登录部门 + String orgCode = sysUserById.getOrgCode(); + if(oConvertUtils.isNotEmpty(loginOrgCode) && !orgCode.equalsIgnoreCase(loginOrgCode)){ + sysUserService.updateUserDepart(username, loginOrgCode, null); + } + } + obj.put("multi_depart", 2); + } + + // 5.vue3版本不加载字典数据,vue2下加载字典 + String vue3Version = request.getHeader(CommonConstant.VERSION); + if(oConvertUtils.isEmpty(vue3Version)){ + obj.put("sysAllDictItems", sysDictService.queryAllDictItems()); + } + + result.setResult(obj); + result.success("登录成功"); + return result; + } + + /** + * 同一账号在同一客户端类型只能登录一次 + * + * @author scott + * @date 2025-10-31 + * PC端、APP端、手机号登录分别独立,互不影响 + * + * @param username 用户名 + * @param newToken 新生成的token + * @param clientType 客户端类型(PC、APP、PHONE) + */ + private void handleSingleSignOn(String username, String newToken, String clientType) { + // 检查是否允许并发登录 + if (GhbBaseConfig.getFirewall() == null || GhbBaseConfig.getFirewall().getIsConcurrent()==null || Boolean.TRUE.equals(GhbBaseConfig.getFirewall().getIsConcurrent())) { + // 允许并发登录,只设置当前用户的token缓存,不踢掉之前的登录 + log.debug("并发登录已启用:用户[{}]在{}端允许多地同时登录", username, clientType); + return; + } + + log.info("【并发登录限制已开启】 用户[{}]在{}端不允许多地同时登录", username, clientType); + // 根据客户端类型选择对应的Redis key前缀 + String redisKeyPrefix; + if (CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)) { + redisKeyPrefix = CommonConstant.PREFIX_USER_TOKEN_APP; + } else if (CommonConstant.CLIENT_TYPE_PHONE.equalsIgnoreCase(clientType)) { + redisKeyPrefix = CommonConstant.PREFIX_USER_TOKEN_PHONE; + } else { + redisKeyPrefix = CommonConstant.PREFIX_USER_TOKEN_PC; + } + + String userTokenKey = redisKeyPrefix + username; + + // 获取该用户在当前客户端类型下之前的token + Object oldTokenObj = redisUtil.get(userTokenKey); + if (oldTokenObj != null && !oldTokenObj.equals(newToken)) { + String oldToken = oldTokenObj.toString(); + // 清除旧登录token的缓存(设置 1 小时过期时间) + redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + oldToken); + redisUtil.set(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + oldToken, "不允许同一账号多地同时登录,当前登录被踢掉!", 60 * 1 * 60); + log.info("【并发登录限制已开启】用户[{}]在{}端的旧登录已被踢下线!", username, clientType); + log.info("【并发登录限制已开启】用户被踢下线,新token: {},旧token:{}", newToken, oldToken); + } + + // 保存新的token到单点登录缓存 + redisUtil.set(userTokenKey, newToken); + redisUtil.expire(userTokenKey, JwtUtil.EXPIRE_TIME * 2 / 1000); + } + + /** + * 获取加密字符串 + * @return + */ + @GetMapping(value = "/getEncryptedString") + public Result> getEncryptedString(){ + Result> result = new Result>(); + Map map = new HashMap(5); + map.put("key", EncryptedString.key); + map.put("iv",EncryptedString.iv); + result.setResult(map); + return result; + } + + /** + * 后台生成图形验证码 :有效 + * @param response + * @param key + */ + @Operation(summary="获取验证码") + @GetMapping(value = "/randomImage/{key}") + public Result randomImage(HttpServletResponse response,@PathVariable("key") String key){ + Result res = new Result(); + try { + //生成验证码,存到redis中 + String code = RandomUtil.randomString(BASE_CHECK_CODES,4); + String lowerCaseCode = code.toLowerCase(); + String keyPrefix = Md5Util.md5Encode(key + GhbBaseConfig.getSignatureSecret(), "utf-8"); + String realKey = keyPrefix + lowerCaseCode; + redisUtil.removeAll(keyPrefix); + redisUtil.set(realKey, lowerCaseCode, 60); + log.debug("获取验证码,Redis key = {},checkCode = {}", realKey, code); + String base64 = RandImageUtil.generate(code); + res.setSuccess(true); + res.setResult(base64); + } catch (Exception e) { + log.error(e.getMessage(), e); + res.error500("获取验证码失败,请检查redis配置!"); + return res; + } + return res; + } + +// /** +// * 切换菜单表为vue3的表 +// */ +// @RequiresRoles({"admin"}) +// @GetMapping(value = "/switchVue3Menu") +// public Result switchVue3Menu(HttpServletResponse response) { +// Result res = new Result(); +// sysPermissionService.switchVue3Menu(); +// return res; +// } + + /** + * app登录 + * @param sysLoginModel + * @return + * @throws Exception + */ + @RequestMapping(value = "/mLogin", method = RequestMethod.POST) + public Result mLogin(@RequestBody SysLoginModel sysLoginModel, HttpServletRequest request) throws Exception { + Result result = new Result(); + String username = sysLoginModel.getUsername(); + // 密码加密传输(尝试 AES解密,失败视为明文) + String password = AesEncryptUtil.resolvePassword(sysLoginModel.getPassword()); + log.debug("登录密码,原始密码:{},解密密码:{}" , sysLoginModel.getPassword(), password); + + JSONObject obj = new JSONObject(); + + // 1.平台用户登录失败锁定用户 + if(isLoginFailOvertimes(username)){ + return result.error500("该用户登录失败次数过多,请于10分钟后再次登录!"); + } + // 2.校验用户是否有效 + SysUser sysUser = sysUserService.getUserByName(username); + result = sysUserService.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + return result; + } + + // 3.校验用户名或密码是否正确 + String userpassword = PasswordUtil.encrypt(username, password, sysUser.getSalt()); + String syspassword = sysUser.getPassword(); + if (!syspassword.equals(userpassword)) { + addLoginFailOvertimes(username); + result.error500("用户名或密码错误"); + return result; + } + + //4.设置登录部门 + String orgCode = sysUser.getOrgCode(); + //登录设置的组织 + String loginOrgCode = sysLoginModel.getLoginOrgCode(); + if(oConvertUtils.isEmpty(orgCode)) { + //如果当前用户无选择部门 查看部门关联信息 + if(oConvertUtils.isNotEmpty(loginOrgCode)){ + sysUser.setOrgCode(loginOrgCode); + this.sysUserService.updateUserDepart(username, loginOrgCode,null); + }else{ + List departs = sysDepartService.queryUserDeparts(sysUser.getId()); + if (departs != null && !departs.isEmpty()) { + orgCode = departs.get(0).getOrgCode(); + sysUser.setOrgCode(orgCode); + this.sysUserService.updateUserDepart(username, orgCode,null); + } + } + }else{ + if(oConvertUtils.isNotEmpty(loginOrgCode) && !orgCode.equalsIgnoreCase(loginOrgCode)){ + sysUser.setOrgCode(loginOrgCode); + sysUserService.updateUserDepart(username, loginOrgCode,null); + } + } + + //5. 设置登录租户 + Result loginTenantError = sysUserService.setLoginTenant(sysUser, obj, username, result); + if (loginTenantError != null) { + return loginTenantError; + } + // 设置登录用户信息 + obj.put("userInfo", sysUser); + + //6. 生成token,并设置超时时间 + String token = JwtUtil.sign(username, syspassword, CommonConstant.CLIENT_TYPE_APP); + redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); + redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.APP_EXPIRE_TIME*2 / 1000); + obj.put("token", token); + result.setResult(obj); + result.setSuccess(true); + result.setCode(200); + + // 7.是否允许同一账号多地同时登录(APP端登录,踢掉之前的APP端登录) + handleSingleSignOn(username, token, CommonConstant.CLIENT_TYPE_APP); + + // 8.登录成功记录日志 + baseCommonService.addLog("用户名: " + username + ",登录成功[移动端]!", CommonConstant.LOG_TYPE_1, null); + return result; + } + + /** + * 图形验证码 + * @param sysLoginModel + * @return + */ + @RequestMapping(value = "/checkCaptcha", method = RequestMethod.POST) + public Result checkCaptcha(@RequestBody SysLoginModel sysLoginModel){ + String captcha = sysLoginModel.getCaptcha(); + String checkKey = sysLoginModel.getCheckKey(); + if(captcha==null){ + return Result.error("验证码无效"); + } + String lowerCaseCaptcha = captcha.toLowerCase(); + String realKey = Md5Util.md5Encode(lowerCaseCaptcha+checkKey, "utf-8"); + Object checkCode = redisUtil.get(realKey); + if(checkCode==null || !checkCode.equals(lowerCaseCaptcha)) { + return Result.error("验证码错误"); + } + return Result.ok(); + } + /** + * 登录二维码 + */ + @Operation(summary = "登录二维码") + @GetMapping("/getLoginQrcode") + public Result getLoginQrcode() { + String qrcodeId = CommonConstant.LOGIN_QRCODE_PRE+IdWorker.getIdStr(); + //定义二维码参数 + Map params = new HashMap(5); + params.put("qrcodeId", qrcodeId); + //存放二维码唯一标识30秒有效 + redisUtil.set(CommonConstant.LOGIN_QRCODE + qrcodeId, qrcodeId, 30); + return Result.OK(params); + } + /** + * 扫码二维码 + */ + @Operation(summary = "扫码登录二维码") + @PostMapping("/scanLoginQrcode") + public Result scanLoginQrcode(@RequestParam String qrcodeId, @RequestParam String token) { + Object check = redisUtil.get(CommonConstant.LOGIN_QRCODE + qrcodeId); + if (oConvertUtils.isNotEmpty(check)) { + //存放token给前台读取 + redisUtil.set(CommonConstant.LOGIN_QRCODE_TOKEN+qrcodeId, token, 60); + } else { + return Result.error("二维码已过期,请刷新后重试"); + } + return Result.OK("扫码成功"); + } + + + /** + * 获取用户扫码后保存的token + */ + @Operation(summary = "获取用户扫码后保存的token") + @GetMapping("/getQrcodeToken") + public Result getQrcodeToken(@RequestParam String qrcodeId) { + Object token = redisUtil.get(CommonConstant.LOGIN_QRCODE_TOKEN + qrcodeId); + Map result = new HashMap(5); + Object qrcodeIdExpire = redisUtil.get(CommonConstant.LOGIN_QRCODE + qrcodeId); + if (oConvertUtils.isEmpty(qrcodeIdExpire)) { + //二维码过期通知前台刷新 + result.put("token", "-2"); + return Result.OK(result); + } + if (oConvertUtils.isNotEmpty(token)) { + result.put("success", true); + result.put("token", token); + } else { + result.put("token", "-1"); + } + return Result.OK(result); + } + + /** + * 登录失败超出次数5 返回true + * @param username + * @return + */ + private boolean isLoginFailOvertimes(String username){ + String key = CommonConstant.LOGIN_FAIL + username; + Object failTime = redisUtil.get(key); + if(failTime!=null){ + Integer val = Integer.parseInt(failTime.toString()); + if(val>5){ + return true; + } + } + return false; + } + + /** + * 记录登录失败次数 + * @param username + */ + private void addLoginFailOvertimes(String username){ + String key = CommonConstant.LOGIN_FAIL + username; + Object failTime = redisUtil.get(key); + Integer val = 0; + if(failTime!=null){ + val = Integer.parseInt(failTime.toString()); + } + // 10分钟,一分钟为60s + redisUtil.set(key, ++val, 600); + } + + /** + * 发送短信验证码接口(修改密码) + * + * @param jsonObject + * @return + */ + @PostMapping(value = "/sendChangePwdSms") + public Result sendSms(@RequestBody JSONObject jsonObject) { + Result result = new Result<>(); + String mobile = jsonObject.get("mobile").toString(); + if (oConvertUtils.isEmpty(mobile)) { + result.setMessage("手机号不允许为空!"); + result.setSuccess(false); + return result; + } + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String username = sysUser.getUsername(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUser::getUsername, username).eq(SysUser::getPhone, mobile); + SysUser user = sysUserService.getOne(query); + if (null == user) { + return Result.error("当前登录用户和绑定的手机号不匹配,无法修改密码!"); + } + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE + mobile; + Object object = redisUtil.get(redisKey); + if (object != null) { + result.setMessage("验证码10分钟内,仍然有效!"); + result.setSuccess(false); + return result; + } + //随机数 + String captcha = RandomUtil.randomNumbers(6); + JSONObject obj = new JSONObject(); + obj.put("code", captcha); + try { + boolean b = DySmsHelper.sendSms(mobile, obj, DySmsEnum.CHANGE_PASSWORD_TEMPLATE_CODE); + if (!b) { + result.setMessage("短信验证码发送失败,请稍后重试"); + result.setSuccess(false); + return result; + } + //【issues/8567】严重:修改密码存在水平越权问题 + obj.put("username",username); + redisUtil.set(redisKey, obj.toJSONString(), 300); + result.setSuccess(true); + } catch (ClientException e) { + e.printStackTrace(); + result.error500(" 短信接口未配置,请联系管理员!"); + return result; + } + return result; + } + + + /** + * 图形验证码 + * @param sysLoginModel + * @return + */ + @RequestMapping(value = "/smsCheckCaptcha", method = RequestMethod.POST) + public Result smsCheckCaptcha(@RequestBody SysLoginModel sysLoginModel, HttpServletRequest request){ + String captcha = sysLoginModel.getCaptcha(); + String checkKey = sysLoginModel.getCheckKey(); + if(captcha==null){ + return Result.error("验证码无效"); + } + String lowerCaseCaptcha = captcha.toLowerCase(); + String realKey = Md5Util.md5Encode(lowerCaseCaptcha+checkKey+GhbBaseConfig.getSignatureSecret(), "utf-8"); + Object checkCode = redisUtil.get(realKey); + if(checkCode==null || !checkCode.equals(lowerCaseCaptcha)) { + return Result.error("验证码错误"); + } + String clientIp = IpUtils.getIpAddr(request); + //清空短信记录数量 + DySmsLimit.clearSendSmsCount(clientIp); + redisUtil.removeAll(realKey); + return Result.ok(); + } + /** + * 登录获取用户部门信息 + * + * @param jsonObject + * @return + */ + @IgnoreAuth + @RequestMapping(value = "/loginGetUserDeparts", method = RequestMethod.POST) + public Result loginGetUserDeparts(@RequestBody JSONObject jsonObject, HttpServletRequest request){ + return sysUserService.loginGetUserDeparts(jsonObject); + } + + /** + * 校验验证码工具方法,校验失败直接返回Result,校验通过返回realKey + */ + private String validateCaptcha(SysLoginModel sysLoginModel, Result result) { + // 判断是否启用登录验证码校验 + if (GhbBaseConfig.getFirewall() != null && Boolean.FALSE.equals(GhbBaseConfig.getFirewall().getEnableLoginCaptcha())) { + log.warn("关闭了登录验证码校验,跳过验证码校验!"); + return "LoginWithoutVerifyCode"; + } + + String captcha = sysLoginModel.getCaptcha(); + if (captcha == null) { + result.error500("验证码无效"); + return null; + } + String lowerCaseCaptcha = captcha.toLowerCase(); + String keyPrefix = Md5Util.md5Encode(sysLoginModel.getCheckKey() + GhbBaseConfig.getSignatureSecret(), "utf-8"); + String realKey = keyPrefix + lowerCaseCaptcha; + Object checkCode = redisUtil.get(realKey); + if (checkCode == null || !checkCode.toString().equals(lowerCaseCaptcha)) { + log.warn("验证码错误,key= {} , Ui checkCode= {}, Redis checkCode = {}", sysLoginModel.getCheckKey(), lowerCaseCaptcha, checkCode); + result.error500("验证码错误"); + result.setCode(HttpStatus.PRECONDITION_FAILED.value()); + return null; + } + return realKey; + } +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAnnouncementController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAnnouncementController.java new file mode 100644 index 0000000..e5736ee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAnnouncementController.java @@ -0,0 +1,807 @@ +package com.ghb.base.modules.system.controller; +import org.jeecg.common.util.RedisUtil; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.jeecg.dingtalk.api.core.response.Response; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.dto.PushMessageDTO; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.CommonSendStatus; +import com.ghb.base.common.constant.WebsocketConst; +import com.ghb.base.common.constant.enums.NoticeTypeEnum; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.*; +import com.ghb.base.common.util.filter.SsrfFileTypeFilter; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.message.enums.RangeDateEnum; +import com.ghb.base.modules.message.websocket.WebSocket; +import com.ghb.base.modules.system.entity.SysAnnouncement; +import com.ghb.base.modules.system.entity.SysAnnouncementSend; +import com.ghb.base.modules.system.service.ISysAnnouncementSendService; +import com.ghb.base.modules.system.service.ISysAnnouncementService; +import com.ghb.base.modules.system.service.impl.SysBaseApiImpl; +import com.ghb.base.modules.system.service.impl.ThirdAppDingtalkServiceImpl; +import com.ghb.base.modules.system.service.impl.ThirdAppWechatEnterpriseServiceImpl; +import com.ghb.base.modules.system.util.XssUtils; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.ghb.base.common.constant.CommonConstant.ANNOUNCEMENT_SEND_STATUS_1; + +/** + * @Title: Controller + * @Description: 系统通告表 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version: V1.0 + */ +@RestController +@RequestMapping("/sys/annountCement") +@Slf4j +public class SysAnnouncementController { + @Autowired + private ISysAnnouncementService sysAnnouncementService; + @Autowired + private ISysAnnouncementSendService sysAnnouncementSendService; + @Resource + private WebSocket webSocket; + @Autowired + ThirdAppWechatEnterpriseServiceImpl wechatEnterpriseService; + @Autowired + ThirdAppDingtalkServiceImpl dingtalkService; + @Autowired + private SysBaseApiImpl sysBaseApi; + @Autowired + @Lazy + private RedisUtil redisUtil; + @Autowired + public RedisTemplate redisTemplate; + //常规报错定义 + private static final String SPECIAL_CHAR_ERROR = "保存失败:消息内容包含数据库不支持的特殊字符,请检查并修改内容!"; + private static final String CONTENT_TOO_LONG_ERROR = "保存失败:消息内容超过最大长度限制,请缩减内容长度!"; + private static final String DEFAULT_ERROR = "操作失败,请稍后重试或联系管理员!"; + /** + * 通告缓存 + */ + String ANNO_CACHE_KEY = "sys:cache:announcement"; + /** + * QQYUN-5072【性能优化】线上通知消息打开有点慢 + */ + public static ExecutorService cachedThreadPool = new ShiroThreadPoolExecutor(0, 1024,60L, TimeUnit.SECONDS, new SynchronousQueue()); + public static ExecutorService completeNoteThreadPool = new ShiroThreadPoolExecutor(0, 1024,60L, TimeUnit.SECONDS, new SynchronousQueue()); + + /** + * 分页列表查询 + * @param sysAnnouncement + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequiresPermissions("system:sysAnnouncement:list") + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> queryPageList(SysAnnouncement sysAnnouncement, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysAnnouncement.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + Result> result = new Result>(); + sysAnnouncement.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysAnnouncement, req.getParameterMap()); + Page page = new Page(pageNo,pageSize); + IPage pageList = sysAnnouncementService.page(page, queryWrapper); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 添加 + * @param sysAnnouncement + * @return + */ + @RequiresPermissions("system:sysAnnouncement:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + public Result add(@RequestBody SysAnnouncement sysAnnouncement) { + Result result = new Result(); + try { + // 代码逻辑说明: 标题处理xss攻击的问题 + String title = XssUtils.scriptXss(sysAnnouncement.getTitile()); + sysAnnouncement.setTitile(title); + //update-begin---author:liusq ---date:2025-04-13 for:【issues/9521】富文本msgContent字段未做XSS过滤,存在存储型XSS漏洞----------- + String msgContent = XssUtils.richTextXss(sysAnnouncement.getMsgContent()); + sysAnnouncement.setMsgContent(msgContent); + //update-end---author:liusq ---date:2025-04-13 for:【issues/9521】富文本msgContent字段未做XSS过滤,存在存储型XSS漏洞----------- + // 【安全校验】校验附件文件名,防止路径遍历攻击 + SsrfFileTypeFilter.checkPathTraversalBatch(sysAnnouncement.getFiles()); + sysAnnouncement.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); + //未发布 + sysAnnouncement.setSendStatus(CommonSendStatus.UNPUBLISHED_STATUS_0); + //流程状态 + sysAnnouncement.setBpmStatus("1"); + sysAnnouncement.setNoticeType(NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue()); + sysAnnouncementService.saveAnnouncement(sysAnnouncement); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500(determineErrorMessage(e)); + } + return result; + } + + /** + * 编辑 + * @param sysAnnouncement + * @return + */ + @RequiresPermissions("system:sysAnnouncement:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result eidt(@RequestBody SysAnnouncement sysAnnouncement) { + Result result = new Result(); + SysAnnouncement sysAnnouncementEntity = sysAnnouncementService.getById(sysAnnouncement.getId()); + try{ + if(sysAnnouncementEntity==null) { + result.error500("未找到对应实体"); + }else { + // 代码逻辑说明: 标题处理xss攻击的问题 + String title = XssUtils.scriptXss(sysAnnouncement.getTitile()); + sysAnnouncement.setTitile(title); + //update-begin---author:liusq ---date:2025-04-13 for:【issues/9521】富文本msgContent字段未做XSS过滤,存在存储型XSS漏洞----------- + String msgContent = XssUtils.richTextXss(sysAnnouncement.getMsgContent()); + sysAnnouncement.setMsgContent(msgContent); + //update-end---author:liusq ---date:2025-04-13 for:【issues/9521】富文本msgContent字段未做XSS过滤,存在存储型XSS漏洞----------- + // 【安全校验】校验附件文件名,防止路径遍历攻击 + SsrfFileTypeFilter.checkPathTraversalBatch(sysAnnouncement.getFiles()); + sysAnnouncement.setNoticeType(NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue()); + boolean ok = sysAnnouncementService.upDateAnnouncement(sysAnnouncement); + //TODO 返回false说明什么? + if(ok) { + result.success("修改成功!"); + } + } + } catch (Exception e) { + result.error500(determineErrorMessage(e)); + } + + return result; + } + /** + * 简单编辑 + * @param sysAnnouncement + * @return + */ + //@RequiresPermissions("system:sysAnnouncement:editIzTop") + @RequestMapping(value = "/editIzTop", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result editIzTop(@RequestBody SysAnnouncement sysAnnouncement) { + Result result = new Result(); + SysAnnouncement sysAnnouncementEntity = sysAnnouncementService.getById(sysAnnouncement.getId()); + if(sysAnnouncementEntity==null) { + result.error500("未找到对应实体"); + }else { + Integer izTop = sysAnnouncement.getIzTop(); + sysAnnouncementEntity.setIzTop(oConvertUtils.getInt(izTop,CommonConstant.IZ_TOP_0)); + sysAnnouncementService.updateById(sysAnnouncementEntity); + result.success("修改成功!"); + } + return result; + } + + /** + * 通过id删除 + * @param id + * @return + */ + @RequiresPermissions("system:sysAnnouncement:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + public Result delete(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysAnnouncement sysAnnouncement = sysAnnouncementService.getById(id); + if(sysAnnouncement==null) { + result.error500("未找到对应实体"); + }else { + sysAnnouncement.setDelFlag(CommonConstant.DEL_FLAG_1.toString()); + boolean ok = sysAnnouncementService.updateById(sysAnnouncement); + if(ok) { + result.success("删除成功!"); + } + } + + return result; + } + + /** + * 批量删除 + * @param ids + * @return + */ + @RequiresPermissions("system:sysAnnouncement:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + Result result = new Result(); + if(ids==null || "".equals(ids.trim())) { + result.error500("参数不识别!"); + }else { + String[] id = ids.split(","); + for(int i=0;i queryById(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysAnnouncement sysAnnouncement = sysAnnouncementService.getById(id); + if(sysAnnouncement==null) { + result.error500("未找到对应实体"); + }else { + result.setResult(sysAnnouncement); + result.setSuccess(true); + } + return result; + } + + /** + * 更新发布操作 + * @param id + * @return + */ + @RequiresPermissions("system:sysAnnouncement:doReleaseData") + @RequestMapping(value = "/doReleaseData", method = RequestMethod.GET) + public Result doReleaseData(@RequestParam(name="id",required=true) String id, HttpServletRequest request) { + Result result = new Result(); + SysAnnouncement sysAnnouncement = sysAnnouncementService.getById(id); + if(sysAnnouncement==null) { + result.error500("未找到对应实体"); + }else { + //发布中 + sysAnnouncement.setSendStatus(CommonSendStatus.PUBLISHED_STATUS_1); + sysAnnouncement.setSendTime(new Date()); + String currentUserName = JwtUtil.getUserNameByToken(request); + sysAnnouncement.setSender(currentUserName); + boolean ok = sysAnnouncementService.updateById(sysAnnouncement); + if(oConvertUtils.isEmpty(sysAnnouncement.getNoticeType())){ + sysAnnouncement.setNoticeType(NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue()); + } + if(ok) { + result.success("系统通知推送成功"); + if(sysAnnouncement.getMsgType().equals(CommonConstant.MSG_TYPE_ALL)) { + // 补全公告和用户之前的关系 + sysAnnouncementService.batchInsertSysAnnouncementSend(sysAnnouncement.getId(), sysAnnouncement.getTenantId()); + + // 推送websocket通知 + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_TOPIC); + obj.put(WebsocketConst.MSG_ID, sysAnnouncement.getId()); + obj.put(WebsocketConst.MSG_TXT, sysAnnouncement.getTitile()); + obj.put(CommonConstant.NOTICE_TYPE, sysAnnouncement.getNoticeType()); + webSocket.sendMessage(obj.toJSONString()); + //update-begin-author:liusq---date:2025-11-13--for: JHHB-827 【审批消息】移动端需要有推送 -全推送 + PushMessageDTO pushMessageDTO = new PushMessageDTO(); + pushMessageDTO.setTitle(sysAnnouncement.getTitile()); + pushMessageDTO.setPushType(CommonConstant.MSG_TYPE_ALL); + pushMessageDTO.setContent(sysAnnouncement.getMsgAbstract()); + sysBaseApi.uniPushMsgToUser(pushMessageDTO); + //update-begin-author:liusq---date:2025-11-13--for: JHHB-827 【审批消息】移动端需要有推送 -全推送 + }else { + // 2.插入用户通告阅读标记表记录 + String userId = sysAnnouncement.getUserIds(); + String[] userIds = userId.substring(0, (userId.length()-1)).split(","); + String anntId = sysAnnouncement.getId(); + Date refDate = new Date(); + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER); + obj.put(WebsocketConst.MSG_ID, sysAnnouncement.getId()); + obj.put(WebsocketConst.MSG_TXT, sysAnnouncement.getTitile()); + obj.put(CommonConstant.NOTICE_TYPE, sysAnnouncement.getNoticeType()); + webSocket.sendMessage(userIds, obj.toJSONString()); + //update-begin-author:liusq---date:2025-11-13--for: JHHB-827 【审批消息】移动端需要有推送 + PushMessageDTO pushMessageDTO = new PushMessageDTO(); + pushMessageDTO.setTitle(sysAnnouncement.getTitile()); + pushMessageDTO.setUserIds(Arrays.asList(userIds)); + pushMessageDTO.setContent(sysAnnouncement.getMsgAbstract()); + sysBaseApi.uniPushMsgToUser(pushMessageDTO); + //update-begin-author:liusq---date:2025-11-13--for: JHHB-827 【审批消息】移动端需要有推送 + } + try { + // 同步企业微信、钉钉的消息通知 + Response dtResponse = dingtalkService.sendActionCardMessage(sysAnnouncement, null, true); + wechatEnterpriseService.sendTextCardMessage(sysAnnouncement, null,true); + + if (dtResponse != null && dtResponse.isSuccess()) { + String taskId = dtResponse.getResult(); + sysAnnouncement.setDtTaskId(taskId); + sysAnnouncementService.updateById(sysAnnouncement); + } + } catch (Exception e) { + log.error("同步发送第三方APP消息失败:", e); + } + } + } + + return result; + } + + /** + * 更新撤销操作 + * @param id + * @return + */ + @RequiresPermissions("system:sysAnnouncement:doReovkeData") + @RequestMapping(value = "/doReovkeData", method = RequestMethod.GET) + public Result doReovkeData(@RequestParam(name="id",required=true) String id, HttpServletRequest request) { + Result result = new Result(); + SysAnnouncement sysAnnouncement = sysAnnouncementService.getById(id); + if(sysAnnouncement==null) { + result.error500("未找到对应实体"); + }else { + //撤销发布 + sysAnnouncement.setSendStatus(CommonSendStatus.REVOKE_STATUS_2); + sysAnnouncement.setCancelTime(new Date()); + boolean ok = sysAnnouncementService.updateById(sysAnnouncement); + if(ok) { + result.success("该系统通知撤销成功"); + if (oConvertUtils.isNotEmpty(sysAnnouncement.getDtTaskId())) { + try { + dingtalkService.recallMessage(sysAnnouncement.getDtTaskId()); + } catch (Exception e) { + log.error("第三方APP撤回消息失败:", e); + } + } + } + } + + return result; + } + + /** + * @功能:补充用户数据,并返回系统消息 + * @return + */ + @RequestMapping(value = "/listByUser", method = RequestMethod.GET) + public Result> listByUser(@RequestParam(required = false, defaultValue = "5") Integer pageSize, HttpServletRequest request) { + long start = System.currentTimeMillis(); + Result> result = new Result>(); + Map sysMsgMap = new HashMap(5); + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + + + // 获取上个月的第一天(只查近两个月的通知) + Date lastMonthStartDay = DateRangeUtils.getLastMonthStartDay(); + log.info("-----查询近两个月收到的未读通知-----,近2月的第一天:{}", lastMonthStartDay); + +// //补推送数据(用户和通知的关系表) +// completeNoteThreadPool.execute(()->{ +// sysAnnouncementService.completeAnnouncementSendInfo(); +// }); + + // 2.查询用户未读的系统消息 + Page anntMsgList = new Page(0, pageSize); + //通知公告消息 + anntMsgList = sysAnnouncementService.querySysCementPageByUserId(anntMsgList,userId,"1",null, lastMonthStartDay); + sysMsgMap.put("anntMsgList", anntMsgList.getRecords()); + sysMsgMap.put("anntMsgTotal", anntMsgList.getTotal()); + + log.info("begin 获取用户近2个月的系统公告 (通知)" + (System.currentTimeMillis() - start) + "毫秒"); + + //系统消息 + Page sysMsgList = new Page(0, pageSize); + sysMsgList = sysAnnouncementService.querySysCementPageByUserId(sysMsgList,userId,"2",null, lastMonthStartDay); + sysMsgMap.put("sysMsgList", sysMsgList.getRecords()); + sysMsgMap.put("sysMsgTotal", sysMsgList.getTotal()); + + log.info("end 获取用户2个月的系统公告 (系统消息)" + (System.currentTimeMillis() - start) + "毫秒"); + + result.setSuccess(true); + result.setResult(sysMsgMap); + return result; + } + + + /** + * 获取未读消息通知数量 + * + * @return + */ + @RequestMapping(value = "/getUnreadMessageCount", method = RequestMethod.GET) + public Result> getUnreadMessageCount(@RequestParam(required = false, defaultValue = "5") Integer pageSize, HttpServletRequest request) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + + // 获取上个月的第一天(只查近两个月的通知) + Date lastMonthStartDay = DateRangeUtils.getLastMonthStartDay(); + log.debug(" ------查询近两个月收到的未读通知消息数量------,近2月的第一天:{}", lastMonthStartDay); + // 代码逻辑说明: 【QQYUN-12162】OA项目改造,系统重消息拆分,目前消息都在一起 需按分类进行拆分--- + Map unreadMessageCount = new HashMap<>(); + //系统消息数量 + Integer systemCount = sysAnnouncementService.getUnreadMessageCountByUserId(userId, lastMonthStartDay, NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue()); + unreadMessageCount.put("systemCount",systemCount); + //流程数量 + Integer flowCount = sysAnnouncementService.getUnreadMessageCountByUserId(userId, lastMonthStartDay, NoticeTypeEnum.NOTICE_TYPE_FLOW.getValue()); + unreadMessageCount.put("flowCount",flowCount); + //文件数量 + Integer fileCount = sysAnnouncementService.getUnreadMessageCountByUserId(userId, lastMonthStartDay, NoticeTypeEnum.NOTICE_TYPE_FILE.getValue()); + unreadMessageCount.put("fileCount",fileCount); + //日程计划数量 + Integer planCount = sysAnnouncementService.getUnreadMessageCountByUserId(userId, lastMonthStartDay, NoticeTypeEnum.NOTICE_TYPE_PLAN.getValue()); + unreadMessageCount.put("planCount",planCount); + Integer count = systemCount + flowCount + fileCount + planCount; + unreadMessageCount.put("count",count); + return Result.ok(unreadMessageCount); + } + + + /** + * 导出excel + * + * @param request + */ + @RequiresPermissions("system:sysAnnouncement:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(SysAnnouncement sysAnnouncement,HttpServletRequest request) { + // Step.1 组装查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(sysAnnouncement); + //Step.2 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + queryWrapper.eq(SysAnnouncement::getDelFlag,CommonConstant.DEL_FLAG_0.toString()); + List pageList = sysAnnouncementService.list(queryWrapper); + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "系统通告列表"); + mv.addObject(NormalExcelConstants.CLASS, SysAnnouncement.class); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("系统通告列表数据", "导出人:"+user.getRealname(), "导出信息")); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("system:sysAnnouncement:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List listSysAnnouncements = ExcelImportUtil.importExcel(file.getInputStream(), SysAnnouncement.class, params); + for (SysAnnouncement sysAnnouncementExcel : listSysAnnouncements) { + if(sysAnnouncementExcel.getDelFlag()==null){ + sysAnnouncementExcel.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); + } + if(oConvertUtils.isEmpty(sysAnnouncementExcel.getIzTop())){ + sysAnnouncementExcel.setIzTop(CommonConstant.IZ_TOP_0); + } + sysAnnouncementService.save(sysAnnouncementExcel); + } + return Result.ok("文件导入成功!数据行数:" + listSysAnnouncements.size()); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败!"); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.error("文件导入失败!"); + } + /** + *同步消息 + * @param anntId + * @return + */ + //@RequiresPermissions("system:sysAnnouncement:syncNotic") + @RequestMapping(value = "/syncNotic", method = RequestMethod.GET) + public Result syncNotic(@RequestParam(name="anntId",required=false) String anntId, HttpServletRequest request) { + Result result = new Result(); + JSONObject obj = new JSONObject(); + if(StringUtils.isNotBlank(anntId)){ + SysAnnouncement sysAnnouncement = sysAnnouncementService.getById(anntId); + if(sysAnnouncement==null) { + result.error500("未找到对应实体"); + }else { + if(sysAnnouncement.getMsgType().equals(CommonConstant.MSG_TYPE_ALL)) { + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_TOPIC); + obj.put(WebsocketConst.MSG_ID, sysAnnouncement.getId()); + obj.put(WebsocketConst.MSG_TXT, sysAnnouncement.getTitile()); + webSocket.sendMessage(obj.toJSONString()); + }else { + // 2.插入用户通告阅读标记表记录 + String userId = sysAnnouncement.getUserIds(); + if(oConvertUtils.isNotEmpty(userId)){ + String[] userIds = userId.substring(0, (userId.length()-1)).split(","); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER); + obj.put(WebsocketConst.MSG_ID, sysAnnouncement.getId()); + obj.put(WebsocketConst.MSG_TXT, sysAnnouncement.getTitile()); + webSocket.sendMessage(userIds, obj.toJSONString()); + } + } + } + }else{ + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_TOPIC); + obj.put(WebsocketConst.MSG_TXT, "批量设置已读"); + webSocket.sendMessage(obj.toJSONString()); + } + return result; + } + + /** + * 通告查看详情页面(用于第三方APP) + * @param modelAndView + * @param id + * @return + */ + @GetMapping("/show/{id}") + public ModelAndView showContent(ModelAndView modelAndView, @PathVariable("id") String id, HttpServletRequest request) { + SysAnnouncement announcement = sysAnnouncementService.getById(id); + if (announcement != null) { + boolean tokenOk = false; + try { + // 验证Token有效性 + tokenOk = TokenUtils.verifyToken(request, sysBaseApi, redisUtil); + } catch (Exception ignored) { + } + // 判断是否传递了Token,并且Token有效,如果传了就不做查看限制,直接返回 + // 如果Token无效,就做查看限制:只能查看已发布的 + if (tokenOk || ANNOUNCEMENT_SEND_STATUS_1.equals(announcement.getSendStatus())) { + LoginUser user = sysBaseApi.getUserByName(announcement.getSender()); + if(oConvertUtils.isNotEmpty(user)){ + announcement.setSender(user.getRealname()); + } + modelAndView.addObject("data", announcement); + modelAndView.setViewName("announcement/showContent"); + return modelAndView; + } + } + modelAndView.setStatus(HttpStatus.NOT_FOUND); + return modelAndView; + } + + /** + * 【vue3用】 消息列表查询 + * @param fromUser + * @param busType + * @param starFlag + * @param msgCategory + * @param beginDate + * @param endDate + * @param pageNo + * @param pageSize + * @return + */ + @RequestMapping(value = "/vue3List", method = RequestMethod.GET) + public Result> vue3List(@RequestParam(name="fromUser", required = false) String fromUser, + @RequestParam(name="busType", required = false) String busType, + @RequestParam(name="starFlag", required = false) String starFlag, + @RequestParam(name="msgCategory", required = false) String msgCategory, + @RequestParam(name="rangeDateKey", required = false) String rangeDateKey, + @RequestParam(name="beginDate", required = false) String beginDate, + @RequestParam(name="endDate", required = false) String endDate, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name= "noticeType", required = false) String noticeType) { + long calStartTime = System.currentTimeMillis(); // 记录开始时间 + + // 1、获取日期查询条件,开始时间和结束时间 + Date beginTime = null, endTime = null; + if (RangeDateEnum.ZDY.getKey().equals(rangeDateKey)) { + // 自定义日期范围查询 + if (oConvertUtils.isNotEmpty(beginDate)) { + beginTime = DateUtils.parseDatetime(beginDate); + } + if (oConvertUtils.isNotEmpty(endDate)) { + endTime = DateUtils.parseDatetime(endDate); + } + } else { + // 日期段落查询 + Date[] arr = RangeDateEnum.getRangeArray(rangeDateKey); + if (arr != null) { + beginTime = arr[0]; + endTime = arr[1]; + } + } + + // 2、根据条件查询用户的通知消息 + List ls = this.sysAnnouncementService.querySysMessageList(pageSize, pageNo, fromUser, starFlag,busType, msgCategory, beginTime, endTime, noticeType); + + // 3、设置当前页的消息为已读 + if (!CollectionUtils.isEmpty(ls)) { + // 设置已读 + String readed = "1"; + List annoceIdList = ls.stream().filter(item -> !readed.equals(item.getReadFlag())).map(item -> item.getId()).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(annoceIdList)) { + cachedThreadPool.execute(() -> { + sysAnnouncementService.updateReaded(annoceIdList); + }); + } + } + + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + webSocket.sendMessage(sysUser.getId(), obj.toJSONString()); + + // 4、性能统计耗时 + long calEndTime = System.currentTimeMillis(); // 记录结束时间 + long duration = calEndTime - calStartTime; // 计算耗时 + //System.out.println("耗时:" + duration + " 毫秒"); + + return Result.ok(ls); + } + + + /** + * 根据用户id获取最新一条消息发送时间(创建时间) + * @param userId + * @return + */ + @GetMapping("/getLastAnnountTime") + public Result> getLastAnnountTime(@RequestParam(name = "userId") String userId,@RequestParam(name="noticeType",required = false) String noticeType){ + Result> result = new Result<>(); + //---------------------------------------------------------------------------------------- + // step.1 此接口过慢,可以采用缓存一小时方案 + String keyString = String.format(CommonConstant.CACHE_KEY_USER_LAST_ANNOUNT_TIME_1HOUR, userId) + "_" + noticeType; + if (redisTemplate.hasKey(keyString)) { + log.debug("[SysAnnouncementSend Redis] 通过Redis缓存查询用户最后一次收到系统通知时间,userId={}", userId); + Page pageList = (Page) redisTemplate.opsForValue().get(keyString); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + //---------------------------------------------------------------------------------------- + + Page page = new Page<>(1,1); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysAnnouncementSend::getUserId,userId); + //只查询上个月和本月,的通知的数据 + query.ne(SysAnnouncementSend::getCreateTime, DateRangeUtils.getLastMonthStartDay()); + query.select(SysAnnouncementSend::getCreateTime); // 提高查询效率 + query.orderByDesc(SysAnnouncementSend::getCreateTime); + Page pageList = sysAnnouncementSendService.page(page, query); + + //---------------------------------------------------------------------------------------- + if (pageList != null && pageList.getSize() > 0) { + // step.3 保留1小时redis缓存 + redisTemplate.opsForValue().set(keyString, pageList, 3600, TimeUnit.SECONDS); + } + //---------------------------------------------------------------------------------------- + + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 清除当前用户所有未读消息 + * @return + */ + @PostMapping("/clearAllUnReadMessage") + public Result clearAllUnReadMessage(){ + sysAnnouncementService.clearAllUnReadMessage(); + return Result.ok("清除未读消息成功"); + } + + /** + * 添加访问次数 + * @param id + * @return + */ + @RequestMapping(value = "/addVisitsNumber", method = RequestMethod.GET) + public Result addVisitsNumber(@RequestParam(name="id",required=true) String id) { + int count = oConvertUtils.getInt(redisUtil.get(ANNO_CACHE_KEY+id),0) + 1; + redisUtil.set(ANNO_CACHE_KEY+id, count); + + if (count % 5 == 0) { + cachedThreadPool.execute(() -> { + sysAnnouncementService.updateVisitsNum(id, count); + }); + // 重置访问次数 + redisUtil.del(ANNO_CACHE_KEY+id); + } + return Result.ok("公告消息访问次数+1次"); + } + + /** + * 批量下载文件 + * @param id + * @param request + * @param response + */ + @GetMapping("/downLoadFiles") + public void downLoadFiles(@RequestParam(name="id") String id, + HttpServletRequest request, + HttpServletResponse response){ + sysAnnouncementService.downLoadFiles(id,request,response); + } + /** + * 根据异常信息确定友好的错误提示 + */ + private String determineErrorMessage(Exception e) { + String errorMsg = e.getMessage(); + if (isSpecialCharacterError(errorMsg)) { + return SPECIAL_CHAR_ERROR; + } else if (isContentTooLongError(errorMsg)) { + return CONTENT_TOO_LONG_ERROR; + } else { + return DEFAULT_ERROR; + } + } + /** + * 判断是否为特殊字符错误 + */ + private boolean isSpecialCharacterError(String errorMsg) { + return errorMsg != null + && errorMsg.contains("Incorrect string value") + && errorMsg.contains("column 'msg_content'"); + } + + /** + * 判断是否为内容过长错误 + */ + private boolean isContentTooLongError(String errorMsg) { + return errorMsg != null + && errorMsg.contains("Data too long for column 'msg_content'"); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAnnouncementSendController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAnnouncementSendController.java new file mode 100644 index 0000000..57936cc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAnnouncementSendController.java @@ -0,0 +1,298 @@ +package com.ghb.base.modules.system.controller; + +import java.util.Arrays; +import java.util.Date; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.DataBaseConstant; +import com.ghb.base.common.constant.WebsocketConst; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.SqlInjectionUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.message.websocket.WebSocket; +import com.ghb.base.modules.system.entity.SysAnnouncementSend; +import com.ghb.base.modules.system.model.AnnouncementSendModel; +import com.ghb.base.modules.system.service.ISysAnnouncementSendService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + + /** + * @Title: Controller + * @Description: 用户通告阅读标记表 + * @Author: Ghb-boot + * @Date: 2019-02-21 + * @Version: V1.0 + */ +@RestController +@RequestMapping("/sys/sysAnnouncementSend") +@Slf4j +public class SysAnnouncementSendController { + @Autowired + private ISysAnnouncementSendService sysAnnouncementSendService; + @Autowired + private WebSocket webSocket; + + /** + * 分页列表查询 + * @param sysAnnouncementSend + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/list") + public Result> queryPageList(SysAnnouncementSend sysAnnouncementSend, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + Result> result = new Result>(); + QueryWrapper queryWrapper = new QueryWrapper(sysAnnouncementSend); + Page page = new Page(pageNo,pageSize); + //排序逻辑 处理 + String column = req.getParameter("column"); + String order = req.getParameter("order"); + + if(oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) { + if(DataBaseConstant.SQL_ASC.equals(order)) { + queryWrapper.orderByAsc(SqlInjectionUtil.getSqlInjectSortField(column)); + }else { + queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortField(column)); + } + } + IPage pageList = sysAnnouncementSendService.page(page, queryWrapper); + //log.info("查询当前页:"+pageList.getCurrent()); + //log.info("查询当前页数量:"+pageList.getSize()); + //log.info("查询结果数量:"+pageList.getRecords().size()); + //log.info("数据总数:"+pageList.getTotal()); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 添加 + * @param sysAnnouncementSend + * @return + */ + @PostMapping(value = "/add") + public Result add(@RequestBody SysAnnouncementSend sysAnnouncementSend) { + Result result = new Result(); + try { + sysAnnouncementSendService.save(sysAnnouncementSend); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑 + * @param sysAnnouncementSend + * @return + */ + @PutMapping(value = "/edit") + public Result eidt(@RequestBody SysAnnouncementSend sysAnnouncementSend) { + Result result = new Result(); + SysAnnouncementSend sysAnnouncementSendEntity = sysAnnouncementSendService.getById(sysAnnouncementSend.getId()); + if(sysAnnouncementSendEntity==null) { + result.error500("未找到对应实体"); + }else { + boolean ok = sysAnnouncementSendService.updateById(sysAnnouncementSend); + //TODO 返回false说明什么? + if(ok) { + result.success("操作成功!"); + } + } + + return result; + } + + /** + * 通过id删除 + * @param id + * @return + */ + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + //根据用户id和通告阅读表的id获取当前用户已阅读的数量 + long count = sysAnnouncementSendService.getReadCountByUserId(id); + if(0 == count) { + result.error500("删除失败,该数据不存在或尚未标记为“已读”"); + }else { + boolean ok = sysAnnouncementSendService.removeById(id); + if(ok) { + result.success("删除成功!"); + } + } + + return result; + } + + /** + * 批量删除 + * @param ids + * @return + */ + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + Result result = new Result(); + if(ids==null || "".equals(ids.trim())) { + result.error500("参数不识别!"); + }else { + this.sysAnnouncementSendService.deleteBatchByIds(ids); + result.success("已阅读的消息删除成功!"); + } + return result; + } + + /** + * 通过id查询 + * @param id + * @return + */ + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysAnnouncementSend sysAnnouncementSend = sysAnnouncementSendService.getById(id); + if(sysAnnouncementSend==null) { + result.error500("未找到对应实体"); + }else { + result.setResult(sysAnnouncementSend); + result.setSuccess(true); + } + return result; + } + + /** + * @功能:更新用户系统消息阅读状态 + * @param json + * @return + */ + @PutMapping(value = "/editByAnntIdAndUserId") + public Result editById(@RequestBody JSONObject json) { + Result result = new Result(); + String anntId = json.getString("anntId"); + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + LambdaUpdateWrapper updateWrapper = new UpdateWrapper().lambda(); + updateWrapper.set(SysAnnouncementSend::getReadFlag, CommonConstant.HAS_READ_FLAG); + updateWrapper.set(SysAnnouncementSend::getReadTime, new Date()); + // 代码逻辑说明: 系统模块存在的sql漏洞写法 + updateWrapper.eq(SysAnnouncementSend::getAnntId,anntId); + updateWrapper.eq(SysAnnouncementSend::getUserId,userId); + //updateWrapper.last("where annt_id ='"+anntId+"' and user_id ='"+userId+"'"); + SysAnnouncementSend announcementSend = new SysAnnouncementSend(); + sysAnnouncementSendService.update(announcementSend, updateWrapper); + result.setSuccess(true); + return result; + } + + /** + * @功能:获取我的消息 + * @return + */ + @GetMapping(value = "/getMyAnnouncementSend") + public Result> getMyAnnouncementSend(AnnouncementSendModel announcementSendModel, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize) { + Result> result = new Result>(); + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + announcementSendModel.setUserId(userId); + announcementSendModel.setPageNo((pageNo-1)*pageSize); + announcementSendModel.setPageSize(pageSize); + // 代码逻辑说明: 【TV360X-545】我的消息列表不能通过时间范围查询--- + if(StringUtils.isNotEmpty(announcementSendModel.getSendTimeBegin())){ + announcementSendModel.setSendTimeBegin(announcementSendModel.getSendTimeBegin() + " 00:00:00"); + } + if(StringUtils.isNotEmpty(announcementSendModel.getSendTimeBegin())){ + announcementSendModel.setSendTimeEnd(announcementSendModel.getSendTimeEnd() + " 23:59:59"); + } + Page pageList = new Page(pageNo,pageSize); + pageList = sysAnnouncementSendService.getMyAnnouncementSendPage(pageList, announcementSendModel); + result.setResult(pageList); + result.setSuccess(true); + return result; + } + + /** + * @功能:一键已读 + * @return + */ + @PutMapping(value = "/readAll") + public Result readAll() { + Result result = new Result(); + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + LambdaUpdateWrapper updateWrapper = new UpdateWrapper().lambda(); + updateWrapper.set(SysAnnouncementSend::getReadFlag, CommonConstant.HAS_READ_FLAG); + updateWrapper.set(SysAnnouncementSend::getReadTime, new Date()); + updateWrapper.eq(SysAnnouncementSend::getUserId,userId); + //updateWrapper.last("where user_id ='"+userId+"'"); + SysAnnouncementSend announcementSend = new SysAnnouncementSend(); + sysAnnouncementSendService.update(announcementSend, updateWrapper); + JSONObject socketParams = new JSONObject(); + socketParams.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_TOPIC); + webSocket.sendMessage(socketParams.toJSONString()); + result.setSuccess(true); + result.setMessage("全部已读"); + return result; + } + + + /** + * 根据消息发送记录ID获取消息内容 + * @param sendId + * @return + */ + @GetMapping(value = "/getOne") + public Result getOne(@RequestParam(name="sendId",required=true) String sendId) { + AnnouncementSendModel model = sysAnnouncementSendService.getOne(sendId); + return Result.ok(model); + } + + /** + * 根据业务类型和ID修改阅读状态 + * @param busType + * @return + */ + @GetMapping(value = "/updateSysAnnounReadFlag") + public Result updateSysAnnounReadFlag( + @RequestParam(name="busId",required=true) String busId, + @RequestParam(name="busType",required=false) String busType) { + //更新阅读状态 + boolean updateFlag = sysAnnouncementSendService.updateReadFlagByBusId(busId,busType); + if(updateFlag){ + //刷新未读数量 + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + webSocket.sendMessage(sysUser.getId(), obj.toJSONString()); + } + return Result.ok(); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAppVersionController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAppVersionController.java new file mode 100644 index 0000000..91f8dfd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysAppVersionController.java @@ -0,0 +1,106 @@ +package com.ghb.base.modules.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysAppVersion; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.alibaba.fastjson.JSONObject; +import java.io.IOException; +import java.io.InputStream; + +/** +* @Description: app系统配置 +* @Author: Ghb-boot +* @Date: 2025-07-05 +* @Version: V1.0 +*/ +@Tag(name="app系统配置") +@RestController +@RequestMapping("/sys/version") +@Slf4j +public class SysAppVersionController{ + + @Autowired + private RedisUtil redisUtil; + /** + * app3版本json文件路径 + */ + private final String JSON_PATH = "classpath:org/Ghb/modules/system/config/json/app3-version.json"; + + /** + * APP缓存前缀 + */ + private String APP3_VERSION = "app3:version"; + /** + * app3版本信息 + * @return + */ + @Operation(summary="app版本") + @GetMapping(value = "/app3version") + public Result app3Version(@RequestParam(name="key", required = false)String appKey) throws Exception { + Object appConfig = redisUtil.get(APP3_VERSION + appKey); + if (oConvertUtils.isNotEmpty(appConfig)) { + try { + SysAppVersion sysAppVersion = (SysAppVersion)appConfig; + if(oConvertUtils.isEmpty(sysAppVersion.getDownloadUrl())){ + String jsonContent = readJson(JSON_PATH); + sysAppVersion = JSONObject.parseObject(jsonContent, SysAppVersion.class); + return Result.OK(sysAppVersion); + } + return Result.OK(sysAppVersion); + } catch (Exception e) { + log.error(e.toString(),e); + return Result.error("app版本信息获取失败:" + e.getMessage()); + } + }else{ + // 缓存中没有,从配置的json文件中获取 + try { + String jsonContent = readJson(JSON_PATH); + SysAppVersion sysAppVersion = JSONObject.parseObject(jsonContent, SysAppVersion.class); + return Result.OK(sysAppVersion); + } catch (Exception e) { + log.error("从JSON文件读取app版本信息失败:{}", e); + } + } + return Result.OK(); + } + + /** + * 保存APP3 + * + * @param sysAppVersion + * @return + */ + @RequiresRoles({"admin"}) + @Operation(summary="app系统配置-保存") + @PostMapping(value = "/saveVersion") + public Result saveVersion(@RequestBody SysAppVersion sysAppVersion) { + String id = sysAppVersion.getId(); + redisUtil.set(APP3_VERSION + id,sysAppVersion); + return Result.OK(); + } + + /** + * 读取json格式文件 + * @param jsonSrc + * @return + */ + private String readJson(String jsonSrc) { + String json = ""; + try { + //换个写法,解决springboot读取jar包中文件的问题 + InputStream stream = getClass().getClassLoader().getResourceAsStream(jsonSrc.replace("classpath:", "")); + json = IOUtils.toString(stream,"UTF-8"); + } catch (IOException e) { + log.error(e.getMessage(),e); + } + return json; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCategoryController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCategoryController.java new file mode 100644 index 0000000..fb9a7ef --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCategoryController.java @@ -0,0 +1,557 @@ +package com.ghb.base.modules.system.controller; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.vo.DictModel; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.ImportExcelUtil; +import com.ghb.base.common.util.ReflectHelper; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysCategory; +import com.ghb.base.modules.system.model.TreeSelectModel; +import com.ghb.base.modules.system.service.ISysCategoryService; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + + /** + * @Description: 分类字典 + * @Author: Ghb-boot + * @Date: 2019-05-29 + * @Version: V1.0 + */ +@RestController +@RequestMapping("/sys/category") +@Slf4j +public class SysCategoryController { + @Autowired + private ISysCategoryService sysCategoryService; + + /** + * 分类编码0 + */ + private static final String CATEGORY_ROOT_CODE = "0"; + + /** + * 分页列表查询 + * @param sysCategory + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/rootList") + public Result> queryPageList(SysCategory sysCategory, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + if(oConvertUtils.isEmpty(sysCategory.getPid())){ + sysCategory.setPid("0"); + } + Result> result = new Result>(); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysCategory.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(),0)); + } + //------------------------------------------------------------------------------------------------ + + //--author:os_chengtgen---date:20190804 -----for: 分类字典页面显示错误,issues:377--------start + //--author:liusq---date:20211119 -----for: 【vue3】分类字典页面查询条件配置--------start + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysCategory, req.getParameterMap()); + String name = sysCategory.getName(); + String code = sysCategory.getCode(); + //QueryWrapper queryWrapper = new QueryWrapper(); + if(StringUtils.isBlank(name)&&StringUtils.isBlank(code)){ + queryWrapper.eq("pid", sysCategory.getPid()); + } + //--author:liusq---date:20211119 -----for: 分类字典页面查询条件配置--------end + //--author:os_chengtgen---date:20190804 -----for:【vue3】 分类字典页面显示错误,issues:377--------end + + Page page = new Page(pageNo, pageSize); + IPage pageList = sysCategoryService.page(page, queryWrapper); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + @GetMapping(value = "/childList") + public Result> queryPageList(SysCategory sysCategory,HttpServletRequest req) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysCategory.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + Result> result = new Result>(); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysCategory, req.getParameterMap()); + List list = sysCategoryService.list(queryWrapper); + result.setSuccess(true); + result.setResult(list); + return result; + } + + + /** + * 添加 + * @param sysCategory + * @return + */ + @PostMapping(value = "/add") + public Result add(@RequestBody SysCategory sysCategory) { + Result result = new Result(); + try { + sysCategoryService.addSysCategory(sysCategory); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑 + * @param sysCategory + * @return + */ + @RequestMapping(value = "/edit", method = { RequestMethod.PUT,RequestMethod.POST }) + public Result edit(@RequestBody SysCategory sysCategory) { + Result result = new Result(); + SysCategory sysCategoryEntity = sysCategoryService.getById(sysCategory.getId()); + if(sysCategoryEntity==null) { + result.error500("未找到对应实体"); + }else { + sysCategoryService.updateSysCategory(sysCategory); + result.success("修改成功!"); + } + return result; + } + + /** + * 通过id删除 + * @param id + * @return + */ + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysCategory sysCategory = sysCategoryService.getById(id); + if(sysCategory==null) { + result.error500("未找到对应实体"); + }else { + this.sysCategoryService.deleteSysCategory(id); + result.success("删除成功!"); + } + + return result; + } + + /** + * 批量删除 + * @param ids + * @return + */ + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + Result result = new Result(); + if(ids==null || "".equals(ids.trim())) { + result.error500("参数不识别!"); + }else { + this.sysCategoryService.deleteSysCategory(ids); + result.success("删除成功!"); + } + return result; + } + + /** + * 通过id查询 + * @param id + * @return + */ + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysCategory sysCategory = sysCategoryService.getById(id); + if(sysCategory==null) { + result.error500("未找到对应实体"); + }else { + result.setResult(sysCategory); + result.setSuccess(true); + } + return result; + } + + /** + * 导出excel + * + * @param request + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysCategory sysCategory) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysCategory.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + + // Step.1 组装查询条件查询数据 + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysCategory, request.getParameterMap()); + List pageList = sysCategoryService.list(queryWrapper); + // Step.2 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + // 过滤选中数据 + String selections = request.getParameter("selections"); + if(oConvertUtils.isEmpty(selections)) { + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + }else { + List selectionList = Arrays.asList(selections.split(",")); + List exportList = pageList.stream().filter(item -> selectionList.contains(item.getId())).collect(Collectors.toList()); + mv.addObject(NormalExcelConstants.DATA_LIST, exportList); + } + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "分类字典列表"); + mv.addObject(NormalExcelConstants.CLASS, SysCategory.class); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //导出支持xlsx + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("分类字典列表数据", "导出人:"+user.getRealname(), "导出信息", ExcelType.XSSF)); + //分类字典导出支持导出字段 + String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS); + if(oConvertUtils.isNotEmpty(exportFields)){ + mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields); + } + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) throws IOException{ + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + // 错误信息 + List errorMessage = new ArrayList<>(); + int successLines = 0, errorLines = 0; + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List listSysCategorys = ExcelImportUtil.importExcel(file.getInputStream(), SysCategory.class, params); + // 代码逻辑说明: [issues/8612]分类字典导入bug #8612 ------------ + Set parentCategoryIds = new HashSet<>(); + //按照编码长度排序 + Collections.sort(listSysCategorys); + log.info("排序后的list====>",listSysCategorys); + for (int i = 0; i < listSysCategorys.size(); i++) { + SysCategory sysCategoryExcel = listSysCategorys.get(i); + String code = sysCategoryExcel.getCode(); + if(code.length()>3){ + String pCode = sysCategoryExcel.getCode().substring(0,code.length()-3); + log.info("pCode====>",pCode); + String pId=sysCategoryService.queryIdByCode(pCode); + log.info("pId====>",pId); + if(StringUtils.isNotBlank(pId)){ + sysCategoryExcel.setPid(pId); + parentCategoryIds.add(pId); + } + }else{ + sysCategoryExcel.setPid("0"); + } + try { + sysCategoryService.save(sysCategoryExcel); + successLines++; + } catch (Exception e) { + errorLines++; + String message = e.getMessage().toLowerCase(); + int lineNumber = i + 1; + // 通过索引名判断出错信息 + if (message.contains(CommonConstant.SQL_INDEX_UNIQ_CATEGORY_CODE)) { + errorMessage.add("第 " + lineNumber + " 行:分类编码已经存在,忽略导入。"); + } else { + errorMessage.add("第 " + lineNumber + " 行:未知错误,忽略导入"); + log.error(e.getMessage(), e); + } + } + } + // 代码逻辑说明: [issues/8612]分类字典导入bug #8612 ------------ + if(oConvertUtils.isObjectNotEmpty(parentCategoryIds)){ + for (String parentCategoryId : parentCategoryIds) { + SysCategory parentCategory = sysCategoryService.getById(parentCategoryId); + if(oConvertUtils.isObjectNotEmpty(parentCategory)){ + parentCategory.setHasChild(CommonConstant.STATUS_1); + sysCategoryService.updateById(parentCategory); + } + } + } + } catch (Exception e) { + errorMessage.add("发生异常:" + e.getMessage()); + log.error(e.getMessage(), e); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return ImportExcelUtil.imporReturnRes(errorLines,successLines,errorMessage); + } + + + + /** + * 加载单个数据 用于回显 + */ + @RequestMapping(value = "/loadOne", method = RequestMethod.GET) + public Result loadOne(@RequestParam(name="field") String field,@RequestParam(name="val") String val) { + Result result = new Result(); + try { + // 代码逻辑说明: issues/3663 sql注入问题 + boolean isClassField = ReflectHelper.isClassField(field, SysCategory.class); + if (!isClassField) { + return Result.error("字段无效,请检查!"); + } + QueryWrapper query = new QueryWrapper(); + query.eq(field, val); + List ls = this.sysCategoryService.list(query); + if(ls==null || ls.size()==0) { + result.setMessage("查询无果"); + result.setSuccess(false); + }else if(ls.size()>1) { + result.setMessage("查询数据异常,["+field+"]存在多个值:"+val); + result.setSuccess(false); + }else { + result.setSuccess(true); + result.setResult(ls.get(0)); + } + } catch (Exception e) { + e.printStackTrace(); + result.setMessage(e.getMessage()); + result.setSuccess(false); + } + return result; + } + + /** + * 加载节点的子数据 + */ + @RequestMapping(value = "/loadTreeChildren", method = RequestMethod.GET) + public Result> loadTreeChildren(@RequestParam(name="pid") String pid) { + Result> result = new Result>(); + try { + List ls = this.sysCategoryService.queryListByPid(pid); + result.setResult(ls); + result.setSuccess(true); + } catch (Exception e) { + e.printStackTrace(); + result.setMessage(e.getMessage()); + result.setSuccess(false); + } + return result; + } + + /** + * 加载一级节点/如果是同步 则所有数据 + */ + @RequestMapping(value = "/loadTreeRoot", method = RequestMethod.GET) + public Result> loadTreeRoot(@RequestParam(name="async") Boolean async,@RequestParam(name="pcode") String pcode) { + Result> result = new Result>(); + try { + List ls = this.sysCategoryService.queryListByCode(pcode); + if(!async) { + loadAllCategoryChildren(ls); + } + result.setResult(ls); + result.setSuccess(true); + } catch (Exception e) { + e.printStackTrace(); + result.setMessage(e.getMessage()); + result.setSuccess(false); + } + return result; + } + + /** + * 递归求子节点 同步加载用到 + */ + private void loadAllCategoryChildren(List ls) { + for (TreeSelectModel tsm : ls) { + List temp = this.sysCategoryService.queryListByPid(tsm.getKey()); + if(temp!=null && temp.size()>0) { + tsm.setChildren(temp); + loadAllCategoryChildren(temp); + } + } + } + + /** + * 校验编码 + * @param pid + * @param code + * @return + */ + @GetMapping(value = "/checkCode") + public Result checkCode(@RequestParam(name="pid",required = false) String pid,@RequestParam(name="code",required = false) String code) { + if(oConvertUtils.isEmpty(code)){ + return Result.error("错误,类型编码为空!"); + } + if(oConvertUtils.isEmpty(pid)){ + return Result.ok(); + } + SysCategory parent = this.sysCategoryService.getById(pid); + if(code.startsWith(parent.getCode())){ + return Result.ok(); + }else{ + return Result.error("编码不符合规范,须以\""+parent.getCode()+"\"开头!"); + } + + } + + + /** + * 分类字典树控件 加载节点 + * @param pid + * @param pcode + * @param condition + * @return + */ + @RequestMapping(value = "/loadTreeData", method = RequestMethod.GET) + public Result> loadDict(@RequestParam(name="pid",required = false) String pid,@RequestParam(name="pcode",required = false) String pcode, @RequestParam(name="condition",required = false) String condition) { + Result> result = new Result>(); + //pid如果传值了 就忽略pcode的作用 + if(oConvertUtils.isEmpty(pid)){ + if(oConvertUtils.isEmpty(pcode)){ + result.setSuccess(false); + result.setMessage("加载分类字典树参数有误.[null]!"); + return result; + }else{ + if(ISysCategoryService.ROOT_PID_VALUE.equals(pcode)){ + pid = ISysCategoryService.ROOT_PID_VALUE; + }else{ + pid = this.sysCategoryService.queryIdByCode(pcode); + } + if(oConvertUtils.isEmpty(pid)){ + result.setSuccess(false); + result.setMessage("加载分类字典树参数有误.[code]!"); + return result; + } + } + } + Map query = null; + if(oConvertUtils.isNotEmpty(condition)) { + query = JSON.parseObject(condition, Map.class); + } + List ls = sysCategoryService.queryListByPid(pid,query); + result.setSuccess(true); + result.setResult(ls); + return result; + } + + /** + * 分类字典控件数据回显[表单页面] + * + * @param ids + * @param delNotExist 是否移除不存在的项,默认为true,设为false如果某个key不存在数据库中,则直接返回key本身 + * @return + */ + @RequestMapping(value = "/loadDictItem", method = RequestMethod.GET) + public Result> loadDictItem(@RequestParam(name = "ids") String ids, @RequestParam(name = "delNotExist", required = false, defaultValue = "true") boolean delNotExist) { + Result> result = new Result<>(); + // 非空判断 + if (StringUtils.isBlank(ids)) { + result.setSuccess(false); + result.setMessage("ids 不能为空"); + return result; + } + // 查询数据 + List textList = sysCategoryService.loadDictItem(ids, delNotExist); + result.setSuccess(true); + result.setResult(textList); + return result; + } + + /** + * [列表页面]加载分类字典数据 用于值的替换 + * @param code + * @return + */ + @RequestMapping(value = "/loadAllData", method = RequestMethod.GET) + public Result> loadAllData(@RequestParam(name="code",required = true) String code) { + Result> result = new Result>(); + LambdaQueryWrapper query = new LambdaQueryWrapper(); + if(oConvertUtils.isNotEmpty(code) && !CATEGORY_ROOT_CODE.equals(code)){ + query.likeRight(SysCategory::getCode,code); + } + List list = this.sysCategoryService.list(query); + if(list==null || list.size()==0) { + result.setMessage("无数据,参数有误.[code]"); + result.setSuccess(false); + return result; + } + List rdList = new ArrayList(); + for (SysCategory c : list) { + rdList.add(new DictModel(c.getId(),c.getName())); + } + result.setSuccess(true); + result.setResult(rdList); + return result; + } + + /** + * 根据父级id批量查询子节点 + * @param parentIds + * @return + */ + @GetMapping("/getChildListBatch") + public Result getChildListBatch(@RequestParam("parentIds") String parentIds) { + try { + QueryWrapper queryWrapper = new QueryWrapper<>(); + List parentIdList = Arrays.asList(parentIds.split(",")); + queryWrapper.in("pid", parentIdList); + List list = sysCategoryService.list(queryWrapper); + IPage pageList = new Page<>(1, 10, list.size()); + pageList.setRecords(list); + return Result.OK(pageList); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("批量查询子节点失败:" + e.getMessage()); + } + } + + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCheckRuleController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCheckRuleController.java new file mode 100644 index 0000000..bb93d91 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCheckRuleController.java @@ -0,0 +1,186 @@ +package com.ghb.base.modules.system.controller; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.system.entity.SysCheckRule; +import com.ghb.base.modules.system.service.ISysCheckRuleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Arrays; + +/** + * @Description: 编码校验规则 + * @Author: Ghb-boot + * @Date: 2020-02-04 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "编码校验规则") +@RestController +@RequestMapping("/sys/checkRule") +public class SysCheckRuleController extends GhbController { + + @Autowired + private ISysCheckRuleService sysCheckRuleService; + + /** + * 分页列表查询 + * + * @param sysCheckRule + * @param pageNo + * @param pageSize + * @param request + * @return + */ + @AutoLog(value = "编码校验规则-分页列表查询") + @Operation(summary = "编码校验规则-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList( + SysCheckRule sysCheckRule, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest request + ) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysCheckRule, request.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = sysCheckRuleService.page(page, queryWrapper); + return Result.ok(pageList); + } + + + /** + * 通过id查询 + * + * @param ruleCode + * @return + */ + @AutoLog(value = "编码校验规则-通过Code校验传入的值") + @Operation(summary = "编码校验规则-通过Code校验传入的值") + @GetMapping(value = "/checkByCode") + public Result checkByCode( + @RequestParam(name = "ruleCode") String ruleCode, + @RequestParam(name = "value") String value + ) throws UnsupportedEncodingException { + SysCheckRule sysCheckRule = sysCheckRuleService.getByCode(ruleCode); + if (sysCheckRule == null) { + return Result.error("该编码不存在"); + } + JSONObject errorResult = sysCheckRuleService.checkValue(sysCheckRule, URLDecoder.decode(value, "UTF-8")); + if (errorResult == null) { + return Result.ok(); + } else { + Result r = Result.error(errorResult.getString("message")); + r.setResult(errorResult); + return r; + } + } + + /** + * 添加 + * + * @param sysCheckRule + * @return + */ + @AutoLog(value = "编码校验规则-添加") + @Operation(summary = "编码校验规则-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody SysCheckRule sysCheckRule) { + sysCheckRuleService.save(sysCheckRule); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param sysCheckRule + * @return + */ + @AutoLog(value = "编码校验规则-编辑") + @Operation(summary = "编码校验规则-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody SysCheckRule sysCheckRule) { + sysCheckRuleService.updateById(sysCheckRule); + return Result.ok("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "编码校验规则-通过id删除") + @Operation(summary = "编码校验规则-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysCheckRuleService.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "编码校验规则-批量删除") + @Operation(summary = "编码校验规则-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + this.sysCheckRuleService.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "编码校验规则-通过id查询") + @Operation(summary = "编码校验规则-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysCheckRule sysCheckRule = sysCheckRuleService.getById(id); + return Result.ok(sysCheckRule); + } + + /** + * 导出excel + * + * @param request + * @param sysCheckRule + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysCheckRule sysCheckRule) { + return super.exportXls(request, sysCheckRule, SysCheckRule.class, "编码校验规则"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysCheckRule.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCommentController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCommentController.java new file mode 100644 index 0000000..1992699 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysCommentController.java @@ -0,0 +1,280 @@ +package com.ghb.base.modules.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.api.dto.DataLogDTO; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.modules.system.entity.SysComment; +import com.ghb.base.modules.system.service.ISysCommentService; +import com.ghb.base.modules.system.vo.SysCommentFileVo; +import com.ghb.base.modules.system.vo.SysCommentVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.List; + +/** + * @Description: 系统评论回复表 + * @Author: Ghb-boot + * @Date: 2022-07-19 + * @Version: V1.0 + */ +@Tag(name = "系统评论回复表") +@RestController +@RequestMapping("/sys/comment") +@Slf4j +public class SysCommentController extends GhbController { + + @Autowired + private ISysCommentService sysCommentService; + + @Autowired + private ISysBaseAPI sysBaseAPI; + + + /** + * 在线预览文件地址 + */ + @Value("${ghb.file-view-domain}/onlinePreview") + private String onlinePreviewDomain; + + /** + * 查询评论+文件 + * + * @param sysComment + * @return + */ + @Operation(summary = "系统评论回复表-列表查询") + @GetMapping(value = "/listByForm") + public Result> queryListByForm(SysComment sysComment) { + List list = sysCommentService.queryFormCommentInfo(sysComment); + IPage pageList = new Page(); + pageList.setRecords(list); + return Result.OK(pageList); + } + + /** + * 查询文件 + * + * @param sysComment + * @return + */ + @Operation(summary = "系统评论回复表-列表查询") + @GetMapping(value = "/fileList") + public Result> queryFileList(SysComment sysComment) { + List list = sysCommentService.queryFormFileList(sysComment.getTableName(), sysComment.getTableDataId()); + IPage pageList = new Page(); + pageList.setRecords(list); + return Result.OK(pageList); + } + + @Operation(summary = "系统评论表-添加文本") + @PostMapping(value = "/addText") + public Result addText(@RequestBody SysComment sysComment) { + String commentId = sysCommentService.saveOne(sysComment); + return Result.OK(commentId); + } + + @Operation(summary = "系统评论表-添加文件") + @PostMapping(value = "/addFile") + public Result addFile(HttpServletRequest request) { + try { + sysCommentService.saveOneFileComment(request); + return Result.OK("success"); + } catch (Exception e) { + log.error("评论文件上传失败:{}", e.getMessage()); + return Result.error("操作失败," + e.getMessage()); + } + } + + /** + * app端添加评论表 + * @param request + * @return + */ + @Operation(summary = "系统评论表-添加文件") + @PostMapping(value = "/appAddFile") + public Result appAddFile(HttpServletRequest request) { + try { + sysCommentService.appSaveOneFileComment(request); + return Result.OK("success"); + } catch (Exception e) { + log.error("评论文件上传失败:{}", e.getMessage()); + return Result.error("操作失败," + e.getMessage()); + } + } + + @Operation(summary = "系统评论回复表-通过id删除") + @DeleteMapping(value = "/deleteOne") + public Result deleteOne(@RequestParam(name = "id", required = true) String id) { + SysComment comment = sysCommentService.getById(id); + if(comment==null){ + return Result.error("该评论已被删除!"); + } + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String username = sysUser.getUsername(); + String admin = "admin"; + //除了admin外 其他人只能删除自己的评论 + if((!admin.equals(username)) && !username.equals(comment.getCreateBy())){ + return Result.error("只能删除自己的评论!"); + } + sysCommentService.deleteOne(id); + //删除评论添加日志 + String logContent = "删除了评论, "+ comment.getCommentContent(); + DataLogDTO dataLog = new DataLogDTO(comment.getTableName(), comment.getTableDataId(), logContent, CommonConstant.DATA_LOG_TYPE_COMMENT); + sysBaseAPI.saveDataLog(dataLog); + return Result.OK("删除成功!"); + } + + + /** + * 获取文件预览的地址 + * @return + */ + @GetMapping(value = "/getFileViewDomain") + public Result getFileViewDomain() { + return Result.OK(onlinePreviewDomain); + } + + + /** + * 分页列表查询 + * + * @param sysComment + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "系统评论回复表-分页列表查询") + @Operation(summary = "系统评论回复表-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(SysComment sysComment, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysComment, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysCommentService.page(page, queryWrapper); + return Result.OK(pageList); + } + + + /** + * 添加 + * + * @param sysComment + * @return + */ + @Operation(summary = "系统评论回复表-添加") + //@RequiresPermissions("com.ghb.base.modules.demo:sys_comment:add") + @PostMapping(value = "/add") + public Result add(@RequestBody SysComment sysComment) { + sysCommentService.save(sysComment); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param sysComment + * @return + */ + //@AutoLog(value = "系统评论回复表-编辑") + @Operation(summary = "系统评论回复表-编辑") + //@RequiresPermissions("com.ghb.base.modules.demo:sys_comment:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody SysComment sysComment) { + sysCommentService.updateById(sysComment); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + //@AutoLog(value = "系统评论回复表-通过id删除") + @Operation(summary = "系统评论回复表-通过id删除") + //@RequiresPermissions("com.ghb.base.modules.demo:sys_comment:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysCommentService.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + //@AutoLog(value = "系统评论回复表-批量删除") + @Operation(summary = "系统评论回复表-批量删除") + //@RequiresPermissions("com.ghb.base.modules.demo:sys_comment:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + this.sysCommentService.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "系统评论回复表-通过id查询") + @Operation(summary = "系统评论回复表-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysComment sysComment = sysCommentService.getById(id); + if (sysComment == null) { + return Result.error("未找到对应数据"); + } + return Result.OK(sysComment); + } + + /** + * 导出excel + * + * @param request + * @param sysComment + */ + //@RequiresPermissions("com.ghb.base.modules.demo:sys_comment:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysComment sysComment) { + return super.exportXls(request, sysComment, SysComment.class, "系统评论回复表"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + //@RequiresPermissions("sys_comment:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysComment.class); + } + + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDataLogController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDataLogController.java new file mode 100644 index 0000000..b8af0f4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDataLogController.java @@ -0,0 +1,108 @@ +package com.ghb.base.modules.system.controller; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysDataLog; +import com.ghb.base.modules.system.service.ISysDataLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + +/** + * @Description: 系统数据日志 + * @author: Ghb-boot + */ +@RestController +@RequestMapping("/sys/dataLog") +@Slf4j +public class SysDataLogController { + @Autowired + private ISysDataLogService service; + + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> queryPageList(SysDataLog dataLog,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) { + Result> result = new Result>(); + dataLog.setType(CommonConstant.DATA_LOG_TYPE_JSON); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(dataLog, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = service.page(page, queryWrapper); + log.info("查询当前页:"+pageList.getCurrent()); + log.info("查询当前页数量:"+pageList.getSize()); + log.info("查询结果数量:"+pageList.getRecords().size()); + log.info("数据总数:"+pageList.getTotal()); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 查询对比数据 + * @param req + * @return + */ + @RequestMapping(value = "/queryCompareList", method = RequestMethod.GET) + public Result> queryCompareList(HttpServletRequest req) { + Result> result = new Result<>(); + String dataId1 = req.getParameter("dataId1"); + String dataId2 = req.getParameter("dataId2"); + List idList = new ArrayList(); + idList.add(dataId1); + idList.add(dataId2); + try { + List list = (List) service.listByIds(idList); + result.setResult(list); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(),e); + } + return result; + } + + /** + * 查询版本信息 + * @param req + * @return + */ + @RequestMapping(value = "/queryDataVerList", method = RequestMethod.GET) + public Result> queryDataVerList(HttpServletRequest req) { + Result> result = new Result<>(); + String dataTable = req.getParameter("dataTable"); + String dataId = req.getParameter("dataId"); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("data_table", dataTable); + queryWrapper.eq("data_id", dataId); + // 代码逻辑说明: 新增查询条件-type + String type = req.getParameter("type"); + if (oConvertUtils.isNotEmpty(type)) { + queryWrapper.eq("type", type); + } + // 按时间倒序排 + queryWrapper.orderByDesc("create_time"); + + List list = service.list(queryWrapper); + if(list==null||list.size()<=0) { + result.error500("未找到版本信息"); + }else { + result.setResult(list); + result.setSuccess(true); + } + return result; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDataSourceController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDataSourceController.java new file mode 100644 index 0000000..438f1cd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDataSourceController.java @@ -0,0 +1,248 @@ +package com.ghb.base.modules.system.controller; + + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.util.dynamic.db.DataSourceCachePool; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.common.util.security.JdbcSecurityUtil; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.config.sign.annotation.SignatureCheck; +import com.ghb.base.modules.system.entity.SysDataSource; +import com.ghb.base.modules.system.service.ISysDataSourceService; +import com.ghb.base.modules.system.util.SecurityUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.List; + +/** + * @Description: 多数据源管理 + * @Author: Ghb-boot + * @Date: 2019-12-25 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "多数据源管理") +@RestController +@RequestMapping("/sys/dataSource") +public class SysDataSourceController extends GhbController { + + @Autowired + private ISysDataSourceService sysDataSourceService; + + + /** + * 分页列表查询 + * + * @param sysDataSource + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "多数据源管理-分页列表查询") + @Operation(summary = "多数据源管理-分页列表查询") + @RequiresPermissions("system:datasource:list") + @GetMapping(value = "/list") + public Result queryPageList( + SysDataSource sysDataSource, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysDataSource.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDataSource, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = sysDataSourceService.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 下拉选项数据 (online报表使用) + * @param sysDataSource + * @param req + * @return + */ + @SignatureCheck + @RequiresPermissions("online:report:add") + @GetMapping(value = "/options") + public Result queryOptions(SysDataSource sysDataSource, HttpServletRequest req) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysDataSource.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDataSource, req.getParameterMap()); + List pageList = sysDataSourceService.list(queryWrapper); + JSONArray array = new JSONArray(pageList.size()); + for (SysDataSource item : pageList) { + JSONObject option = new JSONObject(3); + option.put("value", item.getCode()); + option.put("label", item.getName()); + option.put("text", item.getName()); + array.add(option); + } + return Result.ok(array); + } + + /** + * 添加 + * + * @param sysDataSource + * @return + */ + @AutoLog(value = "多数据源管理-添加") + @Operation(summary = "多数据源管理-添加") + @RequiresPermissions("system:datasource:add") + @PostMapping(value = "/add") + public Result add(@RequestBody SysDataSource sysDataSource) { + // 代码逻辑说明: jdbc连接地址漏洞问题 + try { + JdbcSecurityUtil.validate(sysDataSource.getDbUrl()); + JdbcSecurityUtil.validateDriver(sysDataSource.getDbDriver()); + }catch (GhbBootException e){ + log.error(e.toString()); + return Result.error("操作失败:" + e.getMessage()); + } + return sysDataSourceService.saveDataSource(sysDataSource); + } + + /** + * 编辑 + * + * @param sysDataSource + * @return + */ + @AutoLog(value = "多数据源管理-编辑") + @Operation(summary = "多数据源管理-编辑") + @RequiresPermissions("system:datasource:edit") + @RequestMapping(value = "/edit", method ={RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody SysDataSource sysDataSource) { + // 代码逻辑说明: jdbc连接地址漏洞问题 + try { + JdbcSecurityUtil.validate(sysDataSource.getDbUrl()); + JdbcSecurityUtil.validateDriver(sysDataSource.getDbDriver()); + } catch (GhbBootException e) { + log.error(e.toString()); + return Result.error("操作失败:" + e.getMessage()); + } + return sysDataSourceService.editDataSource(sysDataSource); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "多数据源管理-通过id删除") + @Operation(summary = "多数据源管理-通过id删除") + @RequiresPermissions("system:datasource:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id") String id) { + return sysDataSourceService.deleteDataSource(id); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "多数据源管理-批量删除") + @Operation(summary = "多数据源管理-批量删除") + @RequiresPermissions("system:datasource:delete") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids") String ids) { + List idList = Arrays.asList(ids.split(",")); + idList.forEach(item->{ + SysDataSource sysDataSource = sysDataSourceService.getById(item); + DataSourceCachePool.removeCache(sysDataSource.getCode()); + }); + this.sysDataSourceService.removeByIds(idList); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "多数据源管理-通过id查询") + @Operation(summary = "多数据源管理-通过id查询") + @RequiresPermissions("system:datasource:list") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id") String id) throws InterruptedException { + SysDataSource sysDataSource = sysDataSourceService.getById(id); + //密码解密 + String dbPassword = sysDataSource.getDbPassword(); + if(StringUtils.isNotBlank(dbPassword)){ + String decodedStr = SecurityUtil.jiemi(dbPassword); + sysDataSource.setDbPassword(decodedStr); + } + return Result.ok(sysDataSource); + } + + /** + * 导出excel + * + * @param request + * @param sysDataSource + */ + @RequiresPermissions("system:datasource:export") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysDataSource sysDataSource) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysDataSource.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + return super.exportXls(request, sysDataSource, SysDataSource.class, "多数据源管理"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("system:datasource:import") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysDataSource.class); + } + + + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartController.java new file mode 100644 index 0000000..9b1a900 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartController.java @@ -0,0 +1,805 @@ +package com.ghb.base.modules.system.controller; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.config.TenantContext; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.ImportExcelUtil; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.excelstyle.ExcelExportSysUserStyle; +import com.ghb.base.modules.system.model.DepartIdModel; +import com.ghb.base.modules.system.model.SysDepartTreeModel; +import com.ghb.base.modules.system.service.ISysDepartService; +import com.ghb.base.modules.system.service.ISysUserDepartService; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.vo.SysChangeDepartVo; +import com.ghb.base.modules.system.vo.SysDepartExportVo; +import com.ghb.base.modules.system.vo.SysPositionSelectTreeVo; +import com.ghb.base.modules.system.vo.lowapp.ExportDepartVo; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + *

+ * 部门表 前端控制器 + *

+ * + * @Author: Steve @Since: 2019-01-22 + */ +@RestController +@RequestMapping("/sys/sysDepart") +@Slf4j +public class SysDepartController { + + @Autowired + private ISysDepartService sysDepartService; + @Autowired + public RedisTemplate redisTemplate; + @Autowired + private ISysUserService sysUserService; + @Autowired + private ISysUserDepartService sysUserDepartService; + @Autowired + private RedisUtil redisUtil; + /** + * 查询数据 查出我的部门,并以树结构数据格式响应给前端 + * + * @return + */ + @RequestMapping(value = "/queryMyDeptTreeList", method = RequestMethod.GET) + public Result> queryMyDeptTreeList() { + Result> result = new Result<>(); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + try { + if(oConvertUtils.isNotEmpty(user.getUserIdentity()) && user.getUserIdentity().equals( CommonConstant.USER_IDENTITY_2 )){ + // 代码逻辑说明: 部门查询ids为空后的前端显示问题 issues/I3UD06 + String departIds = user.getDepartIds(); + if(StringUtils.isNotBlank(departIds)){ + List list = sysDepartService.queryMyDeptTreeList(departIds); + result.setResult(list); + } + result.setMessage(CommonConstant.USER_IDENTITY_2.toString()); + result.setSuccess(true); + }else{ + result.setMessage(CommonConstant.USER_IDENTITY_1.toString()); + result.setSuccess(true); + } + } catch (Exception e) { + log.error(e.getMessage(),e); + } + return result; + } + + /** + * 查询数据 查出所有部门,并以树结构数据格式响应给前端 + * + * @return + */ + @RequestMapping(value = "/queryTreeList", method = RequestMethod.GET) + public Result> queryTreeList(@RequestParam(name = "ids", required = false) String ids) { + Result> result = new Result<>(); + try { + // 从内存中读取 +// List list =FindsDepartsChildrenUtil.getSysDepartTreeList(); +// if (CollectionUtils.isEmpty(list)) { +// list = sysDepartService.queryTreeList(); +// } + if(oConvertUtils.isNotEmpty(ids)){ + List departList = sysDepartService.queryTreeList(ids); + result.setResult(departList); + }else{ + List list = sysDepartService.queryTreeList(); + result.setResult(list); + } + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(),e); + } + return result; + } + + /** + * 异步查询部门list + * @param parentId 父节点 异步加载时传递 + * @param ids 前端回显是传递 + * @param primaryKey 主键字段(id或者orgCode) + * @return + */ + @RequestMapping(value = "/queryDepartTreeSync", method = RequestMethod.GET) + public Result> queryDepartTreeSync(@RequestParam(name = "pid", required = false) String parentId,@RequestParam(name = "ids", required = false) String ids, @RequestParam(name = "primaryKey", required = false) String primaryKey, @RequestParam(name = "orgCategory", required = false) String orgCategory) { + Result> result = new Result<>(); + try { + List list = sysDepartService.queryTreeListByPid(parentId,ids, primaryKey, orgCategory); + result.setResult(list); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.setSuccess(false); + result.setMessage("查询失败"); + } + return result; + } + + /** + * 异步查询部门和岗位list + * @param parentId 父节点 异步加载时传递 + * @param ids 前端回显是传递 + * @param primaryKey 主键字段(id或者orgCode) + * @return + */ + @RequestMapping(value = "/queryDepartAndPostTreeSync", method = RequestMethod.GET) + public Result> queryDepartAndPostTreeSync(@RequestParam(name = "pid", required = false) String parentId, + @RequestParam(name = "ids", required = false) String ids, + @RequestParam(name = "primaryKey", required = false) String primaryKey, + @RequestParam(name = "departIds", required = false) String departIds, + @RequestParam(name = "name", required = false) String orgName) { + Result> result = new Result<>(); + try { + List list = sysDepartService.queryDepartAndPostTreeSync(parentId,ids, primaryKey, departIds, orgName); + result.setResult(list); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.setSuccess(false); + result.setMessage("查询失败"); + } + return result; + } + + /** + * 获取某个部门的所有父级部门的ID + * + * @param departId 根据departId查 + * @param orgCode 根据orgCode查,departId和orgCode必须有一个不为空 + */ + @GetMapping("/queryAllParentId") + public Result queryParentIds( + @RequestParam(name = "departId", required = false) String departId, + @RequestParam(name = "orgCode", required = false) String orgCode) { + try { + JSONObject data; + if (oConvertUtils.isNotEmpty(departId)) { + data = sysDepartService.queryAllParentIdByDepartId(departId); + } else if (oConvertUtils.isNotEmpty(orgCode)) { + data = sysDepartService.queryAllParentIdByOrgCode(orgCode); + } else { + return Result.error("departId 和 orgCode 不能都为空!"); + } + return Result.OK(data); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error(e.getMessage()); + } + } + + /** + * 添加新数据 添加用户新建的部门对象数据,并保存到数据库 + * + * @param sysDepart + * @return + */ + @RequiresPermissions("system:depart:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + @CacheEvict(value= {CacheConstant.SYS_DEPARTS_CACHE,CacheConstant.SYS_DEPART_IDS_CACHE}, allEntries=true) + public Result add(@RequestBody SysDepart sysDepart, HttpServletRequest request) { + Result result = new Result(); + String username = JwtUtil.getUserNameByToken(request); + try { + sysDepart.setCreateBy(username); + sysDepartService.saveDepartData(sysDepart, username); + //清除部门树内存 + // FindsDepartsChildrenUtil.clearSysDepartTreeList(); + // FindsDepartsChildrenUtil.clearDepartIdModel(); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑数据 编辑部门的部分数据,并保存到数据库 + * + * @param sysDepart + * @return + */ + @RequiresPermissions("system:depart:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + @CacheEvict(value= {CacheConstant.SYS_DEPARTS_CACHE,CacheConstant.SYS_DEPART_IDS_CACHE}, allEntries=true) + public Result edit(@RequestBody SysDepart sysDepart, HttpServletRequest request) { + String username = JwtUtil.getUserNameByToken(request); + sysDepart.setUpdateBy(username); + Result result = new Result(); + SysDepart sysDepartEntity = sysDepartService.getById(sysDepart.getId()); + if (sysDepartEntity == null) { + result.error500("未找到对应实体"); + } else { + boolean ok = sysDepartService.updateDepartDataById(sysDepart, username); + // TODO 返回false说明什么? + if (ok) { + //清除部门树内存 + //FindsDepartsChildrenUtil.clearSysDepartTreeList(); + //FindsDepartsChildrenUtil.clearDepartIdModel(); + result.success("修改成功!"); + } + } + return result; + } + + /** + * 通过id删除 + * @param id + * @return + */ + @RequiresPermissions("system:depart:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + @CacheEvict(value= {CacheConstant.SYS_DEPARTS_CACHE,CacheConstant.SYS_DEPART_IDS_CACHE}, allEntries=true) + public Result delete(@RequestParam(name="id",required=true) String id) { + + Result result = new Result(); + SysDepart sysDepart = sysDepartService.getById(id); + if(sysDepart==null) { + result.error500("未找到对应实体"); + }else { + sysDepartService.deleteDepart(id); + //清除部门树内存 + //FindsDepartsChildrenUtil.clearSysDepartTreeList(); + // FindsDepartsChildrenUtil.clearDepartIdModel(); + result.success("删除成功!"); + } + return result; + } + + + /** + * 批量删除 根据前端请求的多个ID,对数据库执行删除相关部门数据的操作 + * + * @param ids + * @return + */ + @RequiresPermissions("system:depart:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + @CacheEvict(value= {CacheConstant.SYS_DEPARTS_CACHE,CacheConstant.SYS_DEPART_IDS_CACHE}, allEntries=true) + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + + Result result = new Result(); + if (ids == null || "".equals(ids.trim())) { + result.error500("参数不识别!"); + } else { + this.sysDepartService.deleteBatchWithChildren(Arrays.asList(ids.split(","))); + result.success("删除成功!"); + } + return result; + } + + /** + * 查询数据 添加或编辑页面对该方法发起请求,以树结构形式加载所有部门的名称,方便用户的操作 + * + * @return + */ + @RequestMapping(value = "/queryIdTree", method = RequestMethod.GET) + public Result> queryIdTree() { +// Result> result = new Result>(); +// List idList; +// try { +// idList = FindsDepartsChildrenUtil.wrapDepartIdModel(); +// if (idList != null && idList.size() > 0) { +// result.setResult(idList); +// result.setSuccess(true); +// } else { +// sysDepartService.queryTreeList(); +// idList = FindsDepartsChildrenUtil.wrapDepartIdModel(); +// result.setResult(idList); +// result.setSuccess(true); +// } +// return result; +// } catch (Exception e) { +// log.error(e.getMessage(),e); +// result.setSuccess(false); +// return result; +// } + Result> result = new Result<>(); + try { + List list = sysDepartService.queryDepartIdTreeList(); + result.setResult(list); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(),e); + } + return result; + } + + /** + *

+ * 部门搜索功能方法,根据关键字模糊搜索相关部门 + *

+ * + * @param keyWord + * @return + */ + @RequestMapping(value = "/searchBy", method = RequestMethod.GET) + public Result> searchBy(@RequestParam(name = "keyWord", required = true) String keyWord, + @RequestParam(name = "myDeptSearch", required = false) String myDeptSearch, + @RequestParam(name = "orgCategory", required = false) String orgCategory, + @RequestParam(name = "departIds", required = false) String depIds) { + Result> result = new Result>(); + //部门查询,myDeptSearch为1时为我的部门查询,登录用户为上级时查只查负责部门下数据 + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String departIds = null; + if(oConvertUtils.isNotEmpty(user.getUserIdentity()) && user.getUserIdentity().equals( CommonConstant.USER_IDENTITY_2 )){ + departIds = user.getDepartIds(); + } + List treeList = this.sysDepartService.searchByKeyWord(keyWord,myDeptSearch,departIds,orgCategory,depIds); + if (treeList == null || treeList.size() == 0) { + result.setSuccess(false); + result.setMessage("未查询匹配数据!"); + return result; + } + result.setResult(treeList); + return result; + } + + + /** + * 导出excel + * + * @param request + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(SysDepart sysDepart,HttpServletRequest request) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysDepart.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + + //// Step.1 组装查询条件 + //QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDepart, request.getParameterMap()); + //Step.1 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + //List pageList = sysDepartService.list(queryWrapper); + //按字典排序 + //Collections.sort(pageList, new Comparator() { + //@Override + //public int compare(SysDepart arg0, SysDepart arg1) { + //return arg0.getOrgCode().compareTo(arg1.getOrgCode()); + //} + //}); + // 过滤选中数据 + String selections = request.getParameter("selections"); + List idList = new ArrayList<>(); + if (oConvertUtils.isNotEmpty(selections)) { + idList = Arrays.asList(selections.split(",")); + } + //step.2 组装导出数据 + Integer tenantId = sysDepart == null ? null : sysDepart.getTenantId(); + // 代码逻辑说明: 【TV360X-1671】部门管理不支持选中的记录导出--- + List sysDepartExportVos = sysDepartService.getExportDepart(tenantId,idList); + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "部门列表"); + mv.addObject(NormalExcelConstants.CLASS, SysDepartExportVo.class); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + ExportParams exportParams = new ExportParams("导入规则:\n" + + "1、标题为第三行,部门路径和部门名称的标题不允许修改,否则会匹配失败;第四行为数据填写范围;\n" + + "2、部门路径用英文字符/分割,部门名称为部门路径的最后一位;\n" + + "3、部门从一级名称开始创建,如果有同级就需要多添加一行,如研发部/研发一部;研发部/研发二部;\n" + + "4、自定义的部门编码需要满足规则才能导入。如一级部门编码为A01,那么子部门为A01A01,同级子部门为A01A02,编码固定为三位,首字母为A-Z,后两位为数字0-99,依次递增;", "导出人:" + user.getRealname(), "导出信息", ExcelType.XSSF); + exportParams.setTitleHeight((short)70); + exportParams.setStyle(ExcelExportSysUserStyle.class); + mv.addObject(NormalExcelConstants.PARAMS, exportParams); + mv.addObject(NormalExcelConstants.DATA_LIST, sysDepartExportVos); + + return mv; + } + + /** + * 通过excel导入数据 + * 部门导入方案1: 通过机构编码来计算出部门的父级ID,维护上下级关系; + * 部门导入方案2: 你也可以改造下程序,机构编码直接导入,先不设置父ID;全部导入后,写一个sql,补下父ID; + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("system:depart:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + @CacheEvict(value= {CacheConstant.SYS_DEPARTS_CACHE,CacheConstant.SYS_DEPART_IDS_CACHE}, allEntries=true) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + List errorMessageList = new ArrayList<>(); + //List listSysDeparts = null; + List listSysDeparts = null; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { +// // orgCode编码长度 +// int codeLength = YouBianCodeUtil.ZHANWEI_LENGTH; +// listSysDeparts = ExcelImportUtil.importExcel(file.getInputStream(), SysDepart.class, params); +// //按长度排序 +// Collections.sort(listSysDeparts, new Comparator() { +// @Override +// public int compare(SysDepart arg0, SysDepart arg1) { +// return arg0.getOrgCode().length() - arg1.getOrgCode().length(); +// } +// }); +// +// int num = 0; +// for (SysDepart sysDepart : listSysDeparts) { +// String orgCode = sysDepart.getOrgCode(); +// if(orgCode.length() > codeLength) { +// String parentCode = orgCode.substring(0, orgCode.length()-codeLength); +// QueryWrapper queryWrapper = new QueryWrapper(); +// queryWrapper.eq("org_code", parentCode); +// try { +// SysDepart parentDept = sysDepartService.getOne(queryWrapper); +// if(!parentDept.equals(null)) { +// sysDepart.setParentId(parentDept.getId()); +// //更新父级部门不是叶子结点 +// sysDepartService.updateIzLeaf(parentDept.getId(),CommonConstant.NOT_LEAF); +// } else { +// sysDepart.setParentId(""); +// } +// }catch (Exception e) { +// //没有查找到parentDept +// } +// }else{ +// sysDepart.setParentId(""); +// } +// sysDepart.setOrgType(sysDepart.getOrgCode().length()/codeLength+""); +// sysDepart.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); +// if(oConvertUtils.isEmpty(sysDepart.getOrgCategory())){ +// sysDepart.setOrgCategory("1"); +// } +// ImportExcelUtil.importDateSaveOne(sysDepart, ISysDepartService.class, errorMessageList, num, CommonConstant.SQL_INDEX_UNIQ_DEPART_ORG_CODE); +// num++; +// } + + // 代码逻辑说明: 【QQYUN-5482】系统的部门导入导出也可以改成敲敲云模式的部门路径--- + listSysDeparts = ExcelImportUtil.importExcel(file.getInputStream(), SysDepartExportVo.class, params); + sysDepartService.importSysDepart(listSysDeparts,errorMessageList); + + //清空部门缓存 + List keys3 = redisUtil.scan(CacheConstant.SYS_DEPARTS_CACHE + "*"); + List keys4 = redisUtil.scan(CacheConstant.SYS_DEPART_IDS_CACHE + "*"); + redisTemplate.delete(keys3); + redisTemplate.delete(keys4); + return ImportExcelUtil.imporReturnRes(errorMessageList.size(), listSysDeparts.size() - errorMessageList.size(), errorMessageList); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.error("文件导入失败!"); + } + + + /** + * 查询所有部门信息 + * @return + */ + @GetMapping("listAll") + public Result> listAll(@RequestParam(name = "id", required = false) String id) { + Result> result = new Result<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.orderByAsc(SysDepart::getOrgCode); + if(oConvertUtils.isNotEmpty(id)){ + String[] arr = id.split(","); + query.in(SysDepart::getId,arr); + } + List ls = this.sysDepartService.list(query); + result.setSuccess(true); + result.setResult(ls); + return result; + } + /** + * 查询数据 查出所有部门,并以树结构数据格式响应给前端 + * + * @return + */ + @RequestMapping(value = "/queryTreeByKeyWord", method = RequestMethod.GET) + public Result> queryTreeByKeyWord(@RequestParam(name = "keyWord", required = false) String keyWord) { + Result> result = new Result<>(); + try { + Map map=new HashMap(5); + List list = sysDepartService.queryTreeByKeyWord(keyWord); + //根据keyWord获取用户信息 + LambdaQueryWrapper queryUser = new LambdaQueryWrapper(); + queryUser.eq(SysUser::getDelFlag,CommonConstant.DEL_FLAG_0); + queryUser.and(i -> i.like(SysUser::getUsername, keyWord).or().like(SysUser::getRealname, keyWord)); + List sysUsers = this.sysUserService.list(queryUser); + map.put("userList",sysUsers); + map.put("departList",list); + result.setResult(map); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(),e); + } + return result; + } + + /** + * 根据部门编码获取部门信息 + * + * @param orgCode + * @return + */ + @GetMapping("/getDepartName") + public Result getDepartName(@RequestParam(name = "orgCode") String orgCode) { + Result result = new Result<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysDepart::getOrgCode, orgCode); + SysDepart sysDepart = sysDepartService.getOne(query); + result.setSuccess(true); + result.setResult(sysDepart); + return result; + } + + /** + * 根据部门id获取用户信息 + * + * @param id + * @return + */ + @GetMapping("/getUsersByDepartId") + public Result> getUsersByDepartId(@RequestParam(name = "id") String id) { + Result> result = new Result<>(); + List sysUsers = sysUserDepartService.queryUserByDepId(id); + result.setSuccess(true); + result.setResult(sysUsers); + return result; + } + + /** + * @功能:根据id 批量查询 + * @param deptIds + * @return + */ + @RequestMapping(value = "/queryByIds", method = RequestMethod.GET) + public Result> queryByIds(@RequestParam(name = "deptIds") String deptIds) { + Result> result = new Result<>(); + String[] ids = deptIds.split(","); + Collection idList = Arrays.asList(ids); + Collection deptList = sysDepartService.listByIds(idList); + // 设置部门路径名称 + for (SysDepart depart : deptList) { + String departPathName = sysDepartService.getDepartPathNameByOrgCode(depart.getOrgCode(),null); + depart.setDepartPathName(departPathName); + } + result.setSuccess(true); + result.setResult(deptList); + return result; + } + + @GetMapping("/getMyDepartList") + public Result> getMyDepartList(){ + List list = sysDepartService.getMyDepartList(); + return Result.ok(list); + } + + /** + * 异步查询部门list + * @param parentId 父节点 异步加载时传递 + * @return + */ + @RequestMapping(value = "/queryBookDepTreeSync", method = RequestMethod.GET) + public Result> queryBookDepTreeSync(@RequestParam(name = "pid", required = false) String parentId, + @RequestParam(name = "tenantId") Integer tenantId, + @RequestParam(name = "departName",required = false) String departName) { + Result> result = new Result<>(); + try { + List list = sysDepartService.queryBookDepTreeSync(parentId, tenantId, departName); + result.setResult(list); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(),e); + } + return result; + } + + /** + * 通过部门id和租户id获取用户 【低代码应用: 用于选择部门负责人】 + * @param departId + * @return + */ + @GetMapping("/getUsersByDepartTenantId") + public Result> getUsersByDepartTenantId(@RequestParam("departId") String departId){ + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + List sysUserList = sysUserDepartService.getUsersByDepartTenantId(departId,tenantId); + return Result.ok(sysUserList); + } + + /** + * 导出excel【低代码应用: 用于导出部门】 + * + * @param request + */ + @RequestMapping(value = "/appExportXls") + public ModelAndView appExportXls(SysDepart sysDepart,HttpServletRequest request) { + // Step.1 组装查询条件 + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + List pageList = sysDepartService.getExcelDepart(tenantId); + //Step.2 AutoPoi 导出Excel + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "部门列表"); + mv.addObject(NormalExcelConstants.CLASS, ExportDepartVo.class); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("部门列表数据", "导出人:"+user.getRealname(), "导出信息")); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 导入excel【低代码应用: 用于导出部门】 + * + * @param request + */ + @RequestMapping(value = "/appImportExcel", method = RequestMethod.POST) + @CacheEvict(value= {CacheConstant.SYS_DEPARTS_CACHE,CacheConstant.SYS_DEPART_IDS_CACHE}, allEntries=true) + public Result appImportExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + List errorMessageList = new ArrayList<>(); + List listSysDeparts = null; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + listSysDeparts = ExcelImportUtil.importExcel(file.getInputStream(), ExportDepartVo.class, params); + sysDepartService.importExcel(listSysDeparts,errorMessageList); + //清空部门缓存 + List keys3 = redisUtil.scan(CacheConstant.SYS_DEPARTS_CACHE + "*"); + List keys4 = redisUtil.scan(CacheConstant.SYS_DEPART_IDS_CACHE + "*"); + redisTemplate.delete(keys3); + redisTemplate.delete(keys4); + return ImportExcelUtil.imporReturnRes(errorMessageList.size(), listSysDeparts.size() - errorMessageList.size(), errorMessageList); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.error("文件导入失败!"); + } + + /** + * 根据部门id和职级id获取岗位信息 + */ + @GetMapping("/getPositionByDepartId") + public Result> getPositionByDepartId(@RequestParam(name = "parentId") String parentId, + @RequestParam(name = "departId",required = false) String departId, + @RequestParam(name = "positionId") String positionId){ + List positionByDepartId = sysDepartService.getPositionByDepartId(parentId, departId, positionId); + return Result.OK(positionByDepartId); + } + + /** + * 获取职级关系 + * @param departId + * @return + */ + @GetMapping("/getRankRelation") + public Result> getRankRelation(@RequestParam(name = "departId") String departId){ + List list = sysDepartService.getRankRelation(departId); + return Result.ok(list); + } + /** + * 获取ALL职级关系 + * @param departId + * @return + */ + @GetMapping("/getALLRankRelation") + public Result> getALLRankRelation(@RequestParam(name = "departId",required = false) String departId){ + List list = sysDepartService.getALLRankRelation(departId); + return Result.ok(list); + } + + /** + * 根据部门code获取当前和上级部门名称 + * + * @param orgCode + * @param depId + * @return String 部门名称 + */ + @GetMapping("/getDepartPathNameByOrgCode") + public Result getDepartPathNameByOrgCode(@RequestParam(name = "orgCode", required = false) String orgCode, + @RequestParam(name = "depId", required = false) String depId) { + String departName = sysDepartService.getDepartPathNameByOrgCode(orgCode, depId); + return Result.OK(departName); + } + + /** + * 根据部门id获取部门下的岗位id + * + * @param depIds 当前选择的公司、子公司、部门id + * @return + */ + @GetMapping("/getDepPostIdByDepId") + public Result> getDepPostIdByDepId(@RequestParam(name = "depIds") String depIds) { + String departIds = sysDepartService.getDepPostIdByDepId(depIds); + return Result.OK(departIds); + } + + /** + * 更新改变后的部门数据 + * + * @param changeDepartVo + * @return + */ + @PutMapping("/updateChangeDepart") + @RequiresPermissions("system:depart:updateChange") + @RequiresRoles({"admin"}) + public Result updateChangeDepart(@RequestBody SysChangeDepartVo changeDepartVo) { + sysDepartService.updateChangeDepart(changeDepartVo); + return Result.ok("调整部门位置成功!"); + } + + /** + * 获取部门负责人 + * + * @param departId + * @return + */ + @GetMapping("/getDepartmentHead") + public Result> getDepartmentHead(@RequestParam(name = "departId") String departId, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize){ + Page page = new Page<>(pageNo, pageSize); + IPage pageList = sysDepartService.getDepartmentHead(departId,page); + return Result.OK(pageList); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartPermissionController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartPermissionController.java new file mode 100644 index 0000000..fbbc313 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartPermissionController.java @@ -0,0 +1,322 @@ +package com.ghb.base.modules.system.controller; + +import java.util.*; +import java.util.stream.Collectors; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.entity.SysDepartPermission; +import com.ghb.base.modules.system.entity.SysDepartRolePermission; +import com.ghb.base.modules.system.entity.SysPermission; +import com.ghb.base.modules.system.entity.SysPermissionDataRule; +import com.ghb.base.modules.system.model.TreeModel; +import com.ghb.base.modules.system.service.ISysDepartPermissionService; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.modules.system.service.ISysDepartRolePermissionService; +import com.ghb.base.modules.system.service.ISysPermissionDataRuleService; +import com.ghb.base.modules.system.service.ISysPermissionService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; + + /** + * @Description: 部门权限表 + * @Author: Ghb-boot + * @Date: 2020-02-11 + * @Version: V1.0 + */ +@Slf4j +@Tag(name="部门权限表") +@RestController +@RequestMapping("/sys/sysDepartPermission") +public class SysDepartPermissionController extends GhbController { + @Autowired + private ISysDepartPermissionService sysDepartPermissionService; + + @Autowired + private ISysPermissionDataRuleService sysPermissionDataRuleService; + + @Autowired + private ISysPermissionService sysPermissionService; + + @Autowired + private ISysDepartRolePermissionService sysDepartRolePermissionService; + + @Autowired + private BaseCommonService baseCommonService; + + /** + * 分页列表查询 + * + * @param sysDepartPermission + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @Operation(summary="部门权限表-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(SysDepartPermission sysDepartPermission, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDepartPermission, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysDepartPermissionService.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 添加 + * + * @param sysDepartPermission + * @return + */ + @Operation(summary="部门权限表-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody SysDepartPermission sysDepartPermission) { + sysDepartPermissionService.save(sysDepartPermission); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param sysDepartPermission + * @return + */ + @Operation(summary="部门权限表-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody SysDepartPermission sysDepartPermission) { + sysDepartPermissionService.updateById(sysDepartPermission); + return Result.ok("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @Operation(summary="部门权限表-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + sysDepartPermissionService.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @Operation(summary="部门权限表-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.sysDepartPermissionService.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @Operation(summary="部门权限表-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + SysDepartPermission sysDepartPermission = sysDepartPermissionService.getById(id); + return Result.ok(sysDepartPermission); + } + + /** + * 导出excel + * + * @param request + * @param sysDepartPermission + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysDepartPermission sysDepartPermission) { + return super.exportXls(request, sysDepartPermission, SysDepartPermission.class, "部门权限表"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysDepartPermission.class); + } + + /** + * 部门管理授权查询数据规则数据 + */ + @GetMapping(value = "/datarule/{permissionId}/{departId}") + public Result loadDatarule(@PathVariable("permissionId") String permissionId,@PathVariable("departId") String departId) { + List list = sysPermissionDataRuleService.getPermRuleListByPermId(permissionId); + if(list==null || list.size()==0) { + return Result.error("未找到权限配置信息"); + }else { + Map map = new HashMap(5); + map.put("datarule", list); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysDepartPermission::getPermissionId, permissionId) + .eq(SysDepartPermission::getDepartId,departId); + SysDepartPermission sysDepartPermission = sysDepartPermissionService.getOne(query); + if(sysDepartPermission==null) { + //return Result.error("未找到角色菜单配置信息"); + }else { + String drChecked = sysDepartPermission.getDataRuleIds(); + if(oConvertUtils.isNotEmpty(drChecked)) { + map.put("drChecked", drChecked.endsWith(",")?drChecked.substring(0, drChecked.length()-1):drChecked); + } + } + return Result.ok(map); + //TODO 以后按钮权限的查询也走这个请求 无非在map中多加两个key + } + } + + /** + * 保存数据规则至部门菜单关联表 + */ + @PostMapping(value = "/datarule") + public Result saveDatarule(@RequestBody JSONObject jsonObject) { + try { + String permissionId = jsonObject.getString("permissionId"); + String departId = jsonObject.getString("departId"); + String dataRuleIds = jsonObject.getString("dataRuleIds"); + log.info("保存数据规则>>"+"菜单ID:"+permissionId+"部门ID:"+ departId+"数据权限ID:"+dataRuleIds); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysDepartPermission::getPermissionId, permissionId) + .eq(SysDepartPermission::getDepartId,departId); + SysDepartPermission sysDepartPermission = sysDepartPermissionService.getOne(query); + if(sysDepartPermission==null) { + return Result.error("请先保存部门菜单权限!"); + }else { + sysDepartPermission.setDataRuleIds(dataRuleIds); + this.sysDepartPermissionService.updateById(sysDepartPermission); + } + } catch (Exception e) { + log.error("SysDepartPermissionController.saveDatarule()发生异常:" + e.getMessage(),e); + return Result.error("保存失败"); + } + return Result.ok("保存成功!"); + } + + /** + * 查询角色授权 + * + * @return + */ + @RequestMapping(value = "/queryDeptRolePermission", method = RequestMethod.GET) + public Result> queryDeptRolePermission(@RequestParam(name = "roleId", required = true) String roleId) { + Result> result = new Result<>(); + try { + List list = sysDepartRolePermissionService.list(new QueryWrapper().lambda().eq(SysDepartRolePermission::getRoleId, roleId)); + result.setResult(list.stream().map(sysDepartRolePermission -> String.valueOf(sysDepartRolePermission.getPermissionId())).collect(Collectors.toList())); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 保存角色授权 + * + * @return + */ + @RequestMapping(value = "/saveDeptRolePermission", method = RequestMethod.POST) + public Result saveDeptRolePermission(@RequestBody JSONObject json) { + long start = System.currentTimeMillis(); + Result result = new Result<>(); + try { + String roleId = json.getString("roleId"); + String permissionIds = json.getString("permissionIds"); + String lastPermissionIds = json.getString("lastpermissionIds"); + this.sysDepartRolePermissionService.saveDeptRolePermission(roleId, permissionIds, lastPermissionIds); + result.success("保存成功!"); + // 代码逻辑说明: [VUEN-234]部门角色授权添加敏感日志------------ + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + baseCommonService.addLog("修改部门角色ID:"+roleId+"的权限配置,操作人: " +loginUser.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + log.info("======部门角色授权成功=====耗时:" + (System.currentTimeMillis() - start) + "毫秒"); + } catch (Exception e) { + result.error500("授权失败!"); + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 用户角色授权功能,查询菜单权限树 + * @param request + * @return + */ + @RequestMapping(value = "/queryTreeListForDeptRole", method = RequestMethod.GET) + public Result> queryTreeListForDeptRole(@RequestParam(name="departId",required=true) String departId,HttpServletRequest request) { + Result> result = new Result<>(); + //全部权限ids + List ids = new ArrayList<>(); + try { + List list = sysPermissionService.queryDepartPermissionList(departId); + for(SysPermission sysPer : list) { + ids.add(sysPer.getId()); + } + List treeList = new ArrayList<>(); + getTreeModelList(treeList, list, null); + Map resMap = new HashMap(5); + //全部树节点数据 + resMap.put("treeList", treeList); + //全部树ids + resMap.put("ids", ids); + result.setResult(resMap); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + private void getTreeModelList(List treeList, List metaList, TreeModel temp) { + for (SysPermission permission : metaList) { + String tempPid = permission.getParentId(); + TreeModel tree = new TreeModel(permission.getId(), tempPid, permission.getName(),permission.getRuleFlag(), permission.isLeaf()); + if(temp==null && oConvertUtils.isEmpty(tempPid)) { + treeList.add(tree); + if(!tree.getIsLeaf()) { + getTreeModelList(treeList, metaList, tree); + } + }else if(temp!=null && tempPid!=null && tempPid.equals(temp.getKey())){ + temp.getChildren().add(tree); + if(!tree.getIsLeaf()) { + getTreeModelList(treeList, metaList, tree); + } + } + + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartRoleController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartRoleController.java new file mode 100644 index 0000000..1f38ee0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDepartRoleController.java @@ -0,0 +1,310 @@ +package com.ghb.base.modules.system.controller; + +import java.util.*; +import java.util.stream.Collectors; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.service.*; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.system.base.controller.GhbController; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; + + /** + * @Description: 部门角色 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +@Slf4j +@Tag(name="部门角色") +@RestController +@RequestMapping("/sys/sysDepartRole") +public class SysDepartRoleController extends GhbController { + @Autowired + private ISysDepartRoleService sysDepartRoleService; + + @Autowired + private ISysDepartRoleUserService departRoleUserService; + + @Autowired + private ISysDepartPermissionService sysDepartPermissionService; + + @Autowired + private ISysDepartRolePermissionService sysDepartRolePermissionService; + + @Autowired + private ISysDepartService sysDepartService; + + @Autowired + private BaseCommonService baseCommonService; + + /** + * 分页列表查询 + * + * @param sysDepartRole + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @Operation(summary="部门角色-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(SysDepartRole sysDepartRole, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name="deptId",required=false) String deptId, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDepartRole, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); +// LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); +// List deptIds = null; +// if(oConvertUtils.isEmpty(deptId)){ +// if(oConvertUtils.isNotEmpty(user.getUserIdentity()) && user.getUserIdentity().equals(CommonConstant.USER_IDENTITY_2) ){ +// deptIds = sysDepartService.getMySubDepIdsByDepId(user.getDepartIds()); +// }else{ +// return Result.ok(null); +// } +// }else{ +// deptIds = sysDepartService.getSubDepIdsByDepId(deptId); +// } +// queryWrapper.in("depart_id",deptIds); + + //我的部门,选中部门只能看当前部门下的角色 + // 代码逻辑说明: [QQYUN-10775]验证码可以复用 #7674------------ + if(oConvertUtils.isNotEmpty(deptId)){ + queryWrapper.eq("depart_id",deptId); + IPage pageList = sysDepartRoleService.page(page, queryWrapper); + return Result.ok(pageList); + }else{ + return Result.ok(null); + } + } + + /** + * 添加 + * + * @param sysDepartRole + * @return + */ + @RequiresPermissions("system:depart:role:add") + @Operation(summary="部门角色-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody SysDepartRole sysDepartRole) { + sysDepartRoleService.save(sysDepartRole); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param sysDepartRole + * @return + */ + @Operation(summary="部门角色-编辑") + @RequiresPermissions("system:depart:role:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody SysDepartRole sysDepartRole) { + sysDepartRoleService.updateById(sysDepartRole); + return Result.ok("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "部门角色-通过id删除") + @Operation(summary="部门角色-通过id删除") + @RequiresPermissions("system:depart:role:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + sysDepartRoleService.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "部门角色-批量删除") + @Operation(summary="部门角色-批量删除") + @RequiresPermissions("system:depart:role:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.sysDepartRoleService.deleteDepartRole(Arrays.asList(ids.split(","))); + //this.sysDepartRoleService.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @Operation(summary="部门角色-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + SysDepartRole sysDepartRole = sysDepartRoleService.getById(id); + return Result.ok(sysDepartRole); + } + + /** + * 获取部门下角色 + * @param departId + * @return + */ + @RequestMapping(value = "/getDeptRoleList", method = RequestMethod.GET) + public Result> getDeptRoleList(@RequestParam(value = "departId") String departId,@RequestParam(value = "userId") String userId){ + Result> result = new Result<>(); + //查询选中部门的角色 + List deptRoleList = sysDepartRoleService.list(new LambdaQueryWrapper().eq(SysDepartRole::getDepartId,departId)); + result.setSuccess(true); + result.setResult(deptRoleList); + return result; + } + + /** + * 设置 + * @param json + * @return + */ + @RequiresPermissions("system:depart:role:userAdd") + @RequestMapping(value = "/deptRoleUserAdd", method = RequestMethod.POST) + public Result deptRoleAdd(@RequestBody JSONObject json) { + String newRoleId = json.getString("newRoleId"); + String oldRoleId = json.getString("oldRoleId"); + String userId = json.getString("userId"); + departRoleUserService.deptRoleUserAdd(userId,newRoleId,oldRoleId); + // 代码逻辑说明: [VUEN-234]部门角色分配添加敏感日志------------ + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + baseCommonService.addLog("给部门用户ID:"+userId+"分配角色,操作人: " +loginUser.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + return Result.ok("添加成功!"); + } + + /** + * 根据用户id获取已设置部门角色 + * @param userId + * @return + */ + @RequestMapping(value = "/getDeptRoleByUserId", method = RequestMethod.GET) + public Result> getDeptRoleByUserId(@RequestParam(value = "userId") String userId,@RequestParam(value = "departId") String departId){ + Result> result = new Result<>(); + //查询部门下角色 + List roleList = sysDepartRoleService.list(new QueryWrapper().eq("depart_id",departId)); + List roleIds = roleList.stream().map(SysDepartRole::getId).collect(Collectors.toList()); + //根据角色id,用户id查询已授权角色 + List roleUserList = null; + if(roleIds!=null && roleIds.size()>0){ + roleUserList = departRoleUserService.list(new QueryWrapper().eq("user_id",userId).in("drole_id",roleIds)); + } + result.setSuccess(true); + result.setResult(roleUserList); + return result; + } + + /** + * 查询数据规则数据 + */ + @GetMapping(value = "/datarule/{permissionId}/{departId}/{roleId}") + public Result loadDatarule(@PathVariable("permissionId") String permissionId,@PathVariable("departId") String departId,@PathVariable("roleId") String roleId) { + //查询已授权的部门规则 + List list = sysDepartPermissionService.getPermRuleListByDeptIdAndPermId(departId,permissionId); + if(list==null || list.size()==0) { + return Result.error("未找到权限配置信息"); + }else { + Map map = new HashMap(5); + map.put("datarule", list); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysDepartRolePermission::getPermissionId, permissionId) + .eq(SysDepartRolePermission::getRoleId,roleId); + SysDepartRolePermission sysRolePermission = sysDepartRolePermissionService.getOne(query); + if(sysRolePermission==null) { + //return Result.error("未找到角色菜单配置信息"); + }else { + String drChecked = sysRolePermission.getDataRuleIds(); + if(oConvertUtils.isNotEmpty(drChecked)) { + map.put("drChecked", drChecked.endsWith(",")?drChecked.substring(0, drChecked.length()-1):drChecked); + } + } + return Result.ok(map); + //TODO 以后按钮权限的查询也走这个请求 无非在map中多加两个key + } + } + + /** + * 保存数据规则至角色菜单关联表 + */ + @PostMapping(value = "/datarule") + public Result saveDatarule(@RequestBody JSONObject jsonObject) { + try { + String permissionId = jsonObject.getString("permissionId"); + String roleId = jsonObject.getString("roleId"); + String dataRuleIds = jsonObject.getString("dataRuleIds"); + log.info("保存数据规则>>"+"菜单ID:"+permissionId+"角色ID:"+ roleId+"数据权限ID:"+dataRuleIds); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysDepartRolePermission::getPermissionId, permissionId) + .eq(SysDepartRolePermission::getRoleId,roleId); + SysDepartRolePermission sysRolePermission = sysDepartRolePermissionService.getOne(query); + if(sysRolePermission==null) { + return Result.error("请先保存角色菜单权限!"); + }else { + sysRolePermission.setDataRuleIds(dataRuleIds); + this.sysDepartRolePermissionService.updateById(sysRolePermission); + } + } catch (Exception e) { + log.error("SysRoleController.saveDatarule()发生异常:" + e.getMessage(),e); + return Result.error("保存失败"); + } + return Result.ok("保存成功!"); + } + + /** + * 导出excel + * + * @param request + * @param sysDepartRole + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysDepartRole sysDepartRole) { + return super.exportXls(request, sysDepartRole, SysDepartRole.class, "部门角色"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysDepartRole.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDictController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDictController.java new file mode 100644 index 0000000..cafbf0c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDictController.java @@ -0,0 +1,860 @@ +package com.ghb.base.modules.system.controller; +import org.jeecg.common.util.RedisUtil; + + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.subject.Subject; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.config.TenantContext; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.vo.DictModel; +import com.ghb.base.common.system.vo.DictQuery; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.*; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.config.shiro.ShiroRealm; +import com.ghb.base.modules.system.constant.DefIndexConst; +import com.ghb.base.modules.system.entity.SysDict; +import com.ghb.base.modules.system.entity.SysDictItem; +import com.ghb.base.modules.system.model.SysDictTree; +import com.ghb.base.modules.system.model.TreeSelectModel; +import com.ghb.base.modules.system.service.ISysDictItemService; +import com.ghb.base.modules.system.service.ISysDictService; +import com.ghb.base.modules.system.vo.SysDictBatchVo; +import com.ghb.base.modules.system.vo.SysDictPage; +import com.ghb.base.modules.system.vo.lowapp.SysDictVo; +import org.jeecgframework.poi.excel.ExcelImportCheckUtil; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.*; + +/** + *

+ * 字典表 前端控制器 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +@RestController +@RequestMapping("/sys/dict") +@Slf4j +public class SysDictController { + + @Autowired + private ISysDictService sysDictService; + @Autowired + private ISysDictItemService sysDictItemService; + @Autowired + public RedisTemplate redisTemplate; + @Autowired + private RedisUtil redisUtil; + @Autowired + private ShiroRealm shiroRealm; + + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> queryPageList( + SysDict sysDict, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + // 查询关键字,模糊筛选code和name + @RequestParam(name = "keywords", required = false) String keywords, + HttpServletRequest req + ) { + Result> result = new Result>(); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysDict.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(),0)); + } + //------------------------------------------------------------------------------------------------ + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDict, req.getParameterMap()); + // 查询关键字,模糊筛选code和name + if (oConvertUtils.isNotEmpty(keywords)) { + queryWrapper.and(i -> i.like("dict_code", keywords).or().like("dict_name", keywords)); + } + + Page page = new Page<>(pageNo, pageSize); + IPage pageList = sysDictService.page(page, queryWrapper); + log.debug("查询当前页:"+pageList.getCurrent()); + log.debug("查询当前页数量:"+pageList.getSize()); + log.debug("查询结果数量:"+pageList.getRecords().size()); + log.debug("数据总数:"+pageList.getTotal()); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * @功能:获取树形字典数据 + * @param sysDict + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @SuppressWarnings("unchecked") + @RequestMapping(value = "/treeList", method = RequestMethod.GET) + public Result> treeList(SysDict sysDict,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) { + Result> result = new Result<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + // 构造查询条件 + String dictName = sysDict.getDictName(); + if(oConvertUtils.isNotEmpty(dictName)) { + query.like(true, SysDict::getDictName, dictName); + } + query.orderByDesc(true, SysDict::getCreateTime); + List list = sysDictService.list(query); + List treeList = new ArrayList<>(); + for (SysDict node : list) { + treeList.add(new SysDictTree(node)); + } + result.setSuccess(true); + result.setResult(treeList); + return result; + } + + /** + * 获取全部字典数据 + * + * @return + */ + @RequestMapping(value = "/queryAllDictItems", method = RequestMethod.GET) + public Result queryAllDictItems(HttpServletRequest request) { + Map> res = new HashMap(5); + res = sysDictService.queryAllDictItems(); + return Result.ok(res); + } + + /** + * 获取字典数据 + * @param dictCode + * @return + */ + @RequestMapping(value = "/getDictText/{dictCode}/{key}", method = RequestMethod.GET) + public Result getDictText(@PathVariable("dictCode") String dictCode, @PathVariable("key") String key) { + log.info(" dictCode : "+ dictCode); + Result result = new Result(); + String text = null; + try { + text = sysDictService.queryDictTextByKey(dictCode, key); + result.setSuccess(true); + result.setResult(text); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败"); + return result; + } + return result; + } + + + /** + * 获取字典数据 【接口签名验证】 + * @param dictCode 字典code + * @param dictCode 表名,文本字段,code字段 | 举例:sys_user,realname,id + * @return + */ + @RequestMapping(value = "/getDictItems/{dictCode}", method = RequestMethod.GET) + public Result> getDictItems(@PathVariable("dictCode") String dictCode, @RequestParam(value = "sign",required = false) String sign,HttpServletRequest request) { + log.debug(" dictCode : "+ dictCode); + Result> result = new Result>(); + try { + List ls = sysDictService.getDictItems(dictCode); + if (ls == null) { + result.error500("字典Code格式不正确!"); + return result; + } + result.setSuccess(true); + result.setResult(ls); + log.debug(result.toString()); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + return result; + } + return result; + } + + /** + * 【接口签名验证】 + * 【JSearchSelectTag下拉搜索组件专用接口】 + * 大数据量的字典表 走异步加载 即前端输入内容过滤数据 + * @param dictCode 字典code格式:table,text,code + * @return + */ + @RequestMapping(value = "/loadDict/{dictCode}", method = RequestMethod.GET) + public Result> loadDict(@PathVariable("dictCode") String dictCode, + @RequestParam(name="keyword",required = false) String keyword, + @RequestParam(value = "sign",required = false) String sign, + @RequestParam(name = "pageNo", defaultValue = "1", required = false) Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10", required = false) Integer pageSize) { + + // 代码逻辑说明: /issues/4905 因为中括号(%5)的问题导致的 表单生成器字段配置时,选择关联字段,在进行高级配置时,无法加载数据库列表,提示 Sgin签名校验错误! #4905 RouteToRequestUrlFilter + if(keyword!=null && keyword.indexOf("%5")>=0){ + try { + keyword = URLDecoder.decode(keyword, "UTF-8"); + } catch (UnsupportedEncodingException e) { + log.error("下拉搜索关键字解码失败", e); + } + } + + log.info(" 加载字典表数据,加载关键字: "+ keyword); + Result> result = new Result>(); + try { + List ls = sysDictService.loadDict(dictCode, keyword, pageNo,pageSize); + if (ls == null) { + result.error500("字典Code格式不正确!"); + return result; + } + result.setSuccess(true); + result.setResult(ls); + log.info(result.toString()); + return result; + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败:" + e.getMessage()); + return result; + } + } + + /** + * 【接口签名验证】 + * 【给表单设计器的表字典使用】下拉搜索模式,有值时动态拼接数据 + * @param dictCode + * @param keyword 当前控件的值,可以逗号分割 + * @param sign + * @param pageSize + * @return + */ + @RequestMapping(value = "/loadDictOrderByValue/{dictCode}", method = RequestMethod.GET) + public Result> loadDictOrderByValue( + @PathVariable("dictCode") String dictCode, + @RequestParam(name = "keyword") String keyword, + @RequestParam(value = "sign", required = false) String sign, + @RequestParam(value = "pageSize", required = false) Integer pageSize) { + // 首次查询查出来用户选中的值,并且不分页 + Result> firstRes = this.loadDict(dictCode, keyword, sign,null, null); + if (!firstRes.isSuccess()) { + return firstRes; + } + // 然后再查询出第一页的数据 + Result> result = this.loadDict(dictCode, "", sign,1, pageSize); + if (!result.isSuccess()) { + return result; + } + // 合并两次查询的数据 + List firstList = firstRes.getResult(); + List list = result.getResult(); + for (DictModel firstItem : firstList) { + // anyMatch 表示:判断的条件里,任意一个元素匹配成功,返回true + // allMatch 表示:判断条件里的元素,所有的都匹配成功,返回true + // noneMatch 跟 allMatch 相反,表示:判断条件里的元素,所有的都匹配失败,返回true + boolean none = list.stream().noneMatch(item -> item.getValue().equals(firstItem.getValue())); + // 当元素不存在时,再添加到集合里 + if (none) { + list.add(0, firstItem); + } + } + return result; + } + + /** + * 【接口签名验证】 + * 根据字典code加载字典text 返回 + * @param dictCode 顺序:tableName,text,code + * @param keys 要查询的key + * @param sign + * @param delNotExist 是否移除不存在的项,默认为true,设为false如果某个key不存在数据库中,则直接返回key本身 + * @param request + * @return + */ + @RequestMapping(value = "/loadDictItem/{dictCode}", method = RequestMethod.GET) + public Result> loadDictItem(@PathVariable("dictCode") String dictCode,@RequestParam(name="key") String keys, @RequestParam(value = "sign",required = false) String sign,@RequestParam(value = "delNotExist",required = false,defaultValue = "true") boolean delNotExist,HttpServletRequest request) { + Result> result = new Result<>(); + try { + if(dictCode.indexOf(SymbolConstant.COMMA)!=-1) { + String[] params = dictCode.split(SymbolConstant.COMMA); + if(params.length!=3) { + result.error500("字典Code格式不正确!"); + return result; + } + List texts = sysDictService.queryTableDictByKeys(params[0], params[1], params[2], keys, delNotExist); + + result.setSuccess(true); + result.setResult(texts); + log.info(result.toString()); + }else { + result.error500("字典Code格式不正确!"); + } + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败"); + return result; + } + + return result; + } + + /** + * 【接口签名验证】 + * 根据表名——显示字段-存储字段 pid 加载树形数据 + * @param hasChildField 是否叶子节点字段 + * @param converIsLeafVal 是否需要系统转换 是否叶子节点的值 (0标识不转换、1标准系统自动转换) + * @param tableName 表名 + * @param text label字段 + * @param code value 字段 + * @param condition 查询条件 ? + * + */ + @SuppressWarnings("unchecked") + @RequestMapping(value = "/loadTreeData", method = RequestMethod.GET) + public Result> loadTreeData(@RequestParam(name="pid",required = false) String pid,@RequestParam(name="pidField") String pidField, + @RequestParam(name="tableName") String tableName, + @RequestParam(name="text") String text, + @RequestParam(name="code") String code, + @RequestParam(name="hasChildField") String hasChildField, + @RequestParam(name="converIsLeafVal",defaultValue ="1") int converIsLeafVal, + @RequestParam(name="condition") String condition, + @RequestParam(value = "sign",required = false) String sign,HttpServletRequest request) { + Result> result = new Result>(); + + // 【QQYUN-9207】防止参数为空导致报错 + if (oConvertUtils.isEmpty(tableName) || oConvertUtils.isEmpty(text) || oConvertUtils.isEmpty(code)) { + result.error500("字典Code格式不正确!"); + return result; + } + + // 1.获取查询条件参数 + Map query = null; + if(oConvertUtils.isNotEmpty(condition)) { + query = JSON.parseObject(condition, Map.class); + } + + // 2.返回查询结果 + List ls = sysDictService.queryTreeList(query,tableName, text, code, pidField, pid,hasChildField,converIsLeafVal); + result.setSuccess(true); + result.setResult(ls); + return result; + } + + /** + * 【APP接口】根据字典配置查询表字典数据(目前暂未找到调用的地方) + * @param query + * @param pageNo + * @param pageSize + * @return + */ + @Deprecated + @GetMapping("/queryTableData") + public Result> queryTableData(DictQuery query, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(value = "sign",required = false) String sign,HttpServletRequest request){ + Result> res = new Result>(); + List ls = this.sysDictService.queryDictTablePageList(query,pageSize,pageNo); + res.setResult(ls); + res.setSuccess(true); + return res; + } + + /** + * @功能:新增 + * @param sysDict + * @return + */ + @RequiresPermissions("system:dict:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + public Result add(@RequestBody SysDict sysDict) { + Result result = new Result(); + try { + sysDict.setCreateTime(new Date()); + sysDict.setDelFlag(CommonConstant.DEL_FLAG_0); + sysDictService.save(sysDict); + result.success("保存成功!"); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败"); + } + return result; + } + + /** + * @功能:字典和字典项一起新增(支持批量) + * @param sysDictBatchVo 字典批量数据 + * @return + */ + @RequiresPermissions("system:dict:add") + @RequestMapping(value = "/batchAddDictWithItems", method = RequestMethod.POST) + public Result> batchAddDictWithItems(@RequestBody SysDictBatchVo sysDictBatchVo) { + Result> result = new Result>(); + //update-begin---author:zzl ---date:2026-04-03 for:字典和字典项一起新增(支持批量)--- + log.info("========== 批量新增字典开始 =========="); + log.info("请求参数: {}", JSON.toJSONString(sysDictBatchVo)); + if (sysDictBatchVo == null || sysDictBatchVo.getDictList() == null || sysDictBatchVo.getDictList().isEmpty()) { + log.warn("字典列表为空,参数校验不通过"); + result.error500("字典列表不能为空!"); + return result; + } + int successCount = 0; + int failCount = 0; + StringBuilder message = new StringBuilder(); + List> failList = new ArrayList<>(); + log.info("待处理的字典数量: {}", sysDictBatchVo.getDictList().size()); + for (int i = 0; i < sysDictBatchVo.getDictList().size(); i++) { + SysDictPage sysDictPage = sysDictBatchVo.getDictList().get(i); + log.info("开始处理第 {} 个字典, dictCode: {}, dictName: {}", i + 1, sysDictPage.getDictCode(), sysDictPage.getDictName()); + try { + SysDict sysDict = new SysDict(); + sysDict.setDictName(sysDictPage.getDictName()); + sysDict.setDictCode(sysDictPage.getDictCode()); + sysDict.setDescription(sysDictPage.getDescription()); + sysDict.setDelFlag(CommonConstant.DEL_FLAG_0); + Integer num = sysDictService.saveMain(sysDict, sysDictPage.getSysDictItemList()); + if (num > 0) { + successCount++; + log.info("第 {} 个字典[{}]保存成功", i + 1, sysDictPage.getDictCode()); + } else if (num == -1) { + failCount++; + Map failItem = new HashMap<>(); + failItem.put("dictCode", sysDictPage.getDictCode()); + failItem.put("dictName", sysDictPage.getDictName()); + failItem.put("errorMsg", "字典项值为空,已忽略!"); + failList.add(failItem); + message.append("第").append(i + 1).append("个字典[").append(sysDictPage.getDictCode()).append("]:字典项值为空,已忽略!\n"); + log.warn("第 {} 个字典[{}]字典项值为空,已忽略", i + 1, sysDictPage.getDictCode()); + } else { + failCount++; + Map failItem = new HashMap<>(); + failItem.put("dictCode", sysDictPage.getDictCode()); + failItem.put("dictName", sysDictPage.getDictName()); + failItem.put("errorMsg", "字典编码已经存在!"); + failList.add(failItem); + message.append("第").append(i + 1).append("个字典[").append(sysDictPage.getDictCode()).append("]:字典编码已经存在!\n"); + log.warn("第 {} 个字典[{}]字典编码已经存在", i + 1, sysDictPage.getDictCode()); + } + } catch (Exception e) { + failCount++; + Map failItem = new HashMap<>(); + failItem.put("dictCode", sysDictPage.getDictCode()); + failItem.put("dictName", sysDictPage.getDictName()); + failItem.put("errorMsg", e.getMessage()); + failList.add(failItem); + message.append("第").append(i + 1).append("个字典[").append(sysDictPage.getDictCode()).append("]:").append(e.getMessage()).append("\n"); + log.error("第 {} 个字典[{}]处理异常: {}", i + 1, sysDictPage.getDictCode(), e.getMessage(), e); + } + } + Map returnMap = new HashMap<>(); + returnMap.put("successCount", successCount); + returnMap.put("failCount", failCount); + returnMap.put("message", message.toString()); + returnMap.put("failList", failList); + if (failCount == 0) { + result.success("批量保存成功!共保存 " + successCount + " 个字典!"); + log.info("批量保存成功,共保存 {} 个字典", successCount); + } else if (successCount > 0) { + result.success("部分保存成功!成功 " + successCount + " 个,失败 " + failCount + " 个!"); + log.warn("部分保存成功,成功 {} 个,失败 {} 个", successCount, failCount); + } else { + result.error500("全部保存失败!"); + log.error("全部保存失败!共 {} 个字典", sysDictBatchVo.getDictList().size()); + } + + + result.setResult(returnMap); + log.info("========== 批量新增字典结束 =========="); + //update-end---author:zzl ---date:2026-04-03 for:字典和字典项一起新增(支持批量)--- + return result; + } + + /** + * @功能:编辑 + * @param sysDict + * @return + */ + @RequiresPermissions("system:dict:edit") + @RequestMapping(value = "/edit", method = { RequestMethod.PUT,RequestMethod.POST }) + public Result edit(@RequestBody SysDict sysDict) { + Result result = new Result(); + SysDict sysdict = sysDictService.getById(sysDict.getId()); + if(sysdict==null) { + result.error500("未找到对应实体"); + }else { + sysDict.setUpdateTime(new Date()); + boolean ok = sysDictService.updateById(sysDict); + if(ok) { + result.success("编辑成功!"); + } + } + return result; + } + + /** + * @功能:删除 + * @param id + * @return + */ + @RequiresPermissions("system:dict:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + @CacheEvict(value={CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true) + public Result delete(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + boolean ok = sysDictService.removeById(id); + if(ok) { + result.success("删除成功!"); + }else{ + result.error500("删除失败!"); + } + return result; + } + + /** + * @功能:批量删除 + * @param ids + * @return + */ + @RequiresPermissions("system:dict:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + @CacheEvict(value= {CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true) + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + Result result = new Result(); + if(oConvertUtils.isEmpty(ids)) { + result.error500("参数不识别!"); + }else { + sysDictService.removeByIds(Arrays.asList(ids.split(","))); + result.success("删除成功!"); + } + return result; + } + + /** + * @功能:刷新缓存 + * @return + */ + @RequestMapping(value = "/refleshCache") + public Result refleshCache() { + Result result = new Result(); + //清空字典缓存 +// Set keys = redisTemplate.keys(CacheConstant.SYS_DICT_CACHE + "*"); +// Set keys7 = redisTemplate.keys(CacheConstant.SYS_ENABLE_DICT_CACHE + "*"); +// Set keys2 = redisTemplate.keys(CacheConstant.SYS_DICT_TABLE_CACHE + "*"); +// Set keys21 = redisTemplate.keys(CacheConstant.SYS_DICT_TABLE_BY_KEYS_CACHE + "*"); +// Set keys3 = redisTemplate.keys(CacheConstant.SYS_DEPARTS_CACHE + "*"); +// Set keys4 = redisTemplate.keys(CacheConstant.SYS_DEPART_IDS_CACHE + "*"); +// Set keys5 = redisTemplate.keys( "jmreport:cache:dict*"); +// Set keys6 = redisTemplate.keys( "jmreport:cache:dictTable*"); +// redisTemplate.delete(keys); +// redisTemplate.delete(keys2); +// redisTemplate.delete(keys21); +// redisTemplate.delete(keys3); +// redisTemplate.delete(keys4); +// redisTemplate.delete(keys5); +// redisTemplate.delete(keys6); +// redisTemplate.delete(keys7); + + // 代码逻辑说明: [issue/4358]springCache中的清除缓存的操作使用了“keys” + redisUtil.removeAll(CacheConstant.SYS_DICT_CACHE); + redisUtil.removeAll(CacheConstant.SYS_ENABLE_DICT_CACHE); + redisUtil.removeAll(CacheConstant.SYS_DICT_TABLE_CACHE); + redisUtil.removeAll(CacheConstant.SYS_DICT_TABLE_BY_KEYS_CACHE); + redisUtil.removeAll(CacheConstant.SYS_DEPARTS_CACHE); + redisUtil.removeAll(CacheConstant.SYS_DEPART_IDS_CACHE); + redisUtil.removeAll("jmreport:cache:dict"); + redisUtil.removeAll("jmreport:cache:dictTable"); + + // 清除当前用户的授权缓存信息 + Subject currentUser = SecurityUtils.getSubject(); + if (currentUser.isAuthenticated()) { + shiroRealm.clearCache(currentUser.getPrincipals()); + } + + // 清空默认首页缓存(开源版和商业版会串) + redisUtil.del(DefIndexConst.CACHE_KEY + "::" + DefIndexConst.DEF_INDEX_ALL); + return result; + } + + /** + * 导出excel + * + * @param request + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(SysDict sysDict,HttpServletRequest request) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysDict.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + + // Step.1 组装查询条件 + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDict, request.getParameterMap()); + //Step.2 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + String selections = request.getParameter("selections"); + if(!oConvertUtils.isEmpty(selections)){ + queryWrapper.in("id",selections.split(",")); + } + List pageList = new ArrayList(); + + List sysDictList = sysDictService.list(queryWrapper); + for (SysDict dictMain : sysDictList) { + SysDictPage vo = new SysDictPage(); + BeanUtils.copyProperties(dictMain, vo); + // 查询机票 + List sysDictItemList = sysDictItemService.selectItemsByMainId(dictMain.getId()); + vo.setSysDictItemList(sysDictItemList); + pageList.add(vo); + } + + // 导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "数据字典"); + // 注解对象Class + mv.addObject(NormalExcelConstants.CLASS, SysDictPage.class); + // 自定义表格参数 + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("数据字典列表", "导出人:"+user.getRealname(), "数据字典", ExcelType.XSSF)); + // 导出数据列表 + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param + * @return + */ + @RequiresPermissions("system:dict:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(2); + params.setNeedSave(true); + try { + //导入Excel格式校验,看匹配的字段文本概率 + Boolean t = ExcelImportCheckUtil.check(file.getInputStream(), SysDictPage.class, params); + if(t!=null && !t){ + throw new RuntimeException("导入Excel校验失败 !"); + } + List list = ExcelImportUtil.importExcel(file.getInputStream(), SysDictPage.class, params); + // 错误信息 + List errorMessage = new ArrayList<>(); + int successLines = 0, errorLines = 0; + for (int i=0;i< list.size();i++) { + SysDict po = new SysDict(); + BeanUtils.copyProperties(list.get(i), po); + po.setDelFlag(CommonConstant.DEL_FLAG_0); + try { + Integer integer = sysDictService.saveMain(po, list.get(i).getSysDictItemList()); + if(integer>0){ + successLines++; + // 代码逻辑说明: [JTC-1168]如果字典项值为空,则字典项忽略导入------------ + }else if(integer == -1){ + errorLines++; + errorMessage.add("字典名称:" + po.getDictName() + ",对应字典列表的字典项值不能为空,忽略导入。"); + }else{ + errorLines++; + int lineNumber = i + 1; + // 代码逻辑说明: [JTC-1168]字典编号不能为空------------ + if(oConvertUtils.isEmpty(po.getDictCode())){ + errorMessage.add("第 " + lineNumber + " 行:字典编码不能为空,忽略导入。"); + }else{ + errorMessage.add("第 " + lineNumber + " 行:字典编码已经存在,忽略导入。"); + } + } + } catch (Exception e) { + errorLines++; + int lineNumber = i + 1; + errorMessage.add("第 " + lineNumber + " 行:字典编码已经存在,忽略导入。"); + } + } + return ImportExcelUtil.imporReturnRes(errorLines,successLines,errorMessage); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + return Result.error("文件导入失败!"); + } + + + /** + * 查询被删除的列表 + * @return + */ + @RequestMapping(value = "/deleteList", method = RequestMethod.GET) + public Result> deleteList(HttpServletRequest request) { + Result> result = new Result>(); + String tenantId = TokenUtils.getTenantIdByRequest(request); + List list = this.sysDictService.queryDeleteList(tenantId); + result.setSuccess(true); + result.setResult(list); + return result; + } + + /** + * 物理删除 + * @param id + * @return + */ + @RequestMapping(value = "/deletePhysic/{id}", method = RequestMethod.DELETE) + public Result deletePhysic(@PathVariable("id") String id) { + try { + sysDictService.deleteOneDictPhysically(id); + return Result.ok("删除成功!"); + } catch (Exception e) { + e.printStackTrace(); + return Result.error("删除失败!"); + } + } + + /** + * 逻辑删除的字段,进行取回 + * @param id + * @return + */ + @RequestMapping(value = "/back/{id}", method = RequestMethod.PUT) + public Result back(@PathVariable("id") String id) { + try { + sysDictService.updateDictDelFlag(0,id); + return Result.ok("操作成功!"); + } catch (Exception e) { + e.printStackTrace(); + return Result.error("操作失败!"); + } + } + /** + * 还原被逻辑删除的用户 + * + * @param jsonObject + * @return + */ + @RequestMapping(value = "/putRecycleBin", method = RequestMethod.PUT) + public Result putRecycleBin(@RequestBody JSONObject jsonObject, HttpServletRequest request) { + try { + String ids = jsonObject.getString("ids"); + if (StringUtils.isNotBlank(ids)) { + sysDictService.revertLogicDeleted(Arrays.asList(ids.split(","))); + return Result.ok("操作成功!"); + } + } catch (Exception e) { + e.printStackTrace(); + return Result.error("操作失败!"); + } + return Result.ok("还原成功"); + } + /** + * 彻底删除字典 + * + * @param ids 被删除的字典ID,多个id用半角逗号分割 + * @return + */ + @RequiresPermissions("system:dict:deleteRecycleBin") + @RequestMapping(value = "/deleteRecycleBin", method = RequestMethod.DELETE) + public Result deleteRecycleBin(@RequestParam("ids") String ids) { + try { + if (StringUtils.isNotBlank(ids)) { + sysDictService.removeLogicDeleted(Arrays.asList(ids.split(","))); + } + return Result.ok("删除成功!"); + } catch (Exception e) { + e.printStackTrace(); + return Result.error("删除失败!"); + } + } + + /** + * VUEN-2584【issue】平台sql注入漏洞几个问题 + * 部分特殊函数 可以将查询结果混夹在错误信息中,导致数据库的信息暴露 + * @param e + * @return + */ + @ExceptionHandler(java.sql.SQLException.class) + public Result handleSQLException(Exception e){ + String msg = e.getMessage(); + String extractvalue = "extractvalue"; + String updatexml = "updatexml"; + if(msg!=null && (msg.toLowerCase().indexOf(extractvalue)>=0 || msg.toLowerCase().indexOf(updatexml)>=0)){ + return Result.error("校验失败,sql解析异常!"); + } + return Result.error("校验失败,sql解析异常!" + msg); + } + + /** + * 根据应用id获取字典列表和详情 + * @param request + */ + @GetMapping("/getDictListByLowAppId") + public Result> getDictListByLowAppId(HttpServletRequest request){ + String lowAppId = oConvertUtils.getString(TokenUtils.getLowAppIdByRequest(request)); + List list = sysDictService.getDictListByLowAppId(lowAppId); + return Result.ok(list); + } + + /** + * 添加字典 + * @param sysDictVo + * @param request + * @return + */ + @PostMapping("/addDictByLowAppId") + public Result addDictByLowAppId(@RequestBody SysDictVo sysDictVo,HttpServletRequest request){ + String lowAppId = oConvertUtils.getString(TokenUtils.getLowAppIdByRequest(request)); + String tenantId = oConvertUtils.getString(TokenUtils.getTenantIdByRequest(request)); + sysDictVo.setLowAppId(lowAppId); + sysDictVo.setTenantId(oConvertUtils.getInteger(tenantId, null)); + sysDictService.addDictByLowAppId(sysDictVo); + return Result.ok("添加成功"); + } + + @PutMapping("/editDictByLowAppId") + public Result editDictByLowAppId(@RequestBody SysDictVo sysDictVo,HttpServletRequest request){ + String lowAppId = oConvertUtils.getString(TokenUtils.getLowAppIdByRequest(request)); + sysDictVo.setLowAppId(lowAppId); + sysDictService.editDictByLowAppId(sysDictVo); + return Result.ok("编辑成功"); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDictItemController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDictItemController.java new file mode 100644 index 0000000..4a62826 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysDictItemController.java @@ -0,0 +1,186 @@ +package com.ghb.base.modules.system.controller; + + +import java.util.Arrays; +import java.util.Date; + +import jakarta.servlet.http.HttpServletRequest; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysDictItem; +import com.ghb.base.modules.system.service.ISysDictItemService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + +/** + *

+ * 前端控制器 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +@Tag(name = "数据字典") +@RestController +@RequestMapping("/sys/dictItem") +@Slf4j +public class SysDictItemController { + + @Autowired + private ISysDictItemService sysDictItemService; + + /** + * @功能:查询字典数据 + * @param sysDictItem + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> queryPageList(SysDictItem sysDictItem,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) { + Result> result = new Result>(); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDictItem, req.getParameterMap()); + queryWrapper.orderByAsc("sort_order"); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysDictItemService.page(page, queryWrapper); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * @功能:新增 + * @return + */ + @RequiresPermissions("system:dict:item:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + @CacheEvict(value= {CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true) + public Result add(@RequestBody SysDictItem sysDictItem) { + Result result = new Result(); + try { + sysDictItem.setCreateTime(new Date()); + sysDictItemService.save(sysDictItem); + result.success("保存成功!"); + } catch (Exception e) { + log.error(e.getMessage(),e); + result.error500("操作失败"); + } + return result; + } + + /** + * @功能:编辑 + * @param sysDictItem + * @return + */ + @RequiresPermissions("system:dict:item:edit") + @RequestMapping(value = "/edit", method = { RequestMethod.PUT,RequestMethod.POST }) + @CacheEvict(value={CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true) + public Result edit(@RequestBody SysDictItem sysDictItem) { + Result result = new Result(); + SysDictItem sysdict = sysDictItemService.getById(sysDictItem.getId()); + if(sysdict==null) { + result.error500("未找到对应实体"); + }else { + sysDictItem.setUpdateTime(new Date()); + boolean ok = sysDictItemService.updateById(sysDictItem); + //TODO 返回false说明什么? + if(ok) { + result.success("编辑成功!"); + } + } + return result; + } + + /** + * @功能:删除字典数据 + * @param id + * @return + */ + @RequiresPermissions("system:dict:item:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + @CacheEvict(value={CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true) + public Result delete(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysDictItem joinSystem = sysDictItemService.getById(id); + if(joinSystem==null) { + result.error500("未找到对应实体"); + }else { + boolean ok = sysDictItemService.removeById(id); + if(ok) { + result.success("删除成功!"); + } + } + return result; + } + + /** + * @功能:批量删除字典数据 + * @param ids + * @return + */ + @RequiresPermissions("system:dict:item:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + @CacheEvict(value={CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries=true) + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + Result result = new Result(); + if(ids==null || "".equals(ids.trim())) { + result.error500("参数不识别!"); + }else { + this.sysDictItemService.removeByIds(Arrays.asList(ids.split(","))); + result.success("删除成功!"); + } + return result; + } + + /** + * 字典值重复校验 + * @param sysDictItem + * @param request + * @return + */ + @RequestMapping(value = "/dictItemCheck", method = RequestMethod.GET) + @Operation(summary="字典重复校验接口") + public Result doDictItemCheck(SysDictItem sysDictItem, HttpServletRequest request) { + Long num = Long.valueOf(0); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); + queryWrapper.eq(SysDictItem::getItemValue,sysDictItem.getItemValue()); + queryWrapper.eq(SysDictItem::getDictId,sysDictItem.getDictId()); + if (StringUtils.isNotBlank(sysDictItem.getId())) { + // 编辑页面校验 + queryWrapper.ne(SysDictItem::getId,sysDictItem.getId()); + } + num = sysDictItemService.count(queryWrapper); + if (num == 0) { + // 该值可用 + return Result.ok("该值可用!"); + } else { + // 该值不可用 + log.info("该值不可用,系统中已存在!"); + return Result.error("该值不可用,系统中已存在!"); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysFillRuleController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysFillRuleController.java new file mode 100644 index 0000000..e4cd374 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysFillRuleController.java @@ -0,0 +1,219 @@ +package com.ghb.base.modules.system.controller; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.util.FillRuleUtil; +import com.ghb.base.modules.system.entity.SysFillRule; +import com.ghb.base.modules.system.service.ISysFillRuleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; + +/** + * @Description: 填值规则 + * @Author: Ghb-boot + * @Date: 2019-11-07 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "填值规则") +@RestController +@RequestMapping("/sys/fillRule") +public class SysFillRuleController extends GhbController { + @Autowired + private ISysFillRuleService sysFillRuleService; + + /** + * 分页列表查询 + * + * @param sysFillRule + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "填值规则-分页列表查询") + @Operation(summary = "填值规则-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(SysFillRule sysFillRule, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysFillRule, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = sysFillRuleService.page(page, queryWrapper); + return Result.ok(pageList); + } + + /** + * 测试 ruleCode + * + * @param ruleCode + * @return + */ + @RequiresRoles({"admin"}) + @GetMapping(value = "/testFillRule") + public Result testFillRule(@RequestParam("ruleCode") String ruleCode) { + Object result = FillRuleUtil.executeRule(ruleCode, new JSONObject()); + return Result.ok(result); + } + + /** + * 添加 + * + * @param sysFillRule + * @return + */ + @AutoLog(value = "填值规则-添加") + @Operation(summary = "填值规则-添加") + @RequiresRoles({"admin"}) + @PostMapping(value = "/add") + public Result add(@RequestBody SysFillRule sysFillRule) { + sysFillRuleService.save(sysFillRule); + return Result.ok("添加成功!"); + } + + /** + * 编辑 + * + * @param sysFillRule + * @return + */ + @AutoLog(value = "填值规则-编辑") + @Operation(summary = "填值规则-编辑") + @RequiresRoles({"admin"}) + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody SysFillRule sysFillRule) { + sysFillRuleService.updateById(sysFillRule); + return Result.ok("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "填值规则-通过id删除") + @Operation(summary = "填值规则-通过id删除") + @RequiresRoles({"admin"}) + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysFillRuleService.removeById(id); + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "填值规则-批量删除") + @Operation(summary = "填值规则-批量删除") + @RequiresRoles({"admin"}) + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + this.sysFillRuleService.removeByIds(Arrays.asList(ids.split(","))); + return Result.ok("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "填值规则-通过id查询") + @Operation(summary = "填值规则-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysFillRule sysFillRule = sysFillRuleService.getById(id); + return Result.ok(sysFillRule); + } + + /** + * 导出excel + * + * @param request + * @param sysFillRule + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysFillRule sysFillRule) { + return super.exportXls(request, sysFillRule, SysFillRule.class, "填值规则"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysFillRule.class); + } + + /** + * 通过 ruleCode 执行自定义填值规则 + * + * @param ruleCode 要执行的填值规则编码 + * @param formData 表单数据,可根据表单数据的不同生成不同的填值结果 + * @return 运行后的结果 + */ + @PutMapping("/executeRuleByCode/{ruleCode}") + public Result executeByRuleCode(@PathVariable("ruleCode") String ruleCode, @RequestBody JSONObject formData) { + Object result = FillRuleUtil.executeRule(ruleCode, formData); + return Result.ok(result); + } + + + /** + * 批量通过 ruleCode 执行自定义填值规则 + * + * @param ruleData 要执行的填值规则JSON数组: + * 示例: { "commonFormData": {}, rules: [ { "ruleCode": "xxx", "formData": null } ] } + * @return 运行后的结果,返回示例: [{"ruleCode": "order_num_rule", "result": "CN2019111117212984"}] + * + */ + @PutMapping("/executeRuleByCodeBatch") + public Result executeByRuleCodeBatch(@RequestBody JSONObject ruleData) { + JSONObject commonFormData = ruleData.getJSONObject("commonFormData"); + JSONArray rules = ruleData.getJSONArray("rules"); + // 遍历 rules ,批量执行规则 + JSONArray results = new JSONArray(rules.size()); + for (int i = 0; i < rules.size(); i++) { + JSONObject rule = rules.getJSONObject(i); + String ruleCode = rule.getString("ruleCode"); + JSONObject formData = rule.getJSONObject("formData"); + // 如果没有传递 formData,就用common的 + if (formData == null) { + formData = commonFormData; + } + // 执行填值规则 + Object result = FillRuleUtil.executeRule(ruleCode, formData); + JSONObject obj = new JSONObject(rules.size()); + obj.put("ruleCode", ruleCode); + obj.put("result", result); + results.add(obj); + } + return Result.ok(results); + } + +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysFormFileController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysFormFileController.java new file mode 100644 index 0000000..14075cb --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysFormFileController.java @@ -0,0 +1,152 @@ +package com.ghb.base.modules.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.system.entity.SysFormFile; +import com.ghb.base.modules.system.service.ISysFormFileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; + +/** + * @Description: 表单评论文件 + * @Author: Ghb-boot + * @Date: 2022-07-21 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "表单评论文件") +@RestController +@RequestMapping("/sys/formFile") +public class SysFormFileController extends GhbController { + @Autowired + private ISysFormFileService sysFormFileService; + + /** + * 分页列表查询 + * + * @param sysFormFile + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "表单评论文件-分页列表查询") + @Operation(summary = "表单评论文件-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(SysFormFile sysFormFile, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysFormFile, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysFormFileService.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param sysFormFile + * @return + */ + @AutoLog(value = "表单评论文件-添加") + @Operation(summary = "表单评论文件-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody SysFormFile sysFormFile) { + sysFormFileService.save(sysFormFile); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param sysFormFile + * @return + */ + @AutoLog(value = "表单评论文件-编辑") + @Operation(summary = "表单评论文件-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody SysFormFile sysFormFile) { + sysFormFileService.updateById(sysFormFile); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "表单评论文件-通过id删除") + @Operation(summary = "表单评论文件-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysFormFileService.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "表单评论文件-批量删除") + @Operation(summary = "表单评论文件-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + this.sysFormFileService.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "表单评论文件-通过id查询") + @Operation(summary = "表单评论文件-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysFormFile sysFormFile = sysFormFileService.getById(id); + return Result.OK(sysFormFile); + } + + /** + * 导出excel + * + * @param request + * @param sysFormFile + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysFormFile sysFormFile) { + return super.exportXls(request, sysFormFile, SysFormFile.class, "表单评论文件"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysFormFile.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysGatewayRouteController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysGatewayRouteController.java new file mode 100644 index 0000000..b5eb4f0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysGatewayRouteController.java @@ -0,0 +1,149 @@ +package com.ghb.base.modules.system.controller; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysGatewayRoute; +import com.ghb.base.modules.system.service.ISysGatewayRouteService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.List; + +/** + * @Description: gateway路由管理 + * @Author: Ghb-boot + * @Date: 2020-05-26 + * @Version: V1.0 + */ +@Tag(name = "gateway路由管理") +@RestController +@RequestMapping("/sys/gatewayRoute") +@Slf4j +public class SysGatewayRouteController extends GhbController { + + @Autowired + private ISysGatewayRouteService sysGatewayRouteService; + + @RequiresPermissions("system:gateway:updateAll") + @PostMapping(value = "/updateAll") + public Result updateAll(@RequestBody JSONObject json) { + sysGatewayRouteService.updateAll(json); + return Result.ok("操作成功!"); + } + + @GetMapping(value = "/list") + public Result queryPageList(SysGatewayRoute sysGatewayRoute) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + List ls = sysGatewayRouteService.list(query); + JSONArray array = new JSONArray(); + for(SysGatewayRoute rt: ls){ + JSONObject obj = (JSONObject) JSONObject.toJSON(rt); + if(oConvertUtils.isNotEmpty(rt.getPredicates())){ + obj.put("predicates", JSONArray.parseArray(rt.getPredicates())); + } + if(oConvertUtils.isNotEmpty(rt.getFilters())){ + obj.put("filters", JSONArray.parseArray(rt.getFilters())); + } + array.add(obj); + } + return Result.ok(array); + } + + @GetMapping(value = "/clearRedis") + public Result clearRedis() { + sysGatewayRouteService.clearRedis(); + return Result.ok("清除成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @RequiresPermissions("system:getway:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysGatewayRouteService.deleteById(id); + return Result.ok("删除路由成功"); + } + + /** + * 查询被删除的列表 + * @return + */ + @RequestMapping(value = "/deleteList", method = RequestMethod.GET) + public Result> deleteList(HttpServletRequest request) { + Result> result = new Result<>(); + List list = sysGatewayRouteService.getDeletelist(); + result.setSuccess(true); + result.setResult(list); + return result; + } + + /** + * 还原被逻辑删除的路由 + * + * @param jsonObject + * @return + */ + @RequiresPermissions("system:gateway:putRecycleBin") + @RequestMapping(value = "/putRecycleBin", method = RequestMethod.PUT) + public Result putRecycleBin(@RequestBody JSONObject jsonObject, HttpServletRequest request) { + try { + String ids = jsonObject.getString("ids"); + if (StringUtils.isNotBlank(ids)) { + sysGatewayRouteService.revertLogicDeleted(Arrays.asList(ids.split(","))); + return Result.ok("操作成功!"); + } + } catch (Exception e) { + e.printStackTrace(); + return Result.error("操作失败!"); + } + return Result.ok("还原成功"); + } + /** + * 彻底删除路由 + * + * @param ids 被删除的路由ID,多个id用半角逗号分割 + * @return + */ + @RequiresPermissions("system:gateway:deleteRecycleBin") + @RequestMapping(value = "/deleteRecycleBin", method = RequestMethod.DELETE) + public Result deleteRecycleBin(@RequestParam("ids") String ids) { + try { + if (StringUtils.isNotBlank(ids)) { + sysGatewayRouteService.deleteLogicDeleted(Arrays.asList(ids.split(","))); + } + return Result.ok("删除成功!"); + } catch (Exception e) { + e.printStackTrace(); + return Result.error("删除失败!"); + } + } + /** + * 复制路由 + * + * @param id 路由id + * @return + */ + @RequiresPermissions("system:gateway:copyRoute") + @RequestMapping(value = "/copyRoute", method = RequestMethod.GET) + public Result copyRoute(@RequestParam(name = "id", required = true) String id, HttpServletRequest req) { + Result result = new Result<>(); + SysGatewayRoute sysGatewayRoute= sysGatewayRouteService.copyRoute(id); + result.setResult(sysGatewayRoute); + result.setSuccess(true); + return result; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysLogController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysLogController.java new file mode 100644 index 0000000..1b6e984 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysLogController.java @@ -0,0 +1,219 @@ +package com.ghb.base.modules.system.controller; + + +import java.util.Arrays; +import java.util.List; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.modules.system.entity.SysLog; +import com.ghb.base.modules.system.entity.SysRole; +import com.ghb.base.modules.system.service.ISysLogService; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecgframework.poi.handler.inter.IExcelExportServerEnhanced; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.ModelAndView; + +/** + *

+ * 系统日志表 前端控制器 + *

+ * + * @Author zhangweijian + * @since 2018-12-26 + */ +@RestController +@RequestMapping("/sys/log") +@Slf4j +public class SysLogController extends GhbController { + + @Autowired + private ISysLogService sysLogService; + + /** + * for [issues/8699]AutoPoi在使用@ExcelEntity当设置show=true并且该项为null时报错 + */ + @Resource + private GhbBaseConfig GhbBaseConfig; + + /** + * 全部清除 + */ + private static final String ALL_ClEAR = "allclear"; + + /** + * @功能:查询日志记录 + * @param syslog + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequestMapping(value = "/list", method = RequestMethod.GET) + //@RequiresPermissions("system:log:list") + public Result> queryPageList(SysLog syslog,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) { + Result> result = new Result>(); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(syslog, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + //日志关键词 + String keyWord = req.getParameter("keyWord"); + if(oConvertUtils.isNotEmpty(keyWord)) { + queryWrapper.like("log_content",keyWord); + } + //TODO 过滤逻辑处理 + //TODO begin、end逻辑处理 + //TODO 一个强大的功能,前端传一个字段字符串,后台只返回这些字符串对应的字段 + //创建时间/创建人的赋值 + IPage pageList = sysLogService.page(page, queryWrapper); + log.debug("查询当前页:"+pageList.getCurrent()); + log.debug("查询当前页数量:"+pageList.getSize()); + log.debug("查询结果数量:"+pageList.getRecords().size()); + log.debug("数据总数:"+pageList.getTotal()); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * @功能:删除单个日志记录 + * @param id + * @return + */ + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + //@RequiresPermissions("system:log:delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysLog sysLog = sysLogService.getById(id); + if(sysLog==null) { + result.error500("未找到对应实体"); + }else { + boolean ok = sysLogService.removeById(id); + if(ok) { + result.success("删除成功!"); + } + } + return result; + } + + /** + * @功能:批量,全部清空日志记录 + * @param ids + * @return + */ + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + //@RequiresPermissions("system:log:deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + Result result = new Result(); + if(ids==null || "".equals(ids.trim())) { + result.error500("参数不识别!"); + }else { + if(ALL_ClEAR.equals(ids)) { + this.sysLogService.removeAll(); + result.success("清除成功!"); + } + this.sysLogService.removeByIds(Arrays.asList(ids.split(","))); + result.success("删除成功!"); + } + return result; + } + + /** + * 导出excel + * for [QQYUN-13431]【Ghb】日志管理中添加大数据导出功能 + * @param request + * @param syslog + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysLog syslog) { + // 复制参数,移除排序相关键(column/order 等)防止前端传入排序影响导出顺序 + java.util.Map rawMap = request.getParameterMap(); + java.util.Map paramMap = new java.util.HashMap<>(rawMap); + // 剔除自定义排序参数 + paramMap.remove("column"); + paramMap.remove("order"); + // 组装查询条件(已剔除排序参数) + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(syslog, paramMap); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + // 过滤选中数据 + String selections = request.getParameter("selections"); + if (oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + queryWrapper.in("id", selectionList); + } + // 定义IExcelExportServer + IExcelExportServerEnhanced excelExportServer = new IExcelExportServerEnhanced<>() { + + @Override + public List selectListForExcelExport(Object queryParams, SysLog lastRecord, int pageSize) { + QueryWrapper originalWrapper = (QueryWrapper) queryParams; + // 克隆原始条件,避免多次迭代污染 + QueryWrapper batchWrapper = null; + try { + batchWrapper = (QueryWrapper) originalWrapper.clone(); + } catch (Exception e) { + batchWrapper = originalWrapper; + } + + String lastId = null; + if (lastRecord != null) { + lastId = lastRecord.getId(); + final String cursorLastId = lastId; + // 仅基于雪花ID(全局唯一,数值递增)作为游标,提升索引利用与性能 + // 条件:id < 上一批最后一条的ID,实现“从大到小”倒序分页 + batchWrapper.lt("id", cursorLastId); + } + + // 排序:按 id DESC(雪花ID递增,倒序可获取最新数据) + batchWrapper.orderByDesc("id"); + Page cursorPage = new Page<>(1, pageSize); + List list = service.page(cursorPage, batchWrapper).getRecords(); + + log.info("系统日志游标导出(ID游标) - lastId: {} batchSize: {} 返回: {}", lastId, pageSize, list.size()); + if (!list.isEmpty()) { + SysLog endRecord = list.get(list.size() - 1); + log.debug("本批次最后一条记录游标ID -> id: {}", endRecord.getId()); + } + return list; + } + + @Override + public int getPageSize() { + return 10000; + } + }; + + String title = "系统日志"; + // AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + //此处设置的filename无效 ,前端会重更新设置一下 + mv.addObject(NormalExcelConstants.FILE_NAME, title); + mv.addObject(NormalExcelConstants.CLASS, SysLog.class); + ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, GhbBaseConfig.getPath().getUpload()); + mv.addObject(NormalExcelConstants.PARAMS, exportParams); + mv.addObject(NormalExcelConstants.EXPORT_SERVER, excelExportServer); + mv.addObject(NormalExcelConstants.QUERY_PARAMS, queryWrapper); + return mv; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysPermissionController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysPermissionController.java new file mode 100644 index 0000000..1404269 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysPermissionController.java @@ -0,0 +1,1030 @@ +package com.ghb.base.modules.system.controller; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.subject.Subject; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.Md5Util; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.shiro.ShiroRealm; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.constant.DefIndexConst; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.model.SysPermissionTree; +import com.ghb.base.modules.system.model.TreeModel; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.util.PermissionDataUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 菜单权限表 前端控制器 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +@Slf4j +@RestController +@RequestMapping("/sys/permission") +public class SysPermissionController { + + @Autowired + private ISysPermissionService sysPermissionService; + + @Autowired + private ISysRolePermissionService sysRolePermissionService; + + @Autowired + private ISysPermissionDataRuleService sysPermissionDataRuleService; + + @Autowired + private ISysDepartPermissionService sysDepartPermissionService; + + @Autowired + private ISysUserService sysUserService; + + @Autowired + private GhbBaseConfig GhbBaseConfig; + + @Autowired + private BaseCommonService baseCommonService; + + @Autowired + private ISysRoleIndexService sysRoleIndexService; + + @Autowired + private ShiroRealm shiroRealm; + + /** + * 子菜单 + */ + private static final String CHILDREN = "children"; + + /** + * 加载数据节点 + * + * @return + */ + //@RequiresPermissions("system:permission:list") + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> list(SysPermission sysPermission, HttpServletRequest req) { + long start = System.currentTimeMillis(); + Result> result = new Result<>(); + try { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.orderByAsc(SysPermission::getSortNo); + + //支持通过菜单名字或url,模糊查询 + if(oConvertUtils.isNotEmpty(sysPermission.getName())){ + query.and(wrapper -> wrapper + .like(SysPermission::getName, sysPermission.getName()) + .or() + .like(SysPermission::getUrl, sysPermission.getName()) + ); + } + List list = sysPermissionService.list(query); + List treeList = new ArrayList<>(); + + //如果有菜单名查询条件,则平铺数据 不做上下级 + if(oConvertUtils.isNotEmpty(sysPermission.getName())){ + if(list!=null && list.size()>0){ + treeList = list.stream().map(e -> { + e.setLeaf(true); + return new SysPermissionTree(e); + }).collect(Collectors.toList()); + } + }else{ + getTreeList(treeList, list, null); + } + result.setResult(treeList); + result.setSuccess(true); + log.debug("======获取全部菜单数据=====耗时:" + (System.currentTimeMillis() - start) + "毫秒"); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /*update_begin author:wuxianquan date:20190908 for:先查询一级菜单,当用户点击展开菜单时加载子菜单 */ + /** + * 系统菜单列表(一级菜单) + * + * @return + */ + @RequestMapping(value = "/getSystemMenuList", method = RequestMethod.GET) + public Result> getSystemMenuList() { + long start = System.currentTimeMillis(); + Result> result = new Result<>(); + try { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getMenuType,CommonConstant.MENU_TYPE_0); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.orderByAsc(SysPermission::getSortNo); + List list = sysPermissionService.list(query); + List sysPermissionTreeList = new ArrayList(); + for(SysPermission sysPermission : list){ + SysPermissionTree sysPermissionTree = new SysPermissionTree(sysPermission); + sysPermissionTreeList.add(sysPermissionTree); + } + result.setResult(sysPermissionTreeList); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + log.info("======获取一级菜单数据=====耗时:" + (System.currentTimeMillis() - start) + "毫秒"); + return result; + } + + /** + * 查询子菜单 + * @param parentId + * @return + */ + @RequestMapping(value = "/getSystemSubmenu", method = RequestMethod.GET) + public Result> getSystemSubmenu(@RequestParam("parentId") String parentId){ + Result> result = new Result<>(); + try{ + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getParentId,parentId); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.orderByAsc(SysPermission::getSortNo); + List list = sysPermissionService.list(query); + List sysPermissionTreeList = new ArrayList(); + for(SysPermission sysPermission : list){ + SysPermissionTree sysPermissionTree = new SysPermissionTree(sysPermission); + sysPermissionTreeList.add(sysPermissionTree); + } + result.setResult(sysPermissionTreeList); + result.setSuccess(true); + }catch (Exception e){ + log.error(e.getMessage(), e); + } + return result; + } + /*update_end author:wuxianquan date:20190908 for:先查询一级菜单,当用户点击展开菜单时加载子菜单 */ + + /** + * 查询子菜单 + * + * @param parentIds 父ID(多个采用半角逗号分割) + * @return 返回 key-value 的 Map + */ + @GetMapping("/getSystemSubmenuBatch") + public Result getSystemSubmenuBatch(@RequestParam("parentIds") String parentIds) { + try { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + List parentIdList = Arrays.asList(parentIds.split(",")); + query.in(SysPermission::getParentId, parentIdList); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.orderByAsc(SysPermission::getSortNo); + List list = sysPermissionService.list(query); + Map> listMap = new HashMap(5); + for (SysPermission item : list) { + String pid = item.getParentId(); + if (parentIdList.contains(pid)) { + List mapList = listMap.get(pid); + if (mapList == null) { + mapList = new ArrayList<>(); + } + mapList.add(new SysPermissionTree(item)); + listMap.put(pid, mapList); + } + } + return Result.ok(listMap); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("批量查询子菜单失败:" + e.getMessage()); + } + } + +// /** +// * 查询用户拥有的菜单权限和按钮权限(根据用户账号) +// * +// * @return +// */ +// @RequestMapping(value = "/queryByUser", method = RequestMethod.GET) +// public Result queryByUser(HttpServletRequest req) { +// Result result = new Result<>(); +// try { +// String username = req.getParameter("username"); +// List metaList = sysPermissionService.queryByUser(username); +// JSONArray jsonArray = new JSONArray(); +// this.getPermissionJsonArray(jsonArray, metaList, null); +// result.setResult(jsonArray); +// result.success("查询成功"); +// } catch (Exception e) { +// result.error500("查询失败:" + e.getMessage()); +// log.error(e.getMessage(), e); +// } +// return result; +// } + + /** + * 查询用户拥有的菜单权限和按钮权限 + * + * @return + */ + @RequestMapping(value = "/getUserPermissionByToken", method = RequestMethod.GET) + //@DynamicTable(value = DynamicTableConstant.SYS_ROLE_INDEX) + public Result getUserPermissionByToken(HttpServletRequest request) { + Result result = new Result(); + try { + //直接获取当前用户不适用前端token + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + if (oConvertUtils.isEmpty(loginUser)) { + return Result.error("请登录系统!"); + } + List metaList = sysPermissionService.queryByUser(loginUser.getId()); + //添加首页路由 + + // 代码逻辑说明: 自定义首页地址 LOWCOD-1578 + String version = request.getHeader(CommonConstant.VERSION); + SysRoleIndex defIndexCfg = sysUserService.getDynamicIndexByUserRole(loginUser.getUsername(), version); + if (defIndexCfg == null) { + defIndexCfg = sysRoleIndexService.initDefaultIndex(); + } + + // 如果没有授权角色首页,则自动添加首页路由 + if (!PermissionDataUtil.hasIndexPage(metaList, defIndexCfg)) { + LambdaQueryWrapper indexQueryWrapper = new LambdaQueryWrapper<>(); + indexQueryWrapper.eq(SysPermission::getUrl, defIndexCfg.getUrl()); + SysPermission indexMenu = sysPermissionService.getOne(indexQueryWrapper); + if (indexMenu == null) { + indexMenu = new SysPermission(); + indexMenu.setUrl(defIndexCfg.getUrl()); + indexMenu.setComponent(defIndexCfg.getComponent()); + indexMenu.setRoute(defIndexCfg.getRoute()); + indexMenu.setName(DefIndexConst.DEF_INDEX_NAME); + indexMenu.setMenuType(0); + } + // 如果没有授权一级菜单,则自身变为一级菜单 + if (indexMenu.getParentId() != null && !PermissionDataUtil.hasMenuById(metaList, indexMenu.getParentId())) { + indexMenu.setMenuType(0); + indexMenu.setParentId(null); + } + if (oConvertUtils.isEmpty(indexMenu.getIcon())) { + indexMenu.setIcon("ant-design:home"); + } + metaList.add(0, indexMenu); + } + +/* TODO 注: 这段代码的主要作用是:把首页菜单的组件替换成角色菜单的组件,由于现在的逻辑如果角色菜单不存在则自动插入一条,所以这段代码暂时不需要 + List menus = metaList.stream().filter(sysPermission -> { + if (defIndexCfg.getUrl().equals(sysPermission.getUrl())) { + return true; + } + return defIndexCfg.getUrl().equals(sysPermission.getUrl()); + }).collect(Collectors.toList()); + // 代码逻辑说明: 设置自定义首页地址和组件---------- + if (menus.size() == 1) { + String component = defIndexCfg.getComponent(); + String routeUrl = defIndexCfg.getUrl(); + boolean route = defIndexCfg.isRoute(); + if (oConvertUtils.isNotEmpty(routeUrl)) { + menus.get(0).setComponent(component); + menus.get(0).setRoute(route); + menus.get(0).setUrl(routeUrl); + } else { + menus.get(0).setComponent(component); + } + } +*/ + + JSONObject json = new JSONObject(); + JSONArray menujsonArray = new JSONArray(); + this.getPermissionJsonArray(menujsonArray, metaList, null); + //一级菜单下的子菜单全部是隐藏路由,则一级菜单不显示 + this.handleFirstLevelMenuHidden(menujsonArray); + + JSONArray authjsonArray = new JSONArray(); + this.getAuthJsonArray(authjsonArray, metaList); + //查询所有的权限 + LambdaQueryWrapper query = new LambdaQueryWrapper().select( SysPermission::getName, SysPermission::getPermsType, SysPermission::getPerms, SysPermission::getStatus); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.eq(SysPermission::getMenuType, CommonConstant.MENU_TYPE_2); + //query.eq(SysPermission::getStatus, "1"); + List allAuthList = sysPermissionService.list(query); + JSONArray allauthjsonArray = new JSONArray(); + this.getAllAuthJsonArray(allauthjsonArray, allAuthList); + //路由菜单 + json.put("menu", menujsonArray); + //按钮权限(用户拥有的权限集合) + json.put("auth", authjsonArray); + // 按钮权限(用户拥有的权限集合) + List codeList = metaList.stream() + .filter((permission) -> CommonConstant.MENU_TYPE_2.equals(permission.getMenuType()) && CommonConstant.STATUS_1.equals(permission.getStatus())) + .collect(ArrayList::new, (list, permission) -> list.add(permission.getPerms()), ArrayList::addAll); + // 所拥有的权限编码(vue3专用) + json.put("codeList", codeList); + //全部权限配置集合(按钮权限,访问权限) + json.put("allAuth", allauthjsonArray); + //数据源安全模式 + json.put("sysSafeMode", GhbBaseConfig.getFirewall()!=null? GhbBaseConfig.getFirewall().getDataSourceSafe(): false); + result.setResult(json); + } catch (Exception e) { + result.error500("查询失败:" + e.getMessage()); + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 【vue3专用】获取 + * 1、查询用户拥有的按钮/表单访问权限 + * 2、所有权限 (菜单权限配置) + * 3、系统安全模式 (开启则online报表的数据源必填) + */ + @RequestMapping(value = "/getPermCode", method = RequestMethod.GET) + public Result getPermCode() { + try { + // 直接获取当前用户 + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + if (oConvertUtils.isEmpty(loginUser)) { + return Result.error("请登录系统!"); + } + // 获取当前用户的权限集合 + List metaList = sysPermissionService.queryByUser(loginUser.getId()); + // 按钮权限(用户拥有的权限集合) + List codeList = metaList.stream() + .filter((permission) -> CommonConstant.MENU_TYPE_2.equals(permission.getMenuType()) && CommonConstant.STATUS_1.equals(permission.getStatus())) + .collect(ArrayList::new, (list, permission) -> list.add(permission.getPerms()), ArrayList::addAll); + // + JSONArray authArray = new JSONArray(); + this.getAuthJsonArray(authArray, metaList); + // 查询所有的权限 + LambdaQueryWrapper query = new LambdaQueryWrapper().select( SysPermission::getName, SysPermission::getPermsType, SysPermission::getPerms, SysPermission::getStatus); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.eq(SysPermission::getMenuType, CommonConstant.MENU_TYPE_2); + List allAuthList = sysPermissionService.list(query); + JSONArray allAuthArray = new JSONArray(); + this.getAllAuthJsonArray(allAuthArray, allAuthList); + JSONObject result = new JSONObject(); + // 所拥有的权限编码 + result.put("codeList", codeList); + //按钮权限(用户拥有的权限集合) + result.put("auth", authArray); + //全部权限配置集合(按钮权限,访问权限) + result.put("allAuth", allAuthArray); + //数据源安全模式 + result.put("sysSafeMode", GhbBaseConfig.getFirewall()!=null? GhbBaseConfig.getFirewall().getDataSourceSafe(): null); + return Result.OK(result); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("查询失败:" + e.getMessage()); + } + } + + /** + * 添加菜单 + * @param permission + * @return + */ + @RequiresPermissions("system:permission:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + public Result add(@RequestBody SysPermission permission) { + Result result = new Result(); + try { + permission = PermissionDataUtil.intelligentProcessData(permission); + sysPermissionService.addPermission(permission); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑菜单 + * @param permission + * @return + */ + @RequiresPermissions("system:permission:edit") + @RequestMapping(value = "/edit", method = { RequestMethod.PUT, RequestMethod.POST }) + public Result edit(@RequestBody SysPermission permission) { + Result result = new Result<>(); + try { + permission = PermissionDataUtil.intelligentProcessData(permission); + sysPermissionService.editPermission(permission); + result.success("修改成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 检测菜单路径是否存在 + * @param id + * @param url + * @return + */ + @RequestMapping(value = "/checkPermDuplication", method = RequestMethod.GET) + public Result checkPermDuplication(@RequestParam(name = "id", required = false) String id,@RequestParam(name = "url") String url,@RequestParam(name = "alwaysShow") Boolean alwaysShow) { + Result result = new Result<>(); + try { + boolean check=sysPermissionService.checkPermDuplication(id,url,alwaysShow); + if(check){ + return Result.ok("该值可用!"); + } + return Result.error("访问路径不允许重复,请重定义!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 删除菜单 + * @param id + * @return + */ + @RequiresPermissions("system:permission:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + public Result delete(@RequestParam(name = "id", required = true) String id) { + Result result = new Result<>(); + try { + sysPermissionService.deletePermission(id); + result.success("删除成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500(e.getMessage()); + } + return result; + } + + /** + * 批量删除菜单 + * @param ids + * @return + */ + @RequiresPermissions("system:permission:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + Result result = new Result<>(); + try { + String[] arr = ids.split(","); + for (String id : arr) { + if (oConvertUtils.isNotEmpty(id)) { + try { + sysPermissionService.deletePermission(id); + } catch (GhbBootException e) { + if(e.getMessage()!=null && e.getMessage().contains("未找到菜单信息")){ + log.warn(e.getMessage()); + }else{ + throw e; + } + } + } + } + result.success("删除成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("删除失败!"); + } + return result; + } + + /** + * 获取全部的权限树 + * + * @return + */ + @RequestMapping(value = "/queryTreeList", method = RequestMethod.GET) + public Result> queryTreeList() { + Result> result = new Result<>(); + // 全部权限ids + List ids = new ArrayList<>(); + try { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.orderByAsc(SysPermission::getSortNo); + List list = sysPermissionService.list(query); + for (SysPermission sysPer : list) { + ids.add(sysPer.getId()); + } + List treeList = new ArrayList<>(); + getTreeModelList(treeList, list, null); + + Map resMap = new HashMap(5); + // 全部树节点数据 + resMap.put("treeList", treeList); + // 全部树ids + resMap.put("ids", ids); + result.setResult(resMap); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 异步加载数据节点 [接口是废的,没有用到] + * + * @return + */ + @RequestMapping(value = "/queryListAsync", method = RequestMethod.GET) + public Result> queryAsync(@RequestParam(name = "pid", required = false) String parentId) { + Result> result = new Result<>(); + try { + List list = sysPermissionService.queryListByParentId(parentId); + if (list == null || list.size() <= 0) { + result.error500("未找到角色信息"); + } else { + result.setResult(list); + result.setSuccess(true); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return result; + } + + /** + * 查询角色授权 + * + * @return + */ + @RequestMapping(value = "/queryRolePermission", method = RequestMethod.GET) + public Result> queryRolePermission(@RequestParam(name = "roleId", required = true) String roleId) { + Result> result = new Result<>(); + try { + List list = sysRolePermissionService.list(new QueryWrapper().lambda().eq(SysRolePermission::getRoleId, roleId)); + result.setResult(list.stream().map(sysRolePermission -> String.valueOf(sysRolePermission.getPermissionId())).collect(Collectors.toList())); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 保存角色授权 + * + * @return + */ + @RequestMapping(value = "/saveRolePermission", method = RequestMethod.POST) + @RequiresPermissions("system:permission:saveRole") + public Result saveRolePermission(@RequestBody JSONObject json) { + long start = System.currentTimeMillis(); + Result result = new Result<>(); + try { + String roleId = json.getString("roleId"); + String permissionIds = json.getString("permissionIds"); + String lastPermissionIds = json.getString("lastpermissionIds"); + this.sysRolePermissionService.saveRolePermission(roleId, permissionIds, lastPermissionIds); + // 代码逻辑说明: [VUEN-234]用户管理角色授权添加敏感日志------------ + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + baseCommonService.addLog("修改角色ID: "+roleId+" 的权限配置,操作人: " +loginUser.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + result.success("保存成功!"); + log.info("======角色授权成功=====耗时:" + (System.currentTimeMillis() - start) + "毫秒"); + + // 清除当前用户的授权缓存信息 + Subject currentUser = SecurityUtils.getSubject(); + if (currentUser.isAuthenticated()) { + shiroRealm.clearCache(currentUser.getPrincipals()); + } + + } catch (Exception e) { + result.error500("授权失败!"); + log.error(e.getMessage(), e); + } + return result; + } + + private void getTreeList(List treeList, List metaList, SysPermissionTree temp) { + for (SysPermission permission : metaList) { + String tempPid = permission.getParentId(); + SysPermissionTree tree = new SysPermissionTree(permission); + if (temp == null && oConvertUtils.isEmpty(tempPid)) { + treeList.add(tree); + if (!tree.getIsLeaf()) { + getTreeList(treeList, metaList, tree); + } + } else if (temp != null && tempPid != null && tempPid.equals(temp.getId())) { + temp.getChildren().add(tree); + if (!tree.getIsLeaf()) { + getTreeList(treeList, metaList, tree); + } + } + + } + } + + private void getTreeModelList(List treeList, List metaList, TreeModel temp) { + for (SysPermission permission : metaList) { + String tempPid = permission.getParentId(); + TreeModel tree = new TreeModel(permission); + if (temp == null && oConvertUtils.isEmpty(tempPid)) { + treeList.add(tree); + if (!tree.getIsLeaf()) { + getTreeModelList(treeList, metaList, tree); + } + } else if (temp != null && tempPid != null && tempPid.equals(temp.getKey())) { + temp.getChildren().add(tree); + if (!tree.getIsLeaf()) { + getTreeModelList(treeList, metaList, tree); + } + } + + } + } + + /** + * 一级菜单的子菜单全部是隐藏路由,则一级菜单不显示 + * @param jsonArray + */ + private void handleFirstLevelMenuHidden(JSONArray jsonArray) { + jsonArray = jsonArray.stream().map(obj -> { + JSONObject returnObj = new JSONObject(); + JSONObject jsonObj = (JSONObject)obj; + if(jsonObj.containsKey(CHILDREN)){ + JSONArray childrens = jsonObj.getJSONArray(CHILDREN); + childrens = childrens.stream().filter(arrObj -> !"true".equals(((JSONObject) arrObj).getString("hidden"))).collect(Collectors.toCollection(JSONArray::new)); + if(childrens==null || childrens.size()==0){ + jsonObj.put("hidden",true); + + //vue3版本兼容代码 + JSONObject meta = new JSONObject(); + meta.put("hideMenu",true); + jsonObj.put("meta", meta); + } + } + return returnObj; + }).collect(Collectors.toCollection(JSONArray::new)); + } + + + /** + * 获取权限JSON数组 + * @param jsonArray + * @param allList + */ + private void getAllAuthJsonArray(JSONArray jsonArray,List allList) { + JSONObject json = null; + for (SysPermission permission : allList) { + json = new JSONObject(); + json.put("action", permission.getPerms()); + json.put("status", permission.getStatus()); + //1显示2禁用 + json.put("type", permission.getPermsType()); + json.put("describe", permission.getName()); + jsonArray.add(json); + } + } + + /** + * 获取权限JSON数组 + * @param jsonArray + * @param metaList + */ + private void getAuthJsonArray(JSONArray jsonArray,List metaList) { + for (SysPermission permission : metaList) { + if(permission.getMenuType()==null) { + continue; + } + JSONObject json = null; + if(permission.getMenuType().equals(CommonConstant.MENU_TYPE_2) &&CommonConstant.STATUS_1.equals(permission.getStatus())) { + json = new JSONObject(); + json.put("action", permission.getPerms()); + json.put("type", permission.getPermsType()); + json.put("describe", permission.getName()); + jsonArray.add(json); + } + } + } + /** + * 获取菜单JSON数组 + * @param jsonArray + * @param metaList + * @param parentJson + */ + private void getPermissionJsonArray(JSONArray jsonArray, List metaList, JSONObject parentJson) { + for (SysPermission permission : metaList) { + if (permission.getMenuType() == null) { + continue; + } + String tempPid = permission.getParentId(); + JSONObject json = getPermissionJsonObject(permission); + if(json==null) { + continue; + } + if (parentJson == null && oConvertUtils.isEmpty(tempPid)) { + jsonArray.add(json); + if (!permission.isLeaf()) { + getPermissionJsonArray(jsonArray, metaList, json); + } + } else if (parentJson != null && oConvertUtils.isNotEmpty(tempPid) && tempPid.equals(parentJson.getString("id"))) { + // 类型( 0:一级菜单 1:子菜单 2:按钮 ) + if (permission.getMenuType().equals(CommonConstant.MENU_TYPE_2)) { + JSONObject metaJson = parentJson.getJSONObject("meta"); + if (metaJson.containsKey("permissionList")) { + metaJson.getJSONArray("permissionList").add(json); + } else { + JSONArray permissionList = new JSONArray(); + permissionList.add(json); + metaJson.put("permissionList", permissionList); + } + // 类型( 0:一级菜单 1:子菜单 2:按钮 ) + } else if (permission.getMenuType().equals(CommonConstant.MENU_TYPE_1) || permission.getMenuType().equals(CommonConstant.MENU_TYPE_0)) { + if (parentJson.containsKey("children")) { + parentJson.getJSONArray("children").add(json); + } else { + JSONArray children = new JSONArray(); + children.add(json); + parentJson.put("children", children); + } + + if (!permission.isLeaf()) { + getPermissionJsonArray(jsonArray, metaList, json); + } + } + } + + } + } + + /** + * 根据菜单配置生成路由json + * @param permission + * @return + */ + private JSONObject getPermissionJsonObject(SysPermission permission) { + JSONObject json = new JSONObject(); + // 类型(0:一级菜单 1:子菜单 2:按钮) + if (permission.getMenuType().equals(CommonConstant.MENU_TYPE_2)) { + //json.put("action", permission.getPerms()); + //json.put("type", permission.getPermsType()); + //json.put("describe", permission.getName()); + return null; + } else if (permission.getMenuType().equals(CommonConstant.MENU_TYPE_0) || permission.getMenuType().equals(CommonConstant.MENU_TYPE_1)) { + json.put("id", permission.getId()); + if (permission.isRoute()) { + //表示生成路由 + json.put("route", "1"); + } else { + //表示不生成路由 + json.put("route", "0"); + } + + if (isWwwHttpUrl(permission.getUrl())) { + json.put("path", Md5Util.md5Encode(permission.getUrl(), "utf-8")); + } else { + json.put("path", permission.getUrl()); + } + + // 重要规则:路由name (通过URL生成路由name,路由name供前端开发,页面跳转使用) + if (oConvertUtils.isNotEmpty(permission.getComponentName())) { + json.put("name", permission.getComponentName()); + } else { + json.put("name", urlToRouteName(permission.getUrl())); + } + + JSONObject meta = new JSONObject(); + // 是否隐藏路由,默认都是显示的 + if (permission.isHidden()) { + json.put("hidden", true); + //vue3版本兼容代码 + meta.put("hideMenu",true); + } + // 聚合路由 + if (permission.isAlwaysShow()) { + json.put("alwaysShow", true); + } + json.put("component", permission.getComponent()); + // 由用户设置是否缓存页面 用布尔值 + if (permission.isKeepAlive()) { + meta.put("keepAlive", true); + } else { + meta.put("keepAlive", false); + } + + /*update_begin author:wuxianquan date:20190908 for:往菜单信息里添加外链菜单打开方式 */ + //外链菜单打开方式 + if (permission.isInternalOrExternal()) { + meta.put("internalOrExternal", true); + } else { + meta.put("internalOrExternal", false); + } + /* update_end author:wuxianquan date:20190908 for: 往菜单信息里添加外链菜单打开方式*/ + + meta.put("title", permission.getName()); + + // 代码逻辑说明: 路由缓存问题,关闭了tab页时再打开就不刷新 #842 + String component = permission.getComponent(); + if(oConvertUtils.isNotEmpty(permission.getComponentName()) || oConvertUtils.isNotEmpty(component)){ + meta.put("componentName", oConvertUtils.getString(permission.getComponentName(),component.substring(component.lastIndexOf("/")+1))); + } + + if (oConvertUtils.isEmpty(permission.getParentId())) { + // 一级菜单跳转地址 + json.put("redirect", permission.getRedirect()); + if (oConvertUtils.isNotEmpty(permission.getIcon())) { + meta.put("icon", permission.getIcon()); + } + } else { + if (oConvertUtils.isNotEmpty(permission.getIcon())) { + meta.put("icon", permission.getIcon()); + } + } + if (isWwwHttpUrl(permission.getUrl())) { + meta.put("url", permission.getUrl()); + } + // 代码逻辑说明: 新增适配vue3项目的隐藏tab功能 + if (permission.isHideTab()) { + meta.put("hideTab", true); + } + json.put("meta", meta); + } + + return json; + } + + /** + * 判断是否外网URL 例如: http://localhost:8080/Ghb-boot/swagger-ui.html#/ 支持特殊格式: {{ + * window._CONFIG['domianURL'] }}/druid/ {{ JS代码片段 }},前台解析会自动执行JS代码片段 + * + * @return + */ + private boolean isWwwHttpUrl(String url) { + boolean flag = url != null && (url.startsWith(CommonConstant.HTTP_PROTOCOL) || url.startsWith(CommonConstant.HTTPS_PROTOCOL) || url.startsWith(SymbolConstant.DOUBLE_LEFT_CURLY_BRACKET)); + if (flag) { + return true; + } + return false; + } + + /** + * 通过URL生成路由name(去掉URL前缀斜杠,替换内容中的斜杠‘/’为-) 举例: URL = /isystem/role RouteName = + * isystem-role + * + * @return + */ + private String urlToRouteName(String url) { + if (oConvertUtils.isNotEmpty(url)) { + if (url.startsWith(SymbolConstant.SINGLE_SLASH)) { + url = url.substring(1); + } + url = url.replace("/", "-"); + + // 特殊标记 + url = url.replace(":", "@"); + return url; + } else { + return null; + } + } + + /** + * 根据菜单id来获取其对应的权限数据 + * + * @param sysPermissionDataRule + * @return + */ + @RequestMapping(value = "/getPermRuleListByPermId", method = RequestMethod.GET) + public Result> getPermRuleListByPermId(SysPermissionDataRule sysPermissionDataRule) { + List permRuleList = sysPermissionDataRuleService.getPermRuleListByPermId(sysPermissionDataRule.getPermissionId()); + Result> result = new Result<>(); + result.setSuccess(true); + result.setResult(permRuleList); + return result; + } + + /** + * 添加菜单权限数据 + * + * @param sysPermissionDataRule + * @return + */ + @RequiresPermissions("system:permission:addRule") + @RequestMapping(value = "/addPermissionRule", method = RequestMethod.POST) + public Result addPermissionRule(@RequestBody SysPermissionDataRule sysPermissionDataRule) { + Result result = new Result(); + try { + sysPermissionDataRule.setCreateTime(new Date()); + sysPermissionDataRuleService.savePermissionDataRule(sysPermissionDataRule); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + @RequiresPermissions("system:permission:editRule") + @RequestMapping(value = "/editPermissionRule", method = { RequestMethod.PUT, RequestMethod.POST }) + public Result editPermissionRule(@RequestBody SysPermissionDataRule sysPermissionDataRule) { + Result result = new Result(); + try { + sysPermissionDataRuleService.saveOrUpdate(sysPermissionDataRule); + result.success("更新成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 删除菜单权限数据 + * + * @param id + * @return + */ + @RequiresPermissions("system:permission:deleteRule") + @RequestMapping(value = "/deletePermissionRule", method = RequestMethod.DELETE) + public Result deletePermissionRule(@RequestParam(name = "id", required = true) String id) { + Result result = new Result(); + try { + sysPermissionDataRuleService.deletePermissionDataRule(id); + result.success("删除成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 查询菜单权限数据 + * + * @param sysPermissionDataRule + * @return + */ + @RequestMapping(value = "/queryPermissionRule", method = RequestMethod.GET) + public Result> queryPermissionRule(SysPermissionDataRule sysPermissionDataRule) { + Result> result = new Result<>(); + try { + List permRuleList = sysPermissionDataRuleService.queryPermissionRule(sysPermissionDataRule); + result.setResult(permRuleList); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 部门权限表 + * @param departId + * @return + */ + @RequestMapping(value = "/queryDepartPermission", method = RequestMethod.GET) + public Result> queryDepartPermission(@RequestParam(name = "departId", required = true) String departId) { + Result> result = new Result<>(); + try { + List list = sysDepartPermissionService.list(new QueryWrapper().lambda().eq(SysDepartPermission::getDepartId, departId)); + result.setResult(list.stream().map(sysDepartPermission -> String.valueOf(sysDepartPermission.getPermissionId())).collect(Collectors.toList())); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 保存部门授权 + * + * @return + */ + @RequestMapping(value = "/saveDepartPermission", method = RequestMethod.POST) + @RequiresPermissions("system:permission:saveDepart") + public Result saveDepartPermission(@RequestBody JSONObject json) { + long start = System.currentTimeMillis(); + Result result = new Result<>(); + try { + String departId = json.getString("departId"); + String permissionIds = json.getString("permissionIds"); + String lastPermissionIds = json.getString("lastpermissionIds"); + this.sysDepartPermissionService.saveDepartPermission(departId, permissionIds, lastPermissionIds); + result.success("保存成功!"); + log.info("======部门授权成功=====耗时:" + (System.currentTimeMillis() - start) + "毫秒"); + } catch (Exception e) { + result.error500("授权失败!"); + log.error(e.getMessage(), e); + } + return result; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysPositionController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysPositionController.java new file mode 100644 index 0000000..6f56637 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysPositionController.java @@ -0,0 +1,406 @@ +package com.ghb.base.modules.system.controller; + +import cn.hutool.core.util.RandomUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.ImportExcelUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysPosition; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.service.ISysPositionService; +import com.ghb.base.modules.system.service.ISysUserPositionService; +import com.ghb.base.modules.system.service.ISysUserService; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Description: 职务表 + * @Author: Ghb-boot + * @Date: 2019-09-19 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "职务表") +@RestController +@RequestMapping("/sys/position") +public class SysPositionController { + + @Autowired + private ISysPositionService sysPositionService; + + @Autowired + private ISysUserPositionService userPositionService; + + @Autowired + private ISysUserService userService; + + /** + * 分页列表查询 + * + * @param sysPosition + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "职务表-分页列表查询") + @Operation(summary = "职务表-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(SysPosition sysPosition, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + Result> result = new Result>(); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysPosition.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(),0)); + } + //------------------------------------------------------------------------------------------------ + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysPosition, req.getParameterMap()); + queryWrapper.orderByAsc("post_level"); + queryWrapper.orderByDesc("create_time"); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysPositionService.page(page, queryWrapper); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 添加 + * + * @param sysPosition + * @return + */ + @AutoLog(value = "职务表-添加") + @Operation(summary = "职务表-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody SysPosition sysPosition) { + Result result = new Result(); + try { + //编号是空的,不需要判断多租户隔离了 + if(oConvertUtils.isEmpty(sysPosition.getCode())){ + //生成职位编码10位 + sysPosition.setCode(RandomUtil.randomString(10)); + } + sysPositionService.save(sysPosition); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑 + * + * @param sysPosition + * @return + */ + @AutoLog(value = "职务表-编辑") + @Operation(summary = "职务表-编辑") + @RequestMapping(value = "/edit", method ={RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody SysPosition sysPosition) { + Result result = new Result(); + SysPosition sysPositionEntity = sysPositionService.getById(sysPosition.getId()); + if (sysPositionEntity == null) { + result.error500("未找到对应实体"); + } else { + boolean ok = sysPositionService.updateById(sysPosition); + //TODO 返回false说明什么? + if (ok) { + result.success("修改成功!"); + } + } + + return result; + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "职务表-通过id删除") + @Operation(summary = "职务表-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + try { + sysPositionService.removeById(id); + //删除用户职位关系表 + userPositionService.removeByPositionId(id); + } catch (Exception e) { + log.error("删除失败", e.getMessage()); + return Result.error("删除失败!"); + } + return Result.ok("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "职务表-批量删除") + @Operation(summary = "职务表-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + Result result = new Result(); + if (ids == null || "".equals(ids.trim())) { + result.error500("参数不识别!"); + } else { + this.sysPositionService.removeByIds(Arrays.asList(ids.split(","))); + result.success("删除成功!"); + } + return result; + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "职务表-通过id查询") + @Operation(summary = "职务表-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + Result result = new Result(); + SysPosition sysPosition = sysPositionService.getById(id); + if (sysPosition == null) { + result.error500("未找到对应实体"); + } else { + result.setResult(sysPosition); + result.setSuccess(true); + } + return result; + } + + /** + * 导出excel + * + * @param request + * @param response + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(SysPosition sysPosition,HttpServletRequest request, HttpServletResponse response) { + // Step.1 组装查询条件 + QueryWrapper queryWrapper = null; + try { + String paramsStr = request.getParameter("paramsStr"); + if (oConvertUtils.isNotEmpty(paramsStr)) { + String deString = URLDecoder.decode(paramsStr, "UTF-8"); + sysPosition = JSON.parseObject(deString, SysPosition.class); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysPosition.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(),0)); + } + //------------------------------------------------------------------------------------------------ + } + queryWrapper = QueryGenerator.initQueryWrapper(sysPosition, request.getParameterMap()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + // 代码逻辑说明: [03]职务导出,如果选择数据则只导出相关数据-------------------- + String selections = request.getParameter("selections"); + if(!oConvertUtils.isEmpty(selections)){ + queryWrapper.in("id",selections.split(",")); + } + //Step.2 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + List pageList = sysPositionService.list(queryWrapper); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "职务表列表"); + mv.addObject(NormalExcelConstants.CLASS, SysPosition.class); + //支持导出xlsx格式 + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("职务表列表数据", "导出人:"+user.getRealname(),"导出信息", ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + //职级导出支持导出字段 + String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS); + if(oConvertUtils.isNotEmpty(exportFields)){ + mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields); + } + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response)throws IOException { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + // 错误信息 + List errorMessage = new ArrayList<>(); + int successLines = 0, errorLines = 0; + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List listSysPositions = ExcelImportUtil.importExcel(file.getInputStream(), SysPosition.class, params); + List list = ImportExcelUtil.importDateSave(listSysPositions, ISysPositionService.class, errorMessage,CommonConstant.SQL_INDEX_UNIQ_CODE); + errorLines+=list.size(); + successLines+=(listSysPositions.size()-errorLines); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("文件导入失败:" + e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return ImportExcelUtil.imporReturnRes(errorLines,successLines,errorMessage); + } + + /** + * 通过code查询 + * + * @param code + * @return + */ + @AutoLog(value = "职务表-通过code查询") + @Operation(summary = "职务表-通过code查询") + @GetMapping(value = "/queryByCode") + public Result queryByCode(@RequestParam(name = "code", required = true) String code) { + Result result = new Result(); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("code",code); + SysPosition sysPosition = sysPositionService.getOne(queryWrapper); + if (sysPosition == null) { + result.error500("未找到对应实体"); + } else { + result.setResult(sysPosition); + result.setSuccess(true); + } + return result; + } + + + /** + * 通过多个ID查询 + * + * @param ids + * @return + */ + @AutoLog(value = "职务表-通过多个查询") + @Operation(summary = "职务表-通过多个id查询") + @GetMapping(value = "/queryByIds") + public Result> queryByIds(@RequestParam(name = "ids") String ids) { + Result> result = new Result<>(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in(true,"id",ids.split(",")); + List list = sysPositionService.list(queryWrapper); + if (list == null) { + result.error500("未找到对应实体"); + } else { + result.setResult(list); + result.setSuccess(true); + } + return result; + } + + + + /** + * 获取职位用户列表 + * + * @param pageNo + * @param pageSize + * @param positionId + * @return + */ + @GetMapping("/getPositionUserList") + public Result> getPositionUserList(@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "positionId") String positionId) { + + Page page = new Page<>(pageNo, pageSize); + IPage pageList = userPositionService.getPositionUserList(page, positionId); + List userIds = pageList.getRecords().stream().map(SysUser::getId).collect(Collectors.toList()); + if (null != userIds && userIds.size() > 0) { + Map useDepNames = userService.getDepNamesByUserIds(userIds); + pageList.getRecords().forEach(item -> { + item.setOrgCodeTxt(useDepNames.get(item.getId())); + }); + } + return Result.ok(pageList); + } + + /** + * 添加成员到用户职位关系表 + * + * @param userIds + * @param positionId + * @return + */ + @PostMapping("/savePositionUser") + public Result saveUserPosition(@RequestParam(name = "userIds") String userIds, + @RequestParam(name = "positionId") String positionId) { + userPositionService.saveUserPosition(userIds, positionId); + return Result.ok("添加成功"); + } + + /** + * 职位列表移除成员 + * + * @param userIds + * @param positionId + * @return + */ + @DeleteMapping("/removePositionUser") + public Result removeUserPosition(@RequestParam(name = "userIds") String userIds, + @RequestParam(name = "positionId") String positionId) { + userPositionService.removePositionUser(userIds, positionId); + return Result.OK("移除成员成功"); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysRoleController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysRoleController.java new file mode 100644 index 0000000..7f8ade8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysRoleController.java @@ -0,0 +1,592 @@ +package com.ghb.base.modules.system.controller; + + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.base.BaseMap; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import org.jeecg.common.modules.redis.client.JeecgRedisClient; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.model.TreeModel; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.vo.SysUserRoleCountVo; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; +import com.ghb.base.common.system.vo.LoginUser; +import org.apache.shiro.SecurityUtils; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + +/** + *

+ * 角色表 前端控制器 + *

+ * + * @Author scott + * @since 2018-12-19 + */ +@RestController +@RequestMapping("/sys/role") +@Slf4j +public class SysRoleController { + @Autowired + private ISysRoleService sysRoleService; + + @Autowired + private ISysPermissionDataRuleService sysPermissionDataRuleService; + + @Autowired + private ISysRolePermissionService sysRolePermissionService; + + @Autowired + private ISysPermissionService sysPermissionService; + + @Autowired + private ISysUserRoleService sysUserRoleService; + @Autowired + private BaseCommonService baseCommonService; + @Autowired + private JeecgRedisClient JeecgRedisClient; + + /** + * 分页列表查询 【系统角色,不做租户隔离】 + * @param role + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequiresPermissions("system:role:list") + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> queryPageList(SysRole role, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name="isMultiTranslate", required = false) Boolean isMultiTranslate, + HttpServletRequest req) { + // 代码逻辑说明: 【issues/7948】角色解决根据id查询回显不对--- + if(null != isMultiTranslate && isMultiTranslate){ + pageSize = 100; + } + Result> result = new Result>(); + //QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(role, req.getParameterMap()); + //IPage pageList = sysRoleService.page(page, queryWrapper); + Page page = new Page(pageNo, pageSize); + //换成不做租户隔离的方法,实际上还是存在缺陷(缺陷:如果开启租户隔离,虽然能看到其他租户下的角色,编辑会提示报错) + IPage pageList = sysRoleService.listAllSysRole(page, role); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 分页列表查询【租户角色,做租户隔离】 + * @param role + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequestMapping(value = "/listByTenant", method = RequestMethod.GET) + public Result> listByTenant(SysRole role, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + Result> result = new Result>(); + //此接口必须通过租户来隔离查询 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + role.setTenantId(oConvertUtils.getInt(!"0".equals(TenantContext.getTenant()) ? TenantContext.getTenant() : "", -1)); + } + + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(role, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysRoleService.page(page, queryWrapper); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 添加 + * @param role + * @return + */ + @RequestMapping(value = "/add", method = RequestMethod.POST) + @RequiresPermissions("system:role:add") + public Result add(@RequestBody SysRole role) { + Result result = new Result(); + try { + //开启多租户隔离,角色id自动生成10位 + // 代码逻辑说明: 【TV360X-42】角色新增时设置的编码,保存后不一致--- + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isEmpty(role.getRoleCode())){ + role.setRoleCode(RandomUtil.randomString(10)); + } + role.setCreateTime(new Date()); + sysRoleService.save(role); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑 + * @param role + * @return + */ + @RequiresPermissions("system:role:edit") + @RequestMapping(value = "/edit",method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody SysRole role) { + Result result = new Result(); + SysRole sysrole = sysRoleService.getById(role.getId()); + if(sysrole==null) { + result.error500("未找到对应角色!"); + }else { + role.setUpdateTime(new Date()); + + //------------------------------------------------------------------ + //如果是saas隔离的情况下,判断当前租户id是否是当前租户下的 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + //获取当前用户 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + String username = "admin"; + if (!tenantId.equals(sysrole.getTenantId()) && !username.equals(sysUser.getUsername())) { + baseCommonService.addLog("未经授权,修改非本租户下的角色ID:" + role.getId() + ",操作人:" + sysUser.getUsername(), CommonConstant.LOG_TYPE_2, CommonConstant.OPERATE_TYPE_3); + return Result.error("修改角色失败,当前角色不在此租户中。"); + } + } + //------------------------------------------------------------------ + + boolean ok = sysRoleService.updateById(role); + if(ok) { + result.success("修改成功!"); + } + } + return result; + } + + /** + * 通过id删除 + * @param id + * @return + */ + @RequiresPermissions("system:role:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + public Result delete(@RequestParam(name="id",required=true) String id) { + //如果是saas隔离的情况下,判断当前租户id是否是当前租户下的 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + //获取当前用户 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + Long getRoleCount = sysRoleService.getRoleCountByTenantId(id, tenantId); + String username = "admin"; + if(getRoleCount == 0 && !username.equals(sysUser.getUsername())){ + baseCommonService.addLog("未经授权,删除非本租户下的角色ID:" + id + ",操作人:" + sysUser.getUsername(), CommonConstant.LOG_TYPE_2, CommonConstant.OPERATE_TYPE_4); + return Result.error("删除角色失败,当前角色不在此租户中。"); + } + } + + //是否存在admin角色 + sysRoleService.checkAdminRoleRejectDel(id); + + sysRoleService.deleteRole(id); + + return Result.ok("删除角色成功"); + } + + /** + * 批量删除 + * @param ids + * @return + */ + @RequiresPermissions("system:role:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + baseCommonService.addLog("删除角色操作,角色ids:" + ids, CommonConstant.LOG_TYPE_2, CommonConstant.OPERATE_TYPE_4); + Result result = new Result(); + if(oConvertUtils.isEmpty(ids)) { + result.error500("未选中角色!"); + }else { + //如果是saas隔离的情况下,判断当前租户id是否是当前租户下的 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + String[] roleIds = ids.split(SymbolConstant.COMMA); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String username = "admin"; + for (String id:roleIds) { + Long getRoleCount = sysRoleService.getRoleCountByTenantId(id, tenantId); + //如果存在角色id为0,即不存在,则删除角色 + if(getRoleCount == 0 && !username.equals(sysUser.getUsername()) ){ + baseCommonService.addLog("未经授权,删除非本租户下的角色ID:" + id + ",操作人:" + sysUser.getUsername(), CommonConstant.LOG_TYPE_2, CommonConstant.OPERATE_TYPE_4); + return Result.error("批量删除角色失败,存在角色不在此租户中,禁止批量删除"); + } + } + } + //验证是否为admin角色 + sysRoleService.checkAdminRoleRejectDel(ids); + sysRoleService.deleteBatchRole(ids.split(",")); + result.success("删除角色成功!"); + } + return result; + } + + /** + * 通过id查询 + * @param id + * @return + */ + @RequestMapping(value = "/queryById", method = RequestMethod.GET) + public Result queryById(@RequestParam(name="id",required=true) String id) { + Result result = new Result(); + SysRole sysrole = sysRoleService.getById(id); + if(sysrole==null) { + result.error500("未找到对应实体"); + }else { + result.setResult(sysrole); + result.setSuccess(true); + } + return result; + } + + /** + * 查询全部角色(参与租户隔离) + * + * @return + */ + @RequestMapping(value = "/queryall", method = RequestMethod.GET) + public Result> queryall() { + Result> result = new Result<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper(); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + query.eq(SysRole::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + List list = sysRoleService.list(query); + if(list==null||list.size()<=0) { + result.error500("未找到角色信息"); + }else { + result.setResult(list); + result.setSuccess(true); + } + return result; + } + + /** + * 查询全部系统角色(不做租户隔离) + * + * @return + */ + @RequiresPermissions("system:role:queryallNoByTenant") + @RequestMapping(value = "/queryallNoByTenant", method = RequestMethod.GET) + public Result> queryallNoByTenant() { + Result> result = new Result<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper(); + List list = sysRoleService.list(query); + if(list==null||list.size()<=0) { + result.error500("未找到角色信息"); + }else { + result.setResult(list); + result.setSuccess(true); + } + return result; + } + + /** + * 校验角色编码唯一 + */ + @RequestMapping(value = "/checkRoleCode", method = RequestMethod.GET) + public Result checkUsername(String id,String roleCode) { + Result result = new Result<>(); + //如果此参数为false则程序发生异常 + result.setResult(true); + log.info("--验证角色编码是否唯一---id:"+id+"--roleCode:"+roleCode); + try { + SysRole role = null; + if(oConvertUtils.isNotEmpty(id)) { + role = sysRoleService.getById(id); + } + //SysRole newRole = sysRoleService.getOne(new QueryWrapper().lambda().eq(SysRole::getRoleCode, roleCode)); + SysRole newRole = sysRoleService.getRoleNoTenant(roleCode); + if(newRole!=null) { + //如果根据传入的roleCode查询到信息了,那么就需要做校验了。 + if(role==null) { + //role为空=>新增模式=>只要roleCode存在则返回false + result.setSuccess(false); + result.setMessage("角色编码已存在"); + return result; + }else if(!id.equals(newRole.getId())) { + //否则=>编辑模式=>判断两者ID是否一致- + result.setSuccess(false); + result.setMessage("角色编码已存在"); + return result; + } + } + } catch (Exception e) { + result.setSuccess(false); + result.setResult(false); + result.setMessage(e.getMessage()); + return result; + } + result.setSuccess(true); + return result; + } + + /** + * 导出excel + * @param request + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(SysRole sysRole,HttpServletRequest request) { + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + sysRole.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + + // Step.1 组装查询条件 + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysRole, request.getParameterMap()); + //Step.2 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + List pageList = sysRoleService.list(queryWrapper); + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME,"角色列表"); + mv.addObject(NormalExcelConstants.CLASS,SysRole.class); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //导出支持xlsx + mv.addObject(NormalExcelConstants.PARAMS,new ExportParams("角色列表数据","导出人:"+user.getRealname(),"导出信息", ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST,pageList); + //角色支持指定字段导出 + String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS); + if(oConvertUtils.isNotEmpty(exportFields)){ + mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields); + } + return mv; + } + + /** + * 通过excel导入数据 + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + return sysRoleService.importExcelCheckRoleCode(file, params); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("文件导入失败:" + e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + return Result.error("文件导入失败!"); + } + + /** + * 查询数据规则数据 + */ + @GetMapping(value = "/datarule/{permissionId}/{roleId}") + public Result loadDatarule(@PathVariable("permissionId") String permissionId,@PathVariable("roleId") String roleId) { + List list = sysPermissionDataRuleService.getPermRuleListByPermId(permissionId); + if(list==null || list.size()==0) { + return Result.error("未找到权限配置信息"); + }else { + Map map = new HashMap(5); + map.put("datarule", list); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysRolePermission::getPermissionId, permissionId) + .isNotNull(SysRolePermission::getDataRuleIds) + .eq(SysRolePermission::getRoleId,roleId); + SysRolePermission sysRolePermission = sysRolePermissionService.getOne(query); + if(sysRolePermission==null) { + //return Result.error("未找到角色菜单配置信息"); + }else { + String drChecked = sysRolePermission.getDataRuleIds(); + if(oConvertUtils.isNotEmpty(drChecked)) { + map.put("drChecked", drChecked.endsWith(",")?drChecked.substring(0, drChecked.length()-1):drChecked); + } + } + return Result.ok(map); + //TODO 以后按钮权限的查询也走这个请求 无非在map中多加两个key + } + } + + /** + * 保存数据规则至角色菜单关联表 + */ + @PostMapping(value = "/datarule") + public Result saveDatarule(@RequestBody JSONObject jsonObject) { + try { + String permissionId = jsonObject.getString("permissionId"); + String roleId = jsonObject.getString("roleId"); + String dataRuleIds = jsonObject.getString("dataRuleIds"); + log.info("保存数据规则>>"+"菜单ID:"+permissionId+"角色ID:"+ roleId+"数据权限ID:"+dataRuleIds); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysRolePermission::getPermissionId, permissionId) + .eq(SysRolePermission::getRoleId,roleId); + SysRolePermission sysRolePermission = sysRolePermissionService.getOne(query); + if(sysRolePermission==null) { + return Result.error("请先保存角色菜单权限!"); + }else { + sysRolePermission.setDataRuleIds(dataRuleIds); + this.sysRolePermissionService.updateById(sysRolePermission); + } + } catch (Exception e) { + log.error("SysRoleController.saveDatarule()发生异常:" + e.getMessage(),e); + return Result.error("保存失败"); + } + return Result.ok("保存成功!"); + } + + + /** + * 用户角色授权功能,查询菜单权限树 + * @param request + * @return + */ + @RequestMapping(value = "/queryTreeList", method = RequestMethod.GET) + public Result> queryTreeList(HttpServletRequest request) { + Result> result = new Result<>(); + //全部权限ids + List ids = new ArrayList<>(); + try { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0); + query.orderByAsc(SysPermission::getSortNo); + List list = sysPermissionService.list(query); + for(SysPermission sysPer : list) { + ids.add(sysPer.getId()); + } + List treeList = new ArrayList<>(); + getTreeModelList(treeList, list, null); + Map resMap = new HashMap(5); + //全部树节点数据 + resMap.put("treeList", treeList); + //全部树ids + resMap.put("ids", ids); + result.setResult(resMap); + result.setSuccess(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + private void getTreeModelList(List treeList,List metaList,TreeModel temp) { + for (SysPermission permission : metaList) { + String tempPid = permission.getParentId(); + TreeModel tree = new TreeModel(permission.getId(), tempPid, permission.getName(),permission.getRuleFlag(), permission.isLeaf()); + if(temp==null && oConvertUtils.isEmpty(tempPid)) { + treeList.add(tree); + if(!tree.getIsLeaf()) { + getTreeModelList(treeList, metaList, tree); + } + }else if(temp!=null && tempPid!=null && tempPid.equals(temp.getKey())){ + temp.getChildren().add(tree); + if(!tree.getIsLeaf()) { + getTreeModelList(treeList, metaList, tree); + } + } + + } + } + + /** + * 分页获取全部角色列表(包含每个角色的数量) + * @return + */ + @RequestMapping(value = "/queryPageRoleCount", method = RequestMethod.GET) + public Result> queryPageRoleCount(@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize) { + Result> result = new Result<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper(); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + query.eq(SysRole::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + Page page = new Page<>(pageNo, pageSize); + IPage pageList = sysRoleService.page(page, query); + List records = pageList.getRecords(); + IPage sysRoleCountPage = new PageDTO<>(); + List sysCountVoList = new ArrayList<>(); + //循环角色数据获取每个角色下面对应的角色数量 + for (SysRole role:records) { + LambdaQueryWrapper countQuery = new LambdaQueryWrapper<>(); + countQuery.eq(SysUserRole::getRoleId,role.getId()); + long count = sysUserRoleService.count(countQuery); + SysUserRoleCountVo countVo = new SysUserRoleCountVo(); + BeanUtils.copyProperties(role,countVo); + countVo.setCount(count); + sysCountVoList.add(countVo); + } + sysRoleCountPage.setRecords(sysCountVoList); + sysRoleCountPage.setTotal(pageList.getTotal()); + sysRoleCountPage.setSize(pageList.getSize()); + result.setSuccess(true); + result.setResult(sysRoleCountPage); + return result; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysRoleIndexController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysRoleIndexController.java new file mode 100644 index 0000000..3a81b83 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysRoleIndexController.java @@ -0,0 +1,280 @@ +package com.ghb.base.modules.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.util.JwtUtil; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.constant.DefIndexConst; +import com.ghb.base.modules.system.entity.SysRoleIndex; +import com.ghb.base.modules.system.service.ISysRoleIndexService; +import com.ghb.base.modules.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Arrays; + +/** + * @Description: 角色首页配置 + * @Author: Ghb-boot + * @Date: 2022-03-25 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "角色首页配置") +@RestController +@RequestMapping("/sys/sysRoleIndex") +public class SysRoleIndexController extends GhbController { + @Autowired + private ISysRoleIndexService sysRoleIndexService; + + @Autowired + private ISysUserService sysUserService; + + @Autowired + private RedisUtil redisUtil; + + @Autowired + private BaseCommonService baseCommonService; + /** + * 分页列表查询 + * + * @param sysRoleIndex + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "角色首页配置-分页列表查询") + @Operation(summary = "角色首页配置-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(SysRoleIndex sysRoleIndex, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysRoleIndex, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysRoleIndexService.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param sysRoleIndex + * @return + */ + @RequiresPermissions("system:roleindex:add") + @AutoLog(value = "角色首页配置-添加") + @Operation(summary = "角色首页配置-添加") + @PostMapping(value = "/add") + //@DynamicTable(value = DynamicTableConstant.SYS_ROLE_INDEX) + public Result add(@RequestBody SysRoleIndex sysRoleIndex,HttpServletRequest request) { + String relationType = sysRoleIndex.getRelationType(); + if(oConvertUtils.isEmpty(relationType)){ + sysRoleIndex.setRelationType(CommonConstant.HOME_RELATION_ROLE); + } + sysRoleIndexService.save(sysRoleIndex); + //更新其他全局配置的状态 + sysRoleIndexService.updateOtherDefaultStatus(sysRoleIndex.getRoleCode(),sysRoleIndex.getStatus(),sysRoleIndex.getId()); + sysRoleIndexService.cleanDefaultIndexCache(); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param sysRoleIndex + * @return + */ + @RequiresPermissions("system:roleindex:edit") + @AutoLog(value = "角色首页配置-编辑") + @Operation(summary = "角色首页配置-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + //@DynamicTable(value = DynamicTableConstant.SYS_ROLE_INDEX) + public Result edit(@RequestBody SysRoleIndex sysRoleIndex,HttpServletRequest request) { + String relationType = sysRoleIndex.getRelationType(); + if(oConvertUtils.isEmpty(relationType)){ + sysRoleIndex.setRelationType(CommonConstant.HOME_RELATION_ROLE); + } + sysRoleIndexService.updateById(sysRoleIndex); + //更新其他全局配置的状态 + sysRoleIndexService.updateOtherDefaultStatus(sysRoleIndex.getRoleCode(),sysRoleIndex.getStatus(),sysRoleIndex.getId()); + sysRoleIndexService.cleanDefaultIndexCache(); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "角色首页配置-通过id删除") + @Operation(summary = "角色首页配置-通过id删除") + @RequiresPermissions("system:roleindex:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + sysRoleIndexService.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "角色首页配置-批量删除") + @Operation(summary = "角色首页配置-批量删除") + @RequiresPermissions("system:roleindex:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + baseCommonService.addLog("批量删除用户, ids: " +ids ,CommonConstant.LOG_TYPE_2, 3); + this.sysRoleIndexService.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "角色首页配置-通过id查询") + @Operation(summary = "角色首页配置-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysRoleIndex sysRoleIndex = sysRoleIndexService.getById(id); + return Result.OK(sysRoleIndex); + } + + /** + * 导出excel + * + * @param request + * @param sysRoleIndex + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysRoleIndex sysRoleIndex) { + return super.exportXls(request, sysRoleIndex, SysRoleIndex.class, "角色首页配置"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysRoleIndex.class); + } + + /** + * 通过code查询 + * + * @param roleCode + * @return + */ + @AutoLog(value = "角色首页配置-通过code查询") + @Operation(summary = "角色首页配置-通过code查询") + @GetMapping(value = "/queryByCode") + //@DynamicTable(value = DynamicTableConstant.SYS_ROLE_INDEX) + public Result queryByCode(@RequestParam(name = "roleCode", required = true) String roleCode,HttpServletRequest request) { + SysRoleIndex sysRoleIndex = sysRoleIndexService.getOne(new LambdaQueryWrapper().eq(SysRoleIndex::getRoleCode, roleCode)); + return Result.OK(sysRoleIndex); + } + + /** + * 查询默认首页配置 + */ + @GetMapping("/queryDefIndex") + public Result queryDefIndex() { + SysRoleIndex defIndexCfg = sysRoleIndexService.queryDefaultIndex(); + return Result.OK(defIndexCfg); + } + + /** + * 更新默认首页配置 + */ + @RequiresPermissions("system:permission:setDefIndex") + @PutMapping("/updateDefIndex") + public Result updateDefIndex( + @RequestParam("url") String url, + @RequestParam("component") String component, + @RequestParam("isRoute") Boolean isRoute + ) { + boolean success = sysRoleIndexService.updateDefaultIndex(url, component, isRoute); + if (success) { + return Result.OK("设置成功"); + } else { + return Result.error("设置失败"); + } + } + /** + * 切换默认门户 + * + * @param sysRoleIndex + * @return + */ + @PostMapping(value = "/changeDefHome") + public Result changeDefHome(@RequestBody SysRoleIndex sysRoleIndex,HttpServletRequest request) { + String username = JwtUtil.getUserNameByToken(request); + sysRoleIndex.setRoleCode(username); + sysRoleIndexService.changeDefHome(sysRoleIndex); + // 代码逻辑说明: 切换完成后的homePath获取 + String version = request.getHeader(CommonConstant.VERSION); + String homePath = null; + SysRoleIndex defIndexCfg = sysUserService.getDynamicIndexByUserRole(username, version); + if (defIndexCfg == null) { + defIndexCfg = sysRoleIndexService.initDefaultIndex(); + } + if (oConvertUtils.isNotEmpty(version) && defIndexCfg != null && oConvertUtils.isNotEmpty(defIndexCfg.getUrl())) { + homePath = defIndexCfg.getUrl(); + if (!homePath.startsWith(SymbolConstant.SINGLE_SLASH)) { + homePath = SymbolConstant.SINGLE_SLASH + homePath; + } + } + return Result.OK(homePath); + } + /** + * 获取门户类型 + * + * @return + */ + @GetMapping(value = "/getCurrentHome") + public Result getCurrentHome(HttpServletRequest request) { + String username = JwtUtil.getUserNameByToken(request); + Object homeType = redisUtil.get(DefIndexConst.CACHE_TYPE + username); + return Result.OK(oConvertUtils.getString(homeType,DefIndexConst.HOME_TYPE_MENU)); + } + + /** + * 清除缓存 + * + * @return + */ + @RequestMapping(value = "/cleanDefaultIndexCache") + public Result cleanDefaultIndexCache(HttpServletRequest request) { + sysRoleIndexService.cleanDefaultIndexCache(); + return Result.OK(); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysTableWhiteListController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysTableWhiteListController.java new file mode 100644 index 0000000..cdb3e53 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysTableWhiteListController.java @@ -0,0 +1,153 @@ +package com.ghb.base.modules.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.system.entity.SysTableWhiteList; +import com.ghb.base.modules.system.service.ISysTableWhiteListService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * @Description: 系统表白名单 + * @Author: Ghb-boot + * @Date: 2023-09-12 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "系统表白名单") +@RestController +@RequestMapping("/sys/tableWhiteList") +public class SysTableWhiteListController extends GhbController { + + @Autowired + private ISysTableWhiteListService sysTableWhiteListService; + + /** + * 分页列表查询 + * + * @param sysTableWhiteList + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@RequiresRoles("admin") + @RequiresPermissions("system:tableWhite:list") + @GetMapping(value = "/list") + public Result queryPageList( + SysTableWhiteList sysTableWhiteList, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req + ) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysTableWhiteList, req.getParameterMap()); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = sysTableWhiteListService.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param sysTableWhiteList + * @return + */ + @AutoLog(value = "系统表白名单-添加") + @Operation(summary = "系统表白名单-添加") + //@RequiresRoles("admin") + @RequiresPermissions("system:tableWhite:add") + @PostMapping(value = "/add") + public Result add(@RequestBody SysTableWhiteList sysTableWhiteList) { + if (sysTableWhiteListService.add(sysTableWhiteList)) { + return Result.OK("添加成功!"); + } else { + return Result.error("添加失败!"); + } + } + + /** + * 编辑 + * + * @param sysTableWhiteList + * @return + */ + @AutoLog(value = "系统表白名单-编辑") + @Operation(summary = "系统表白名单-编辑") + //@RequiresRoles("admin") + @RequiresPermissions("system:tableWhite:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody SysTableWhiteList sysTableWhiteList) { + if (sysTableWhiteListService.edit(sysTableWhiteList)) { + return Result.OK("编辑成功!"); + } else { + return Result.error("编辑失败!"); + } + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "系统表白名单-通过id删除") + @Operation(summary = "系统表白名单-通过id删除") +// @RequiresRoles("admin") + @RequiresPermissions("system:tableWhite:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id") String id) { + if (sysTableWhiteListService.deleteByIds(id)) { + return Result.OK("删除成功!"); + } else { + return Result.error("删除失败!"); + } + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "系统表白名单-批量删除") + @Operation(summary = "系统表白名单-批量删除") +// @RequiresRoles("admin") + @RequiresPermissions("system:tableWhite:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids") String ids) { + if (sysTableWhiteListService.deleteByIds(ids)) { + return Result.OK("批量删除成功!"); + } else { + return Result.error("批量删除失败!"); + } + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "系统表白名单-通过id查询") + @Operation(summary = "系统表白名单-通过id查询") +// @RequiresRoles("admin") + @RequiresPermissions("system:tableWhite:queryById") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + SysTableWhiteList sysTableWhiteList = sysTableWhiteListService.getById(id); + return Result.OK(sysTableWhiteList); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysTenantController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysTenantController.java new file mode 100644 index 0000000..f161b45 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysTenantController.java @@ -0,0 +1,1048 @@ +package com.ghb.base.modules.system.controller; + + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.PermissionData; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.PasswordUtil; +import com.ghb.base.common.util.TokenUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.config.sign.annotation.SignatureCheck; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.service.ISysTenantPackService; +import com.ghb.base.modules.system.service.ISysTenantService; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.service.ISysUserTenantService; +import com.ghb.base.modules.system.service.ISysDepartService; +import com.ghb.base.modules.system.vo.SysUserTenantVo; +import com.ghb.base.modules.system.vo.tenant.TenantDepartAuthInfo; +import com.ghb.base.modules.system.vo.tenant.TenantPackModel; +import com.ghb.base.modules.system.vo.tenant.TenantPackUser; +import com.ghb.base.modules.system.vo.tenant.TenantPackUserCount; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * 租户配置信息 + * @author: Ghb-boot + */ +@Slf4j +@RestController +@RequestMapping("/sys/tenant") +public class SysTenantController { + + @Autowired + private ISysTenantService sysTenantService; + + @Autowired + private ISysUserService sysUserService; + + @Autowired + private ISysUserTenantService relationService; + + @Autowired + private ISysTenantPackService sysTenantPackService; + + @Autowired + private BaseCommonService baseCommonService; + + @Autowired + private ISysDepartService sysDepartService; + + /** + * 获取列表数据 + * @param sysTenant + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequiresPermissions("system:tenant:list") + @PermissionData(pageComponent = "system/TenantList") + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> queryPageList(SysTenant sysTenant,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) { + Result> result = new Result>(); + //---author:zhangyafei---date:20210916-----for: 租户管理添加日期范围查询--- + Date beginDate=null; + Date endDate=null; + if(oConvertUtils.isNotEmpty(sysTenant)) { + beginDate=sysTenant.getBeginDate(); + endDate=sysTenant.getEndDate(); + sysTenant.setBeginDate(null); + sysTenant.setEndDate(null); + } + //---author:zhangyafei---date:20210916-----for: 租户管理添加日期范围查询--- + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysTenant, req.getParameterMap()); + //---author:zhangyafei---date:20210916-----for: 租户管理添加日期范围查询--- + if(oConvertUtils.isNotEmpty(sysTenant)){ + queryWrapper.ge(oConvertUtils.isNotEmpty(beginDate),"begin_date",beginDate); + queryWrapper.le(oConvertUtils.isNotEmpty(endDate),"end_date",endDate); + } + //---author:zhangyafei---date:20210916-----for: 租户管理添加日期范围查询--- + Page page = new Page(pageNo, pageSize); + IPage pageList = sysTenantService.page(page, queryWrapper); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 获取租户删除的列表 + * @param sysTenant + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping("/recycleBinPageList") + @RequiresPermissions("system:tenant:recycleBinPageList") + public Result> recycleBinPageList(SysTenant sysTenant,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req){ + Result> result = new Result>(); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysTenantService.getRecycleBinPageList(page, sysTenant); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 添加 + * @param + * @return + */ + @RequiresPermissions("system:tenant:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + public Result add(@RequestBody SysTenant sysTenant) { + Result result = new Result(); + if(sysTenant!=null && oConvertUtils.isNotEmpty(sysTenant.getId()) && sysTenantService.getById(sysTenant.getId())!=null){ + return result.error500("该编号已存在!"); + } + try { + sysTenantService.saveTenant(sysTenant); + //添加默认产品包 + sysTenantPackService.addTenantDefaultPack(sysTenant.getId()); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * [QQYUN-11032]【Ghb】租户套餐管理增加初始化套餐包按钮 + * @param tenantId + * @return + * @author chenrui + * @date 2025/2/6 18:24 + */ + @RequiresPermissions("system:tenant:syncDefaultPack") + @PostMapping(value = "/syncDefaultPack") + public Result syncDefaultPack(@RequestParam(name="tenantId",required=true) Integer tenantId) { + //同步默认产品包 + sysTenantPackService.syncDefaultPack(tenantId); + return Result.OK("操作成功"); + } + + /** + * 编辑 + * @param + * @return + */ + @RequiresPermissions("system:tenant:edit") + @RequestMapping(value = "/edit", method ={RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody SysTenant tenant) { + Result result = new Result(); + SysTenant sysTenant = sysTenantService.getById(tenant.getId()); + if(sysTenant==null) { + return result.error500("未找到对应实体"); + } + if(oConvertUtils.isEmpty(sysTenant.getHouseNumber())){ + tenant.setHouseNumber(RandomUtil.randomStringUpper(6)); + } + boolean ok = sysTenantService.updateById(tenant); + if(ok) { + result.success("修改成功!"); + } + return result; + } + + /** + * 通过id删除 + * @param id + * @return + */ + @RequiresPermissions("system:tenant:delete") + @RequestMapping(value = "/delete", method ={RequestMethod.DELETE, RequestMethod.POST}) + public Result delete(@RequestParam(name="id",required=true) String id) { + //------------------------------------------------------------------ + //如果是saas隔离的情况下,判断当前租户id是否是当前租户下的 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + //获取当前用户 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + SysTenant sysTenant = sysTenantService.getById(id); + + String username = "admin"; + String createdBy = sysUser.getUsername(); + if (!sysTenant.getCreateBy().equals(createdBy) && !username.equals(createdBy)) { + baseCommonService.addLog("未经授权,不能删除非自己创建的租户,租户ID:" + id + ",操作人:" + sysUser.getUsername(), CommonConstant.LOG_TYPE_2, CommonConstant.OPERATE_TYPE_3); + return Result.error("删除租户失败,当前操作人不是租户的创建人!"); + } + } + //------------------------------------------------------------------ + + sysTenantService.removeTenantById(id); + return Result.ok("删除成功"); + } + + /** + * 批量删除 + * @param ids + * @return + */ + @RequiresPermissions("system:tenant:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + Result result = new Result<>(); + if(oConvertUtils.isEmpty(ids)) { + result.error500("未选中租户!"); + }else { + String[] ls = ids.split(","); + // 过滤掉已被引用的租户 + List idList = new ArrayList<>(); + for (String id : ls) { + //------------------------------------------------------------------ + //如果是saas隔离的情况下,判断当前租户id是否是当前租户下的 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + //获取当前用户 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + SysTenant sysTenant = sysTenantService.getById(id); + + String username = "admin"; + String createdBy = sysUser.getUsername(); + if (!sysTenant.getCreateBy().equals(createdBy) && !username.equals(createdBy)) { + baseCommonService.addLog("未经授权,不能删除非自己创建的租户,租户ID:" + id + ",操作人:" + sysUser.getUsername(), CommonConstant.LOG_TYPE_2, CommonConstant.OPERATE_TYPE_3); + return Result.error("删除租户失败,当前操作人不是租户的创建人!"); + } + } + //------------------------------------------------------------------ + + idList.add(Integer.parseInt(id)); + } + // 代码逻辑说明: 【QQYUN-5723】3、租户删除直接删除,不删除中间表------------ + sysTenantService.removeByIds(idList); + result.success("删除成功!"); + } + return result; + } + + /** + * 通过id查询 + * @param id + * @return + */ + @SignatureCheck + @RequestMapping(value = "/queryById", method = RequestMethod.GET) + public Result queryById(@RequestParam(name="id",required=true) String id) { + log.info("【敏感接口】查询租户信息,租户ID:{}", id); + + Result result = new Result(); + if(oConvertUtils.isEmpty(id)){ + result.error500("参数为空!"); + } + //------------------------------------------------------------------------------------------------ + //获取登录用户信息 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】, admin给特权可以管理所有租户 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && !"admin".equals(sysUser.getUsername())){ + Integer loginSessionTenant = oConvertUtils.getInt(TenantContext.getTenant()); + if(loginSessionTenant!=null && !loginSessionTenant.equals(Integer.valueOf(id))){ + result.error500("无权限访问他人租户!"); + return result; + } + } + //------------------------------------------------------------------------------------------------ + SysTenant sysTenant = sysTenantService.getById(id); + if(sysTenant==null) { + result.error500("未找到对应实体"); + }else { + result.setResult(sysTenant); + result.setSuccess(true); + } + return result; + } + + + /** + * 查询有效的 租户数据 + * @return + */ + @RequiresPermissions("system:tenant:queryList") + @RequestMapping(value = "/queryList", method = RequestMethod.GET) + public Result> queryList(@RequestParam(name="ids",required=false) String ids) { + Result> result = new Result>(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysTenant::getStatus, 1); + if(oConvertUtils.isNotEmpty(ids)){ + query.in(SysTenant::getId, ids.split(",")); + } + //此处查询忽略时间条件 + List ls = sysTenantService.list(query); + result.setSuccess(true); + result.setResult(ls); + return result; + } + + /** + * 产品包分页列表查询 + * + * @param sysTenantPack + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @GetMapping(value = "/packList") + @RequiresPermissions("system:tenant:packList") + public Result> queryPackPageList(SysTenantPack sysTenantPack, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysTenantPack, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysTenantPackService.page(page, queryWrapper); + List records = pageList.getRecords(); + if (null != records && records.size() > 0) { + pageList.setRecords(sysTenantPackService.setPermissions(records)); + } + return Result.OK(pageList); + } + + /** + * 创建租户产品包 + * + * @param sysTenantPack + * @return + */ + @PostMapping(value = "/addPackPermission") + @RequiresPermissions("system:tenant:add:pack") + public Result addPackPermission(@RequestBody SysTenantPack sysTenantPack) { + sysTenantPackService.addPackPermission(sysTenantPack); + return Result.ok("创建租户产品包成功"); + } + + /** + * 创建租户产品包 + * + * @param sysTenantPack + * @return + */ + @PutMapping(value = "/editPackPermission") + @RequiresPermissions("system:tenant:edit:pack") + public Result editPackPermission(@RequestBody SysTenantPack sysTenantPack) { + sysTenantPackService.editPackPermission(sysTenantPack); + return Result.ok("修改租户产品包成功"); + } + + /** + * 批量删除用户菜单 + * + * @param ids + * @return + */ + @DeleteMapping("/deleteTenantPack") + @RequiresPermissions("system:tenant:delete:pack") + public Result deleteTenantPack(@RequestParam(value = "ids") String ids) { + sysTenantPackService.deleteTenantPack(ids); + return Result.ok("删除租户产品包成功"); + } + + + + //===========【低代码应用,前端专用接口 —— 加入限制只能维护和查看自己拥有的租户】========================================================== + /** + * 查询当前用户的所有有效租户【低代码应用专用接口】 + * @return + */ + @RequestMapping(value = "/getCurrentUserTenant", method = RequestMethod.GET) + public Result> getCurrentUserTenant() { + Result> result = new Result>(); + try { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + // 代码逻辑说明: [QQYUN-3371]租户逻辑改造,改成关系表------------ + List tenantIdList = relationService.getTenantIdsByUserId(sysUser.getId()); + Map map = new HashMap(5); + if (null!=tenantIdList && tenantIdList.size()>0) { + // 该方法仅查询有效的租户,如果返回0个就说明所有的租户均无效。 + List tenantList = sysTenantService.queryEffectiveTenant(tenantIdList); + map.put("list", tenantList); + } + result.setSuccess(true); + result.setResult(map); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("查询失败!"); + } + return result; + } + + /** + * 邀请用户【低代码应用专用接口】 + * @param ids + * @param phone + * @return + */ + @SignatureCheck + @PutMapping("/invitationUserJoin") + @RequiresPermissions("system:tenant:invitation:user") + public Result invitationUserJoin(@RequestParam("ids") String ids,@RequestParam(value = "phone", required = false) String phone, @RequestParam(value = "username", required = false) String username){ + if(oConvertUtils.isEmpty(phone) && oConvertUtils.isEmpty(username)){ + return Result.error("手机号和用户账号不能同时为空!"); + } + sysTenantService.invitationUserJoin(ids,phone,username); + return Result.ok("邀请用户成功"); + } + + /** + * 获取用户列表数据【低代码应用专用接口】 + * @param user + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequestMapping(value = "/getTenantUserList", method = RequestMethod.GET) + @RequiresPermissions("system:tenant:user:list") + public Result> getTenantUserList(SysUser user, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name="userTenantId") String userTenantId, + HttpServletRequest req) { + Result> result = new Result<>(); + Page page = new Page<>(pageNo, pageSize); + Page pageList = relationService.getPageUserList(page,Integer.valueOf(userTenantId),user); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 请离用户租户【低代码应用专用接口】 + * @param userIds + * @param tenantId + * @return + */ + @PutMapping("/leaveTenant") + @RequiresPermissions("system:tenant:leave") + public Result leaveTenant(@RequestParam("userIds") String userIds, + @RequestParam("tenantId") String tenantId){ + Result result = new Result<>(); + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && !"admin".equals(sysUser.getUsername())){ + Integer loginSessionTenant = oConvertUtils.getInt(TenantContext.getTenant()); + if(loginSessionTenant!=null && !loginSessionTenant.equals(Integer.valueOf(tenantId))){ + result.error500("无权限访问他人租户!"); + return result; + } + } + sysTenantService.leaveTenant(userIds,tenantId); + return Result.ok("请离成功"); + } + + /** + * 编辑(只允许修改自己拥有的租户)【低代码应用专用接口】 + * @param + * @return + */ + @RequestMapping(value = "/editOwnTenant", method ={RequestMethod.PUT, RequestMethod.POST}) + public Result editOwnTenant(@RequestBody SysTenant tenant,HttpServletRequest req) { + Result result = new Result(); + String tenantId = TokenUtils.getTenantIdByRequest(req); + if(!tenantId.equals(tenant.getId().toString())){ + return result.error500("无权修改他人租户!"); + } + + SysTenant sysTenant = sysTenantService.getById(tenant.getId()); + if(sysTenant==null) { + return result.error500("未找到对应实体"); + } + if(oConvertUtils.isEmpty(sysTenant.getHouseNumber())){ + tenant.setHouseNumber(RandomUtil.randomStringUpper(6)); + } + boolean ok = sysTenantService.updateById(tenant); + if(ok) { + result.success("修改成功!"); + } + return result; + } + + /** + * 创建租户并且将用户保存到中间表【低代码应用专用接口】 + * @param sysTenant + */ + @PostMapping("/saveTenantJoinUser") + public Result saveTenantJoinUser(@RequestBody SysTenant sysTenant){ + Result result = new Result<>(); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + Integer tenantId = sysTenantService.saveTenantJoinUser(sysTenant, sysUser.getId()); + result.setSuccess(true); + result.setMessage("创建成功"); + result.setResult(tenantId); + return result; + } + + /** + * 申请加入租户通过门牌号【低代码应用专用接口】 + * @param sysTenant + */ + @SignatureCheck + @PostMapping("/joinTenantByHouseNumber") + public Result joinTenantByHouseNumber(@RequestBody SysTenant sysTenant){ + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + Integer tenantId = sysTenantService.joinTenantByHouseNumber(sysTenant, sysUser.getId()); + Result result = new Result<>(); + if(tenantId != 0){ + result.setMessage("申请加入组织成功"); + result.setSuccess(true); + result.setResult(tenantId); + return result; + }else{ + result.setMessage("该门牌号不存在"); + result.setSuccess(false); + return result; + } + } + + /** + * 分页获取租户用户数据(vue3用户租户页面)【低代码应用专用接口】 + * + * @param pageNo + * @param pageSize + * @param userTenantStatus + * @param type + * @param req + * @return + */ + @GetMapping("/getUserTenantPageList") + //@RequiresPermissions("system:tenant:tenantPageList") + public Result> getUserTenantPageList(@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "userTenantStatus") String userTenantStatus, + @RequestParam(name = "type", required = false) String type, + SysUser user, + HttpServletRequest req) { + Page page = new Page(pageNo, pageSize); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String tenantId = oConvertUtils.getString(TenantContext.getTenant(), "0"); + IPage list = relationService.getUserTenantPageList(page, Arrays.asList(userTenantStatus.split(SymbolConstant.COMMA)), user, Integer.valueOf(tenantId)); + return Result.ok(list); + } + + /** + * 通过用户id获取租户列表【低代码应用专用接口】 + * + * @param userTenantStatus 关系表的状态 + * @return + */ + @GetMapping("/getTenantListByUserId") + //@RequiresPermissions("system:tenant:getTenantListByUserId") + public Result> getTenantListByUserId(@RequestParam(name = "userTenantStatus", required = false) String userTenantStatus) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + List list = null; + if (oConvertUtils.isNotEmpty(userTenantStatus)) { + list = Arrays.asList(userTenantStatus.split(SymbolConstant.COMMA)); + } + //租户状态,用户id,租户用户关系状态 + List sysTenant = relationService.getTenantListByUserId(sysUser.getId(), list); + return Result.ok(sysTenant); + } + + /** + * 【敲敲云管理员】 同意申请者加入租户 + * + * 更新用户租户关系状态【低代码应用专用接口】 + */ + @PutMapping("/updateUserTenantStatus") + @RequiresPermissions("system:tenant:updateUserTenantStatus") + public Result updateUserTenantStatus(@RequestBody SysUserTenant userTenant) { + String tenantId = TenantContext.getTenant(); + if (oConvertUtils.isEmpty(tenantId)) { + return Result.error("未找到当前租户信息"); + } + relationService.updateUserTenantStatus(userTenant.getUserId(), tenantId, userTenant.getStatus()); + return Result.ok("更新用户租户状态成功"); + } + + /** + * 同意或者拒绝用户加入(敲敲云专用) + * @param userTenant + * @return + */ + @PutMapping("/agreeOrRejectUserJoin") + public Result agreeOrRejectUserJoin(@RequestBody SysUserTenant userTenant) { + String tenantId = TenantContext.getTenant(); + if (oConvertUtils.isEmpty(tenantId)) { + return Result.error("未找到当前租户信息"); + } + sysTenantPackService.izHaveManageUserAuth(tenantId); + relationService.updateUserTenantStatus(userTenant.getUserId(), tenantId, userTenant.getStatus()); + return Result.ok("更新用户租户状态成功"); + } + + /** + * 注销租户【低代码应用专用接口】 + * + * @param sysTenant + * @return + */ + @PutMapping("/cancelTenant") + //@RequiresPermissions("system:tenant:cancelTenant") + public Result cancelTenant(@RequestBody SysTenant sysTenant,HttpServletRequest request) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + SysTenant tenant = sysTenantService.getById(sysTenant.getId()); + if (null == tenant) { + return Result.error("未找到当前租户信息"); + } + if (!sysUser.getUsername().equals(tenant.getCreateBy())) { + return Result.error("无权限,只能注销自己创建的租户!"); + } + SysUser userById = sysUserService.getById(sysUser.getId()); + String loginPassword = request.getParameter("loginPassword"); + String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(),loginPassword, userById.getSalt()); + if (!passwordEncode.equals(userById.getPassword())) { + return Result.error("密码不正确"); + } + sysTenantService.removeById(sysTenant.getId()); + return Result.ok("注销成功"); + } + + /** + * 获取租户用户不同状态下的数量【低代码应用专用接口】 + * @return + */ + @GetMapping("/getTenantStatusCount") + public Result getTenantStatusCount(@RequestParam(value = "status",defaultValue = "1") String status, HttpServletRequest req){ + String tenantId = TokenUtils.getTenantIdByRequest(req); + if (null == tenantId) { + return Result.error("未找到当前租户信息"); + } + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getTenantId,tenantId); + query.eq(SysUserTenant::getStatus,status); + long count = relationService.count(query); + return Result.ok(count); + } + + /** + * 用户取消租户申请【低代码应用专用接口】 + * @param tenantId + * @return + */ + @PutMapping("/cancelApplyTenant") + public Result cancelApplyTenant(@RequestParam("tenantId") String tenantId){ + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + sysTenantService.leaveTenant(sysUser.getId(),tenantId); + return Result.ok("取消申请成功"); + } + + //===========【低代码应用,前端专用接口 —— 加入限制只能维护和查看自己拥有的租户】========================================================== + + /** + * 彻底删除租户 + * @param ids + * @return + */ + @DeleteMapping("/deleteLogicDeleted") + @RequiresPermissions("system:tenant:deleteTenantLogic") + public Result deleteTenantLogic(@RequestParam("ids") String ids){ + sysTenantService.deleteTenantLogic(ids); + return Result.ok("彻底删除成功"); + } + + /** + * 还原删除的租户 + * @param ids + * @return + */ + @PutMapping("/revertTenantLogic") + @RequiresPermissions("system:tenant:revertTenantLogic") + public Result revertTenantLogic(@RequestParam("ids") String ids){ + sysTenantService.revertTenantLogic(ids); + return Result.ok("还原成功"); + } + + /** + * 退出租户【低代码应用专用接口】 + * @param sysTenant + * @param request + * @return + */ + @DeleteMapping("/exitUserTenant") + public Result exitUserTenant(@RequestBody SysTenant sysTenant,HttpServletRequest request){ + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //验证用户是否已存在 + Integer count = relationService.userTenantIzExist(sysUser.getId(),sysTenant.getId()); + if (count == 0) { + return Result.error("此租户下没有当前用户"); + } + //验证密码 + String loginPassword = request.getParameter("loginPassword"); + SysUser userById = sysUserService.getById(sysUser.getId()); + String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(),loginPassword, userById.getSalt()); + if (!passwordEncode.equals(userById.getPassword())) { + return Result.error("密码不正确"); + } + //退出登录 + sysTenantService.exitUserTenant(sysUser.getId(),sysUser.getUsername(),String.valueOf(sysTenant.getId())); + return Result.ok("退出租户成功"); + } + + /** + * 变更租户拥有者【低代码应用专用接口】 + * @param userId + * @return + */ + @PostMapping("/changeOwenUserTenant") + public Result changeOwenUserTenant(@RequestParam("userId") String userId, + @RequestParam("tenantId") String tenantId){ + sysTenantService.changeOwenUserTenant(userId,tenantId); + return Result.ok("退出租户成功"); + } + + /** + * 邀请用户到租户,通过手机号匹配 【低代码应用专用接口】 + * @param phone + * @param departId + * @return + */ + @SignatureCheck + @RequiresPermissions("system:tenant:invitation:user") + @PostMapping("/invitationUser") + public Result invitationUser(@RequestParam(name="phone") String phone, + @RequestParam(name="departId",defaultValue = "") String departId){ + return sysTenantService.invitationUser(phone,departId); + } + + + /** + * 获取 租户产品包-3个默认admin的人员数量 + * @param tenantId + * @return + */ + @GetMapping("/loadAdminPackCount") + public Result> loadAdminPackCount(@RequestParam("tenantId") Integer tenantId){ + List list = sysTenantService.queryTenantPackUserCount(tenantId); + return Result.ok(list); + } + + /** + * 查询租户产品包信息 + * @param packModel + * @return + */ + @GetMapping("/getTenantPackInfo") + public Result getTenantPackInfo(TenantPackModel packModel){ + TenantPackModel tenantPackModel = sysTenantService.queryTenantPack(packModel); + return Result.ok(tenantPackModel); + } + + + /** + * 添加用户和产品包的关系数据 + * @param sysTenantPackUser + * @return + */ + @PostMapping("/addTenantPackUser") + public Result addTenantPackUser(@RequestBody SysTenantPackUser sysTenantPackUser){ + sysTenantService.addBatchTenantPackUser(sysTenantPackUser); + return Result.ok("操作成功!"); + } + + /** + * 从产品包移除用户 + * @param sysTenantPackUser + * @return + */ + @PutMapping("/deleteTenantPackUser") + public Result deleteTenantPackUser(@RequestBody SysTenantPackUser sysTenantPackUser){ + sysTenantService.deleteTenantPackUser(sysTenantPackUser); + return Result.ok("操作成功!"); + } + + + /** + * 修改申请状态 + * @param sysTenant + * @return + */ + @PutMapping("/updateApplyStatus") + public Result updateApplyStatus(@RequestBody SysTenant sysTenant){ + SysTenant entity = this.sysTenantService.getById(sysTenant.getId()); + if(entity==null){ + return Result.error("租户不存在!"); + } + entity.setApplyStatus(sysTenant.getApplyStatus()); + sysTenantService.updateById(entity); + return Result.ok(""); + } + + + /** + * 获取产品包人员申请列表 + * @param tenantId + * @return + */ + @GetMapping("/getTenantPackApplyUsers") + public Result getTenantPackApplyUsers(@RequestParam("tenantId") Integer tenantId){ + List list = sysTenantService.getTenantPackApplyUsers(tenantId); + return Result.ok(list); + } + + /** + * 个人 申请成为管理员 + * @param sysTenantPackUser + * @return + */ + @PostMapping("/doApplyTenantPackUser") + public Result doApplyTenantPackUser(@RequestBody SysTenantPackUser sysTenantPackUser){ + sysTenantService.doApplyTenantPackUser(sysTenantPackUser); + return Result.ok("申请成功!"); + } + + /** + * 申请通过 成为管理员 + * @param sysTenantPackUser + * @return + */ + @PutMapping("/passApply") + public Result passApply(@RequestBody SysTenantPackUser sysTenantPackUser){ + sysTenantService.passApply(sysTenantPackUser); + return Result.ok("操作成功!"); + } + + /** + * 拒绝申请 成为管理员 + * @param sysTenantPackUser + * @return + */ + @PutMapping("/deleteApply") + public Result deleteApply(@RequestBody SysTenantPackUser sysTenantPackUser){ + sysTenantService.deleteApply(sysTenantPackUser); + return Result.ok(""); + } + + /** + * 查看是否已经申请过了超级管理员 + * @return + */ + @GetMapping("/getApplySuperAdminCount") + public Result getApplySuperAdminCount(){ + Long count = sysTenantService.getApplySuperAdminCount(); + return Result.ok(count); + } + + /** + * 进入应用组织页面 查询租户信息及当前用户是否有 管理员的权限-- + * @param id + * @return + */ + @RequestMapping(value = "/queryTenantAuthInfo", method = RequestMethod.GET) + public Result queryTenantAuthInfo(@RequestParam(name="id",required=true) String id) { + TenantDepartAuthInfo info = sysTenantService.getTenantDepartAuthInfo(Integer.parseInt(id)); + return Result.ok(info); + } + + /** + * 获取产品包下的用户列表(分页) + * @param tenantId + * @param packId + * @param status + * @param pageNo + * @param pageSize + * @return + */ + @GetMapping("/queryTenantPackUserList") + public Result> queryTenantPackUserList(@RequestParam("tenantId") String tenantId, + @RequestParam("packId") String packId, + @RequestParam("status") Integer status, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize){ + Page page = new Page<>(pageNo,pageSize); + IPage pageList = sysTenantService.queryTenantPackUserList(tenantId,packId,status,page); + return Result.ok(pageList); + } + + /** + * 获取当前租户下的部门和成员数量 + */ + @GetMapping("/getTenantCount") + public Result> getTenantCount(HttpServletRequest request){ + Map map = new HashMap<>(); + // 代码逻辑说明: 【QQYUN-7177】用户数量显示不正确--- + if(oConvertUtils.isEmpty(TokenUtils.getTenantIdByRequest(request))){ + return Result.error("当前租户为空,禁止访问!"); + } + Integer tenantId = oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(request)); + Long userCount = relationService.getUserCount(tenantId,CommonConstant.USER_TENANT_NORMAL); + map.put("userCount",userCount); + LambdaQueryWrapper departQuery = new LambdaQueryWrapper<>(); + departQuery.eq(SysDepart::getDelFlag,String.valueOf(CommonConstant.DEL_FLAG_0)); + departQuery.eq(SysDepart::getTenantId,tenantId); + //部门状态暂时没用,先注释掉 + //departQuery.eq(SysDepart::getStatus,CommonConstant.STATUS_1); + long departCount = sysDepartService.count(departQuery); + map.put("departCount",departCount); + return Result.ok(map); + } + + /** + * 通过用户id获取租户列表(分页) + * + * @param sysUserTenantVo + * @return + */ + @GetMapping("/getTenantPageListByUserId") + public Result> getTenantPageListByUserId(SysUserTenantVo sysUserTenantVo, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + List list = null; + String userTenantStatus = sysUserTenantVo.getUserTenantStatus(); + if (oConvertUtils.isNotEmpty(userTenantStatus)) { + list = Arrays.asList(userTenantStatus.split(SymbolConstant.COMMA)); + } + Page page = new Page<>(pageNo,pageSize); + IPage pageList = relationService.getTenantPageListByUserId(page,sysUser.getId(),list,sysUserTenantVo); + return Result.ok(pageList); + } + + /** 【被邀请人使用】 + * 同意或拒绝加入租户 + */ + @SignatureCheck + @PutMapping("/agreeOrRefuseJoinTenant") + public Result agreeOrRefuseJoinTenant(@RequestParam("tenantId") Integer tenantId, + @RequestParam("status") String status){ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + SysTenant tenant = sysTenantService.getById(tenantId); + if(null == tenant){ + return Result.error("不存在该组织"); + } + SysUserTenant sysUserTenant = relationService.getUserTenantByTenantId(userId, tenantId); + if (null == sysUserTenant) { + return Result.error("该用户不存在该组织中,无权修改"); + } + String content = ""; + SysUser user = new SysUser(); + user.setUsername(sysUserTenant.getCreateBy()); + String realname = oConvertUtils.getString(sysUser.getRealname(),sysUser.getUsername()); + //成功加入 + if(CommonConstant.USER_TENANT_NORMAL.equals(status)){ + //修改租户状态 + relationService.agreeJoinTenant(userId,tenantId); + content = content + realname + "已同意您发送的加入 " + tenant.getName() + " 的邀请"; + sysTenantService.sendMsgForAgreeAndRefuseJoin(user, content); + return Result.OK("您已同意该组织的邀请"); + }else if(CommonConstant.USER_TENANT_REFUSE.equals(status)){ + //直接删除关系表即可 + relationService.refuseJoinTenant(userId,tenantId); + content = content + realname + "拒绝了您发送的加入 " + tenant.getName() + " 的邀请"; + sysTenantService.sendMsgForAgreeAndRefuseJoin(user, content); + return Result.OK("您已成功拒绝该组织的邀请"); + } + return Result.error("类型不匹配,禁止修改数据"); + } + + /** + * 目前只给敲敲云租户下删除用户使用 + * + * 根据密码删除用户 + */ + @DeleteMapping("/deleteUserByPassword") + public Result deleteUserByPassword(@RequestBody SysUser sysUser,HttpServletRequest request){ + Integer tenantId = oConvertUtils.getInteger(TokenUtils.getTenantIdByRequest(request), null); + sysTenantService.deleteUserByPassword(sysUser, tenantId); + return Result.ok("删除用户成功"); + } + + /** + * 查询当前用户的所有有效租户【知识库专用接口】 + * @return + */ + @RequestMapping(value = "/getCurrentUserTenantForFile", method = RequestMethod.GET) + public Result> getCurrentUserTenantForFile() { + Result> result = new Result>(); + try { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + List tenantList = sysTenantService.getTenantListByUserId(sysUser.getId()); + Map map = new HashMap<>(5); + //在开启saas租户隔离的时候并且租户数据不为空,则返回租户信息 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && CollectionUtil.isNotEmpty(tenantList)) { + map.put("list", tenantList); + } + result.setSuccess(true); + result.setResult(map); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("查询失败!"); + } + return result; + } + + /** + * 目前只给敲敲云人员与部门下的用户删除使用 + * + * 删除用户 + */ + @DeleteMapping("/deleteUser") + public Result deleteUser(@RequestBody SysUser sysUser,HttpServletRequest request){ + Integer tenantId = oConvertUtils.getInteger(TokenUtils.getTenantIdByRequest(request), null); + sysTenantService.deleteUser(sysUser, tenantId); + return Result.ok("删除用户成功"); + } + + /** + * 根据租户id和用户id获取用户的产品包列表和当前用户下的产品包id + * + * @param tenantId + * @param request + * @return + */ + @GetMapping("/listPackByTenantUserId") + public Result> listPackByTenantUserId(@RequestParam("tenantId") String tenantId, + @RequestParam("userId") String userId, + HttpServletRequest request) { + if (null == tenantId) { + return null; + } + List list = sysTenantPackService.getPackListByTenantId(tenantId); + List userPackIdList = sysTenantPackService.getPackIdByUserIdAndTenantId(userId, oConvertUtils.getInt(tenantId)); + Map map = new HashMap<>(5); + map.put("packList", list); + map.put("userPackIdList", userPackIdList); + return Result.ok(map); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUgroupController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUgroupController.java new file mode 100644 index 0000000..d69a26c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUgroupController.java @@ -0,0 +1,173 @@ +package com.ghb.base.modules.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.system.entity.SysUgroup; +import com.ghb.base.modules.system.service.ISysUgroupService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Arrays; +import java.util.Date; + /** + * @Description: 用户组表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +@Tag(name="用户组表") +@RestController +@RequestMapping("/sys/ugroup") +@Slf4j +public class SysUgroupController extends GhbController { + @Autowired + private ISysUgroupService sysUgroupService; + + + /** + * 分页列表查询 + * + * @param sysUgroup + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "用户组表-分页列表查询") + @Operation(summary="用户组表-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(SysUgroup sysUgroup, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + + + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysUgroup, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysUgroupService.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param sysUgroup + * @return + */ + @AutoLog(value = "用户组表-添加") + @Operation(summary="用户组表-添加") + @RequiresPermissions("system:sys_ugroup:add") + @PostMapping(value = "/add") + public Result add(@RequestBody SysUgroup sysUgroup) { + Result result = new Result(); + try { + sysUgroup.setCreateTime(new Date()); + sysUgroupService.save(sysUgroup); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑 + * + * @param sysUgroup + * @return + */ + @AutoLog(value = "用户组表-编辑") + @Operation(summary="用户组表-编辑") + @RequiresPermissions("system:sys_ugroup:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody SysUgroup sysUgroup) { + sysUgroupService.updateById(sysUgroup); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "用户组表-通过id删除") + @Operation(summary="用户组表-通过id删除") + @RequiresPermissions("system:sys_ugroup:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + sysUgroupService.deleteById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "用户组表-批量删除") + @Operation(summary="用户组表-批量删除") + @RequiresPermissions("system:sys_ugroup:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.sysUgroupService.deleteByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "用户组表-通过id查询") + @Operation(summary="用户组表-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + SysUgroup sysUgroup = sysUgroupService.getById(id); + if(sysUgroup==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(sysUgroup); + } + + /** + * 导出excel + * + * @param request + * @param sysUgroup + */ + @RequiresPermissions("system:sys_ugroup:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysUgroup sysUgroup) { + return super.exportXls(request, sysUgroup, SysUgroup.class, "用户组表"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("system:sys_ugroup:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysUgroup.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUgroupUserController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUgroupUserController.java new file mode 100644 index 0000000..5157411 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUgroupUserController.java @@ -0,0 +1,164 @@ +package com.ghb.base.modules.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.modules.system.entity.SysUgroupUser; +import com.ghb.base.modules.system.service.ISysUgroupUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Arrays; + /** + * @Description: 用户组关系表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +@Tag(name="用户组关系表") +@RestController +@RequestMapping("/system/sysUgroupUser") +@Slf4j +public class SysUgroupUserController extends GhbController { + @Autowired + private ISysUgroupUserService sysUgroupUserService; + + /** + * 分页列表查询 + * + * @param sysUgroupUser + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "用户组关系表-分页列表查询") + @Operation(summary="用户组关系表-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(SysUgroupUser sysUgroupUser, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + + + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysUgroupUser, req.getParameterMap()); + Page page = new Page(pageNo, pageSize); + IPage pageList = sysUgroupUserService.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param sysUgroupUser + * @return + */ + @AutoLog(value = "用户组关系表-添加") + @Operation(summary="用户组关系表-添加") + @RequiresPermissions("system:sys_ugroup_user:add") + @PostMapping(value = "/add") + public Result add(@RequestBody SysUgroupUser sysUgroupUser) { + sysUgroupUserService.save(sysUgroupUser); + + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param sysUgroupUser + * @return + */ + @AutoLog(value = "用户组关系表-编辑") + @Operation(summary="用户组关系表-编辑") + @RequiresPermissions("system:sys_ugroup_user:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody SysUgroupUser sysUgroupUser) { + sysUgroupUserService.updateById(sysUgroupUser); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "用户组关系表-通过id删除") + @Operation(summary="用户组关系表-通过id删除") + @RequiresPermissions("system:sys_ugroup_user:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + sysUgroupUserService.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "用户组关系表-批量删除") + @Operation(summary="用户组关系表-批量删除") + @RequiresPermissions("system:sys_ugroup_user:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.sysUgroupUserService.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "用户组关系表-通过id查询") + @Operation(summary="用户组关系表-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + SysUgroupUser sysUgroupUser = sysUgroupUserService.getById(id); + if(sysUgroupUser==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(sysUgroupUser); + } + + /** + * 导出excel + * + * @param request + * @param sysUgroupUser + */ + @RequiresPermissions("system:sys_ugroup_user:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, SysUgroupUser sysUgroupUser) { + return super.exportXls(request, sysUgroupUser, SysUgroupUser.class, "用户组关系表"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("system:sys_ugroup_user:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, SysUgroupUser.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUploadController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUploadController.java new file mode 100644 index 0000000..c21f8c9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUploadController.java @@ -0,0 +1,66 @@ +package com.ghb.base.modules.system.controller; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.util.CommonUtils; +import com.ghb.base.common.util.MinioUtil; +import com.ghb.base.common.util.filter.SsrfFileTypeFilter; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.oss.entity.OssFile; +import com.ghb.base.modules.oss.service.IOssFileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * minio文件上传示例 + * @author: Ghb-boot + */ +@Slf4j +@RestController +@RequestMapping("/sys/upload") +public class SysUploadController { + @Autowired + private IOssFileService ossFileService; + + /** + * 上传 + * @param request + */ + @PostMapping(value = "/uploadMinio") + public Result uploadMinio(HttpServletRequest request) throws Exception { + Result result = new Result<>(); + // 获取业务路径 + String bizPath = request.getParameter("biz"); + // 获取上传文件对象 + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + MultipartFile file = multipartRequest.getFile("file"); + + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(file, bizPath); + + if(oConvertUtils.isEmpty(bizPath)){ + bizPath = ""; + } + // 获取文件名 + String orgName = file.getOriginalFilename(); + orgName = CommonUtils.getFileName(orgName); + String fileUrl = MinioUtil.upload(file,bizPath); + if(oConvertUtils.isEmpty(fileUrl)){ + return Result.error("上传失败,请检查配置信息是否正确!"); + } + //保存文件信息 + OssFile minioFile = new OssFile(); + minioFile.setFileName(orgName); + minioFile.setUrl(fileUrl); + ossFileService.save(minioFile); + result.setMessage(fileUrl); + result.setSuccess(true); + return result; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUserController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUserController.java new file mode 100644 index 0000000..b83528a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUserController.java @@ -0,0 +1,2195 @@ +package com.ghb.base.modules.system.controller; +import org.jeecg.common.util.RedisUtil; + + +import cn.hutool.core.util.RandomUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.RequiresRoles; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.PermissionData; +import org.jeecg.common.base.BaseMap; +import org.jeecg.common.config.TenantContext; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.PasswordConstant; +import com.ghb.base.common.constant.SymbolConstant; +import org.jeecg.common.modules.redis.client.JeecgRedisClient; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.*; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.excelstyle.ExcelExportSysUserStyle; +import com.ghb.base.modules.system.model.DepartIdModel; +import com.ghb.base.modules.system.model.SysUserSysDepPostModel; +import com.ghb.base.modules.system.model.SysUserSysDepartModel; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.util.ImportSysUserCache; +import com.ghb.base.modules.system.vo.SysDepartUsersVO; +import com.ghb.base.modules.system.vo.SysUserExportVo; +import com.ghb.base.modules.system.vo.SysUserGroupVO; +import com.ghb.base.modules.system.vo.SysUserRoleVO; +import com.ghb.base.modules.system.vo.lowapp.DepartAndUserInfo; +import com.ghb.base.modules.system.vo.lowapp.UpdateDepartInfo; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 用户表 前端控制器 + *

+ * + * @Author scott + * @since 2018-12-20 + */ +@Slf4j +@RestController +@RequestMapping("/sys/user") +public class SysUserController { + + @Autowired + private ISysUserService sysUserService; + + @Autowired + private ISysDepartService sysDepartService; + + @Autowired + private ISysUserRoleService sysUserRoleService; + + @Autowired + private ISysUgroupUserService sysUgroupUserService; + + @Autowired + private ISysUserDepartService sysUserDepartService; + + @Autowired + private ISysDepartRoleUserService departRoleUserService; + + @Autowired + private ISysDepartRoleService departRoleService; + + @Autowired + private RedisUtil redisUtil; + + @Value("${ghb.path.upload}") + private String upLoadPath; + + @Autowired + private BaseCommonService baseCommonService; + + @Autowired + private ISysPositionService sysPositionService; + + @Autowired + private ISysUserTenantService userTenantService; + + @Autowired + private JeecgRedisClient JeecgRedisClient; + @Autowired + private GhbBaseConfig GhbBaseConfig; + + /** + * 获取租户下用户数据(支持租户隔离) + * @param user + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @PermissionData(pageComponent = "system/UserList") + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> queryPageList(SysUser user,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(user, req.getParameterMap()); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + String tenantId = oConvertUtils.getString(TenantContext.getTenant(), "-1"); + List userIds = userTenantService.getUserIdsByTenantId(Integer.valueOf(tenantId)); + if (oConvertUtils.listIsNotEmpty(userIds)) { + queryWrapper.in("id", userIds); + }else{ + queryWrapper.eq("id", "通过租户查询不到任何用户"); + } + } + //------------------------------------------------------------------------------------------------ + return sysUserService.queryPageList(req, queryWrapper, pageSize, pageNo); + } + + /** + * 获取系统用户数据(查询全部用户,不做租户隔离) + * + * @param user + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequiresPermissions("system:user:listAll") + @RequestMapping(value = "/listAll", method = RequestMethod.GET) + public Result> queryAllPageList(SysUser user, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(user, req.getParameterMap()); + return sysUserService.queryPageList(req, queryWrapper, pageSize, pageNo); + } + + @RequiresPermissions("system:user:add") + @RequestMapping(value = "/add", method = RequestMethod.POST) + public Result add(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + String selectedRoles = jsonObject.getString("selectedroles"); + String selectedDeparts = jsonObject.getString("selecteddeparts"); + try { + SysUser user = JSON.parseObject(jsonObject.toJSONString(), SysUser.class); + user.setCreateTime(new Date());//设置创建时间 + String salt = oConvertUtils.randomGen(8); + user.setSalt(salt); + String passwordEncode = PasswordUtil.encrypt(user.getUsername(), user.getPassword(), salt); + user.setPassword(passwordEncode); + user.setStatus(1); + user.setDelFlag(CommonConstant.DEL_FLAG_0); + //用户表字段org_code不能在这里设置他的值 + user.setOrgCode(null); + user.setLastPwdUpdateTime(new Date()); + // 保存用户走一个service 保证事务 + //获取租户ids + String relTenantIds = jsonObject.getString("relTenantIds"); + sysUserService.saveUser(user, selectedRoles, selectedDeparts, relTenantIds, false); + baseCommonService.addLog("添加用户,username: " +user.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + @RequiresPermissions("system:user:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + try { + SysUser sysUser = sysUserService.getById(jsonObject.getString("id")); + baseCommonService.addLog("编辑用户,username: " +sysUser.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + if(sysUser==null) { + result.error500("未找到对应实体"); + }else { + SysUser user = JSON.parseObject(jsonObject.toJSONString(), SysUser.class); + user.setUpdateTime(new Date()); + //String passwordEncode = PasswordUtil.encrypt(user.getUsername(), user.getPassword(), sysUser.getSalt()); + user.setPassword(sysUser.getPassword()); + String roles = jsonObject.getString("selectedroles"); + String departs = jsonObject.getString("selecteddeparts"); + if(oConvertUtils.isEmpty(departs)){ + //vue3.0前端只传递了departIds + departs=user.getDepartIds(); + } + //用户表字段org_code不能在这里设置他的值 + user.setOrgCode(null); + // 修改用户走一个service 保证事务 + //获取租户ids + String relTenantIds = jsonObject.getString("relTenantIds"); + String updateFromPage = jsonObject.getString("updateFromPage"); + //update-begin---author:wangshuai---date:2025-11-12---for:【JHHB-776】用户编辑,应该从数据库查出老数据,页面传递什么字段,把这些字段覆盖数据库查询结果,再更新--- + oConvertUtils.copyNonNullFields(user, sysUser); + sysUserService.editUser(sysUser, roles, departs, relTenantIds, updateFromPage); + //update-end---author:wangshuai---date:2025-11-12---for:【JHHB-776】用户编辑,应该从数据库查出老数据,页面传递什么字段,把这些字段覆盖数据库查询结果,再更新--- + result.success("修改成功!"); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 添加用户【后台租户模式专用,敲敲云不要用这个】 + * + * @param jsonObject + * @return + */ + @RequiresPermissions("system:user:addTenantUser") + @RequestMapping(value = "/addTenantUser", method = RequestMethod.POST) + public Result addTenantUser(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + String selectedRoles = jsonObject.getString("selectedroles"); + String selectedDeparts = jsonObject.getString("selecteddeparts"); + try { + SysUser user = JSON.parseObject(jsonObject.toJSONString(), SysUser.class); + user.setCreateTime(new Date());//设置创建时间 + String salt = oConvertUtils.randomGen(8); + user.setSalt(salt); + String passwordEncode = PasswordUtil.encrypt(user.getUsername(), user.getPassword(), salt); + user.setPassword(passwordEncode); + user.setStatus(1); + user.setDelFlag(CommonConstant.DEL_FLAG_0); + //用户表字段org_code不能在这里设置他的值 + user.setOrgCode(null); + // 保存用户走一个service 保证事务 + //获取租户ids + String relTenantIds = jsonObject.getString("relTenantIds"); + sysUserService.saveUser(user, selectedRoles, selectedDeparts, relTenantIds, true); + baseCommonService.addLog("添加用户,username: " + user.getUsername(), CommonConstant.LOG_TYPE_2, 2); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 删除用户 + */ + @RequiresPermissions("system:user:delete") + @RequestMapping(value = "/delete", method = RequestMethod.DELETE) + public Result delete(@RequestParam(name="id",required=true) String id) { + baseCommonService.addLog("删除用户,id: " +id ,CommonConstant.LOG_TYPE_2, 3); + List userNameList = sysUserService.userIdToUsername(Arrays.asList(id)); + this.sysUserService.deleteUser(id); + + if (!userNameList.isEmpty()) { + String joinedString = String.join(",", userNameList); + } + return Result.ok("删除用户成功"); + } + + /** + * 批量删除用户 + */ + @RequiresPermissions("system:user:deleteBatch") + @RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE) + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + baseCommonService.addLog("批量删除用户, ids: " +ids ,CommonConstant.LOG_TYPE_2, 3); + List userNameList = sysUserService.userIdToUsername(Arrays.asList(ids.split(","))); + this.sysUserService.deleteBatchUsers(ids); + + // 用户变更,触发同步工作流 + if (!userNameList.isEmpty()) { + String joinedString = String.join(",", userNameList); + } + return Result.ok("批量删除用户成功"); + } + + /** + * 冻结&解冻用户 + * @param jsonObject + * @return + */ + @RequiresPermissions("system:user:frozenBatch") + @RequestMapping(value = "/frozenBatch", method = RequestMethod.PUT) + public Result frozenBatch(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + try { + String ids = jsonObject.getString("ids"); + sysUserService.checkUserAdminRejectDel(ids); + String status = jsonObject.getString("status"); + String[] arr = ids.split(","); + for (String id : arr) { + if(oConvertUtils.isNotEmpty(id)) { + // 代码逻辑说明: [QQYUN-5577]用户列表-冻结用户,再解冻之后,用户还是无法登陆,有缓存问题 #5066------------ + sysUserService.updateStatus(id,status); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"+e.getMessage()); + } + result.success("操作成功!"); + return result; + + } + /** + * 重置为系统密码接口 + * @param usernames + * @return + */ + @RequiresRoles({"admin"}) + @RequiresPermissions("system:user:resetPassword") + @RequestMapping(value = "/resetPassword", method = RequestMethod.PUT) + public Result resetPassword(@RequestParam(name = "usernames") String usernames) { + Result result = new Result(); + try { + sysUserService.resetToSysPassword(usernames); + result.success("操作成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500(e.getMessage()); + } + return result; + + } + + @RequiresPermissions("system:user:queryById") + @RequestMapping(value = "/queryById", method = RequestMethod.GET) + public Result queryById(@RequestParam(name = "id", required = true) String id) { + Result result = new Result(); + SysUser sysUser = sysUserService.getById(id); + if (sysUser == null) { + result.error500("未找到对应实体"); + } else { + result.setResult(sysUser); + result.setSuccess(true); + } + return result; + } + + @RequiresPermissions("system:user:queryUserRole") + @RequestMapping(value = "/queryUserRole", method = RequestMethod.GET) + public Result> queryUserRole(@RequestParam(name = "userid", required = true) String userid) { + Result> result = new Result<>(); + List list = new ArrayList(); + List userRole = sysUserRoleService.list(new QueryWrapper().lambda().eq(SysUserRole::getUserId, userid)); + if (userRole == null || userRole.size() <= 0) { + result.error500("未找到用户相关角色信息"); + } else { + for (SysUserRole sysUserRole : userRole) { + list.add(sysUserRole.getRoleId()); + } + result.setSuccess(true); + result.setResult(list); + } + return result; + } + + + /** + * 校验用户账号是否唯一
+ * 可以校验其他 需要检验什么就传什么。。。 + * + * @param sysUser + * @return + */ + @RequestMapping(value = "/checkOnlyUser", method = RequestMethod.GET) + public Result checkOnlyUser(SysUser sysUser) { + Result result = new Result<>(); + //如果此参数为false则程序发生异常 + result.setResult(true); + try { + //通过传入信息查询新的用户信息 + sysUser.setPassword(null); + SysUser user = sysUserService.getOne(new QueryWrapper(sysUser)); + if (user != null) { + result.setSuccess(false); + result.setMessage("用户账号已存在"); + return result; + } + + } catch (Exception e) { + result.setSuccess(false); + result.setMessage(e.getMessage()); + return result; + } + result.setSuccess(true); + return result; + } + + /** + * 修改密码 + */ + @RequiresPermissions("system:user:changepwd") + @RequestMapping(value = "/changePassword", method = RequestMethod.PUT) + public Result changePassword(@RequestBody SysUser sysUser, HttpServletRequest request) { + //------------------------------------------------------------------------------------- + //增加 check防止恶意刷短信接口 + String clientIp = IpUtils.getIpAddr(request); + if(!DySmsLimit.canSendSms(clientIp)){ + log.warn("-------- IP地址:{}, 短信接口请求太多,有攻击风险!", clientIp); + return Result.error("短信接口请求太多,请稍后再试!"); + } + //------------------------------------------------------------------------------------- + SysUser u = this.sysUserService.getOne(new LambdaQueryWrapper().eq(SysUser::getUsername, sysUser.getUsername())); + if (u == null) { + return Result.error("用户不存在!"); + } + sysUser.setId(u.getId()); + // 代码逻辑说明: [VUEN-234]修改密码添加敏感日志------------ + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + baseCommonService.addLog("修改用户 "+sysUser.getUsername()+" 的密码,操作人: " +loginUser.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + return sysUserService.changePassword(sysUser); + } + + /** + * 查询指定用户和部门关联的数据 + * + * @param userId + * @return + */ + @RequestMapping(value = "/userDepartList", method = RequestMethod.GET) + public Result> getUserDepartsList(@RequestParam(name = "userId", required = true) String userId) { + Result> result = new Result<>(); + try { + List depIdModelList = this.sysUserDepartService.queryDepartIdsOfUser(userId); + if (depIdModelList != null && depIdModelList.size() > 0) { + result.setSuccess(true); + result.setMessage("查找成功"); + result.setResult(depIdModelList); + } else { + result.setSuccess(false); + result.setMessage("查找失败"); + } + return result; + } catch (Exception e) { + log.error(e.getMessage(), e); + result.setSuccess(false); + result.setMessage("查找过程中出现了异常: " + e.getMessage()); + return result; + } + + } + + /** + * 生成在添加用户情况下没有主键的问题,返回给前端,根据该id绑定部门数据 + * + * @return + */ + @RequestMapping(value = "/generateUserId", method = RequestMethod.GET) + public Result generateUserId() { + Result result = new Result<>(); + //System.out.println("我执行了,生成用户ID=============================="); + String userId = UUID.randomUUID().toString().replace("-", ""); + result.setSuccess(true); + result.setResult(userId); + return result; + } + + /** + * 根据部门id查询用户信息 + * + * @param id + * @return + */ + @RequestMapping(value = "/queryUserByDepId", method = RequestMethod.GET) + public Result> queryUserByDepId(@RequestParam(name = "id", required = true) String id,@RequestParam(name="realname",required=false) String realname) { + Result> result = new Result<>(); + //List userList = sysUserDepartService.queryUserByDepId(id); + SysDepart sysDepart = sysDepartService.getById(id); + List userList = sysUserDepartService.queryUserByDepCode(sysDepart.getOrgCode(),realname); + + //批量查询用户的所属部门 + //step.1 先拿到全部的 useids + //step.2 通过 useids,一次性查询用户的所属部门名字 + List userIds = userList.stream().map(SysUser::getId).collect(Collectors.toList()); + if(userIds!=null && userIds.size()>0){ + Map useDepNames = sysUserService.getDepNamesByUserIds(userIds); + userList.forEach(item->{ + //TODO 临时借用这个字段用于页面展示 + item.setOrgCodeTxt(useDepNames.get(item.getId())); + }); + } + + try { + result.setSuccess(true); + result.setResult(userList); + return result; + } catch (Exception e) { + log.error(e.getMessage(), e); + result.setSuccess(false); + return result; + } + } + + /** + * 用户选择组件 专用 根据用户账号或部门分页查询 + * @param departId + * @param username + * @return + */ + @RequestMapping(value = "/queryUserComponentData", method = RequestMethod.GET) + public Result> queryUserComponentData( + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name = "departId", required = false) String departId, + @RequestParam(name="realname",required=false) String realname, + @RequestParam(name="username",required=false) String username, + @RequestParam(name="isMultiTranslate",required=false) String isMultiTranslate, + @RequestParam(name="id",required = false) String id) { + // 代码逻辑说明: VUEN-1702【禁止问题】sql注入漏洞 + String[] arr = new String[]{departId, realname, username, id}; + SqlInjectionUtil.filterContent(arr, SymbolConstant.SINGLE_QUOTATION_MARK); + IPage pageList = sysUserDepartService.queryDepartUserPageList(departId, username, realname, pageSize, pageNo,id,isMultiTranslate); + return Result.OK(pageList); + } + + /** + * 导出excel + * + * @param request + * @param sysUser + */ + @RequiresPermissions("system:user:export") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(SysUser sysUser,HttpServletRequest request) { + // Step.1 组装查询条件 + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysUser, request.getParameterMap()); + queryWrapper.ne("username", "_reserve_user_external"); + //Step.2 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + // 代码逻辑说明: [03]用户导出,如果选择数据则只导出相关数据-------------------- + String selections = request.getParameter("selections"); + if(!oConvertUtils.isEmpty(selections)){ + queryWrapper.in("id",selections.split(",")); + } + //是否存在部门id + boolean izDepartId = true; + String departId = request.getParameter("departId"); + if (oConvertUtils.isNotEmpty(departId)) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.in(SysUserDepart::getDepId, Arrays.asList(departId.split(","))); + List list = sysUserDepartService.list(query); + List userIds = list.stream().map(SysUserDepart::getUserId).collect(Collectors.toList()); + if (oConvertUtils.listIsNotEmpty(userIds)) { + queryWrapper.in("id", userIds); + }else{ + izDepartId = false; + } + } + List list = new ArrayList<>(); + // 代码逻辑说明: 【JHHB-762】【用户管理】需要支持按组织架构查询用户--- + if(izDepartId){ + List pageList = sysUserService.list(queryWrapper); + list = sysUserService.getDepartAndRoleExportMsg(pageList); + } + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "用户列表"); + mv.addObject(NormalExcelConstants.CLASS, SysUserExportVo.class); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + ExportParams exportParams = new ExportParams("导入规则:\n" + + "1. 用户名为必填项,仅支持新增数据导入;\n" + + "2. 多个部门、角色或负责部门请用英文分号 ; 分隔,如:财务部;研发部;\n" + + "3. 部门层级请用英文斜杠 / 分隔,如:北京公司/财务部/财务一部;\n" + + "4. 部门类型需与部门层级一致,也用 / 分隔,如:公司/部门/部门 或 1/3/3,多个类型用 ; 分隔。机构类型编码:公司(1),子公司(4),部门(3);\n" + + "5. 部门根据用户名匹配,若存在多个则关联最新创建的部门,不存在时自动新增;\n" + + "6. 负责部门与所属部门导入规则一致,若所属部门不包含负责部门,则不关联负责部门;\n" + + "7. 用户主岗位导入时会在部门下自动创建新岗位,职级为空时默认不与岗位建立关联。", "导出人:" + user.getRealname(), "导出信息"); + exportParams.setTitleHeight((short)70); + exportParams.setStyle(ExcelExportSysUserStyle.class); + exportParams.setImageBasePath(upLoadPath); + //导出为xlsx + exportParams.setType(ExcelType.XSSF); + mv.addObject(NormalExcelConstants.PARAMS, exportParams); + mv.addObject(NormalExcelConstants.DATA_LIST, list); + //用户导出支持导出字段 + String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS); + if(oConvertUtils.isNotEmpty(exportFields)){ + mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields); + } + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("system:user:import") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response)throws IOException { + //return ImportOldUserUtil.importOldSysUser(request); + return sysUserService.importSysUser(request); + } + + /** + * @功能:根据id 批量查询 + * @param userIds + * @return + */ + @RequestMapping(value = "/queryByIds", method = RequestMethod.GET) + public Result> queryByIds(@RequestParam(name = "userIds") String userIds) { + Result> result = new Result<>(); + String[] userId = userIds.split(","); + Collection idList = Arrays.asList(userId); + Collection userRole = sysUserService.listByIds(idList); + result.setSuccess(true); + result.setResult(userRole); + return result; + } + + + /** + * @功能:根据id 批量查询 + * @param userNames + * @return + */ + @RequestMapping(value = "/queryByNames", method = RequestMethod.GET) + public Result>> queryByNames(@RequestParam(name = "userNames") String userNames) { + Result>> result = new Result<>(); + String[] names = userNames.split(","); + //update-begin---author:zzl ---date:2026-04-03 for:只返回username和realname字段---- + List userList = sysUserService.lambdaQuery() + .in(SysUser::getUsername, names) + .select(SysUser::getUsername, SysUser::getRealname) + .list(); + List> dataList = userList.stream().map(user -> { + Map map = new java.util.HashMap<>(); + map.put("username", user.getUsername()); + map.put("realname", user.getRealname()); + return map; + }).collect(java.util.stream.Collectors.toList()); + result.setSuccess(true); + result.setResult(dataList); + //update-end---author:zzl ---date:2026-04-03 for:只返回username和realname字段---- + return result; + } + + /** + * @功能:根据userName查询用户以及部门信息 + * @param userName + * @return + */ + @RequestMapping(value = "/queryUserAndDeptByName", method = RequestMethod.GET) + public Result> queryUserAndDeptByName(@RequestParam(name = "userName") String userName) { + Map userInfo= sysUserService.queryUserAndDeptByName(userName); + return Result.ok(userInfo); + } + + /** + * 首页用户重置密码 + */ + @RequiresPermissions("system:user:updatepwd") + @RequestMapping(value = "/updatePassword", method = RequestMethod.PUT) + public Result updatePassword(@RequestBody JSONObject json) { + String username = json.getString("username"); + String oldpassword = json.getString("oldpassword"); + String password = json.getString("password"); + String confirmpassword = json.getString("confirmpassword"); + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + if(!sysUser.getUsername().equals(username)){ + return Result.error("只允许修改自己的密码!"); + } + SysUser user = this.sysUserService.getOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username)); + if(user==null) { + return Result.error("用户不存在!"); + } + // 代码逻辑说明: [VUEN-234]修改密码添加敏感日志------------ + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + baseCommonService.addLog("修改密码,username: " +loginUser.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + return sysUserService.resetPassword(username,oldpassword,password,confirmpassword); + } + + @RequestMapping(value = "/userRoleList", method = RequestMethod.GET) + public Result> userRoleList(@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, HttpServletRequest req) { + Result> result = new Result>(); + Page page = new Page(pageNo, pageSize); + String roleId = req.getParameter("roleId"); + String username = req.getParameter("username"); + String realname = req.getParameter("realname"); + IPage pageList = sysUserService.getUserByRoleId(page,roleId,username,realname); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + + /** + * 给指定角色添加用户 + * + * @param + * @return + */ + @RequiresPermissions("system:user:addUserRole") + @RequestMapping(value = "/addSysUserRole", method = RequestMethod.POST) + public Result addSysUserRole(@RequestBody SysUserRoleVO sysUserRoleVO) { + Result result = new Result(); + //TODO 判断当前操作的角色是当前登录租户下的 + try { + String sysRoleId = sysUserRoleVO.getRoleId(); + for(String sysUserId:sysUserRoleVO.getUserIdList()) { + SysUserRole sysUserRole = new SysUserRole(sysUserId,sysRoleId); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("role_id", sysRoleId).eq("user_id",sysUserId); + SysUserRole one = sysUserRoleService.getOne(queryWrapper); + if(one==null){ + sysUserRoleService.save(sysUserRole); + } + + } + result.setMessage("添加成功!"); + result.setSuccess(true); + return result; + }catch(Exception e) { + log.error(e.getMessage(), e); + result.setSuccess(false); + result.setMessage("出错了: " + e.getMessage()); + return result; + } + } + /** + * 删除指定角色的用户关系 + * @param + * @return + */ + @RequiresPermissions("system:user:deleteRole") + @RequestMapping(value = "/deleteUserRole", method = RequestMethod.DELETE) + public Result deleteUserRole(@RequestParam(name="roleId") String roleId, + @RequestParam(name="userId",required=true) String userId + ) { + Result result = new Result(); + try { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("role_id", roleId).eq("user_id",userId); + sysUserRoleService.remove(queryWrapper); + result.success("删除成功!"); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("删除失败!"); + } + return result; + } + + /** + * 批量删除指定角色的用户关系 + * + * @param + * @return + */ + @RequiresPermissions("system:user:deleteRoleBatch") + @RequestMapping(value = "/deleteUserRoleBatch", method = RequestMethod.DELETE) + public Result deleteUserRoleBatch( + @RequestParam(name="roleId") String roleId, + @RequestParam(name="userIds",required=true) String userIds) { + Result result = new Result(); + try { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("role_id", roleId).in("user_id",Arrays.asList(userIds.split(","))); + sysUserRoleService.remove(queryWrapper); + result.success("删除成功!"); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("删除失败!"); + } + return result; + } + /** + * 给指定用户组添加用户 + * + * @param + * @return + */ + @RequestMapping(value = "/addSysUserGroup", method = RequestMethod.POST) + public Result addSysUserGroup(@RequestBody SysUserGroupVO sysUserGroupVO) { + Result result = new Result(); + try { + String groupId = sysUserGroupVO.getGroupId(); + for(String sysUserId : sysUserGroupVO.getUserIdList()) { + SysUgroupUser sysUgroupUser = new SysUgroupUser(sysUserId,groupId); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("group_id", groupId).eq("user_id",sysUserId); + SysUgroupUser one = sysUgroupUserService.getOne(queryWrapper); + if(one==null){ + sysUgroupUserService.save(sysUgroupUser); + } + + } + result.setMessage("添加成功!"); + result.setSuccess(true); + return result; + }catch(Exception e) { + log.error(e.getMessage(), e); + result.setSuccess(false); + result.setMessage("出错了: " + e.getMessage()); + return result; + } + } + /** + * 删除指定用户组的用户关系 + * @param + * @return + */ + @RequestMapping(value = "/deleteGroupUser", method = RequestMethod.DELETE) + public Result deleteGroupUser(@RequestParam(name="groupId") String groupId, + @RequestParam(name="userId",required=true) String userId + ) { + Result result = new Result(); + try { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("group_id", groupId).eq("user_id",userId); + sysUgroupUserService.remove(queryWrapper); + result.success("删除成功!"); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("删除失败!"); + } + return result; + } + /** + * 批量删除指定用户组下的用户关系 + * + * @param + * @return + */ + @RequestMapping(value = "/deleteUserGroupBatch", method = RequestMethod.DELETE) + public Result deleteUserGroupBatch( + @RequestParam(name="groupId") String groupId, + @RequestParam(name="userIds",required=true) String userIds) { + Result result = new Result(); + try { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("group_id", groupId).in("user_id",Arrays.asList(userIds.split(","))); + sysUgroupUserService.remove(queryWrapper); + result.success("删除成功!"); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("删除失败!"); + } + return result; + } + /** + * 用户组下用户分页列表查询 + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @RequestMapping(value = "/groupUserList", method = RequestMethod.GET) + public Result> groupUserList(@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, HttpServletRequest req) { + Result> result = new Result>(); + Page page = new Page(pageNo, pageSize); + String groupId = req.getParameter("groupId"); + String username = req.getParameter("username"); + String realname = req.getParameter("realname"); + IPage pageList = sysUserService.getUserByUgroupId(page,groupId,username,realname); + result.setSuccess(true); + result.setResult(pageList); + return result; + } + /** + * 部门用户列表 + */ + @RequestMapping(value = "/departUserList", method = RequestMethod.GET) + public Result> departUserList(@RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, HttpServletRequest req) { + Result> result = new Result>(); + Page page = new Page(pageNo, pageSize); + String depId = req.getParameter("depId"); + String username = req.getParameter("username"); + //根据部门ID查询,当前和下级所有的部门IDS + List subDepids = new ArrayList<>(); + //部门id为空时,查询我的部门下所有用户 + if(oConvertUtils.isEmpty(depId)){ + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + int userIdentity = user.getUserIdentity() != null?user.getUserIdentity():CommonConstant.USER_IDENTITY_1; + // 代码逻辑说明: [QQYUN-10775]验证码可以复用 #7674------------ + if(oConvertUtils.isNotEmpty(userIdentity) && userIdentity == CommonConstant.USER_IDENTITY_2 + && oConvertUtils.isNotEmpty(user.getDepartIds())) { + subDepids = sysDepartService.getMySubDepIdsByDepId(user.getDepartIds()); + } + }else{ + subDepids = sysDepartService.getSubDepIdsByDepId(depId); + } + if(subDepids != null && subDepids.size()>0){ + IPage pageList = sysUserService.getUserByDepIds(page,subDepids,username); + //批量查询用户的所属部门 + //step.1 先拿到全部的 useids + //step.2 通过 useids,一次性查询用户的所属部门名字 + List userIds = pageList.getRecords().stream().map(SysUser::getId).collect(Collectors.toList()); + if(userIds!=null && userIds.size()>0){ + Map useDepNames = sysUserService.getDepNamesByUserIds(userIds); + pageList.getRecords().forEach(item -> { + //批量查询用户的所属部门 + item.setOrgCode(useDepNames.get(item.getId())); + }); + } + //设置租户id + page.setRecords(userTenantService.setUserTenantIds(page.getRecords())); + result.setSuccess(true); + result.setResult(pageList); + }else{ + result.setSuccess(true); + result.setResult(null); + } + return result; + } + + + /** + * 根据 orgCode 查询用户,包括子部门下的用户 + * 若某个用户包含多个部门,则会显示多条记录,可自行处理成单条记录 + */ + @GetMapping("/queryByOrgCode") + public Result queryByDepartId( + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "orgCode") String orgCode, + SysUser userParams + ) { + IPage pageList = sysUserService.queryUserByOrgCode(orgCode, userParams, new Page(pageNo, pageSize)); + return Result.ok(pageList); + } + + /** + * 根据 orgCode 查询用户,包括子部门下的用户 【不包含岗位下的用户】 + * 针对通讯录模块做的接口,将多个部门的用户合并成一条记录,并转成对前端友好的格式 + */ + @GetMapping("/queryByOrgCodeForAddressList") + public Result queryByOrgCodeForAddressList( + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "orgCode",required = false) String orgCode, + SysUser userParams + ) { + IPage page = new Page(pageNo, pageSize); + IPage pageList = sysUserService.queryUserByOrgCode(orgCode, userParams, page); + List list = pageList.getRecords(); + + // 记录所有出现过的 user, key = userId + Map hasUser = new HashMap<>(list.size()); + + JSONArray resultJson = new JSONArray(list.size()); + + for (SysUserSysDepartModel item : list) { + String userId = item.getId(); + // userId + JSONObject getModel = hasUser.get(userId); + // 之前已存在过该用户,直接合并数据 + if (getModel != null) { + String departName = getModel.get("departName").toString(); + getModel.put("departName", (departName + " | " + item.getDepartName())); + } else { + // 将用户对象转换为json格式,并将部门信息合并到 json 中 + JSONObject json = JSON.parseObject(JSON.toJSONString(item)); + json.remove("id"); + json.put("userId", userId); + json.put("departId", item.getDepartId()); + json.put("departName", item.getDepartName()); +// json.put("avatar", item.getSysUser().getAvatar()); + resultJson.add(json); + hasUser.put(userId, json); + } + } + + IPage result = new Page<>(pageNo, pageSize, pageList.getTotal()); + result.setRecords(resultJson.toJavaList(JSONObject.class)); + return Result.ok(result); + } + + /** + * 根据 orgCode 查询用户,包括公司、子公司、岗位部门下的用户 + */ + @GetMapping("/queryDepartPostByOrgCode") + public Result queryDepartPostByOrgCode(@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "orgCode",required = false) String orgCode, + SysUser userParams + ) { + IPage page = new Page(pageNo, pageSize); + IPage pageList = sysUserService.queryDepartPostUserByOrgCode(orgCode, userParams, page); + return Result.ok(pageList); + } + + /** + * 根据 orgCode 查询用户信息(部门全路径,主岗位和兼职岗位的信息),包括公司、子公司、部门 + */ + @GetMapping("/queryDepartUserByOrgCode") + public Result> queryDepartUserByOrgCode(@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "orgCode",required = false) String orgCode, + SysUser userParams + ) { + IPage page = new Page(pageNo, pageSize); + IPage pageList = sysUserService.queryDepartUserByOrgCode(orgCode, userParams, page); + return Result.ok(pageList); + } + + /** + * 通讯录点击用户获取用户详情(包含用户基本信息、部门全路径、主岗位兼职岗位全路径) + * + * @param userId + * @return + */ + @GetMapping("/getUserDetailByUserId") + public Result getUserDetailByUserId(@RequestParam(name = "userId") String userId) { + Result result = new Result(); + try { + SysUserSysDepPostModel sysDepPostModel = sysUserService.getUserDetailByUserId(userId); + result.setSuccess(true); + result.setResult(sysDepPostModel); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.setSuccess(false); + result.setMessage("查询失败: " + e.getMessage()); + } + return result; + } + + /** + * 给指定部门添加对应的用户 + */ + @RequiresPermissions("system:user:editDepartWithUser") + @RequestMapping(value = "/editSysDepartWithUser", method = RequestMethod.POST) + public Result editSysDepartWithUser(@RequestBody SysDepartUsersVO sysDepartUsersVO) { + Result result = new Result(); + try { + String sysDepId = sysDepartUsersVO.getDepId(); + boolean updated = false; + for(String sysUserId:sysDepartUsersVO.getUserIdList()) { + SysUserDepart sysUserDepart = new SysUserDepart(null,sysUserId,sysDepId); + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("dep_id", sysDepId).eq("user_id",sysUserId); + SysUserDepart one = sysUserDepartService.getOne(queryWrapper); + if(one==null){ + updated = true; + sysUserDepartService.save(sysUserDepart); + } + } + // 【JHHB-737】更新关系后清空用户缓存 + if (updated) { + redisUtil.removeAll(CacheConstant.SYS_USERS_CACHE); + } + result.setMessage("添加成功!"); + result.setSuccess(true); + return result; + }catch(Exception e) { + log.error(e.getMessage(), e); + result.setSuccess(false); + result.setMessage("出错了: " + e.getMessage()); + return result; + } + } + + /** + * 删除指定机构的用户关系 + */ + @RequiresPermissions("system:user:deleteUserInDepart") + @RequestMapping(value = "/deleteUserInDepart", method = RequestMethod.DELETE) + public Result deleteUserInDepart(@RequestParam(name="depId") String depId, + @RequestParam(name="userId",required=true) String userId + ) { + Result result = new Result(); + try { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("dep_id", depId).eq("user_id",userId); + boolean b = sysUserDepartService.remove(queryWrapper); + if(b){ + List sysDepartRoleList = departRoleService.list(new QueryWrapper().eq("depart_id",depId)); + List roleIds = sysDepartRoleList.stream().map(SysDepartRole::getId).collect(Collectors.toList()); + if(roleIds != null && roleIds.size()>0){ + QueryWrapper query = new QueryWrapper<>(); + query.eq("user_id",userId).in("drole_id",roleIds); + departRoleUserService.remove(query); + } + result.success("删除成功!"); + }else{ + result.error500("当前选中部门与用户无关联关系!"); + } + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("删除失败!"); + } + return result; + } + + /** + * 批量删除指定机构的用户关系 + */ + @RequiresPermissions("system:user:deleteUserInDepartBatch") + @RequestMapping(value = "/deleteUserInDepartBatch", method = RequestMethod.DELETE) + public Result deleteUserInDepartBatch( + @RequestParam(name="depId") String depId, + @RequestParam(name="userIds",required=true) String userIds) { + Result result = new Result(); + try { + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("dep_id", depId).in("user_id",Arrays.asList(userIds.split(","))); + boolean b = sysUserDepartService.remove(queryWrapper); + if(b){ + departRoleUserService.removeDeptRoleUser(Arrays.asList(userIds.split(",")),depId); + }else{ + result.error500("删除失败,目标用户不在当前部门!"); + return result; + } + result.success("删除成功!"); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("删除失败!"); + } + return result; + } + + /** + * 查询当前用户的所有部门/当前部门编码 + * @return + */ + @RequestMapping(value = "/getCurrentUserDeparts", method = RequestMethod.GET) + public Result> getCurrentUserDeparts() { + Result> result = new Result>(); + try { + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + List list = this.sysDepartService.queryUserDeparts(sysUser.getId()); + Map map = new HashMap(5); + map.put("list", list); + map.put("orgCode", sysUser.getOrgCode()); + result.setSuccess(true); + result.setResult(map); + }catch(Exception e) { + log.error(e.getMessage(), e); + result.error500("查询失败!"); + } + return result; + } + + + + + /** + * 用户注册接口 + * + * @param jsonObject + * @param user + * @return + */ + @PostMapping("/register") + public Result userRegister(@RequestBody JSONObject jsonObject, SysUser user) { + Result result = new Result(); + String phone = jsonObject.getString("phone"); + String smscode = jsonObject.getString("smscode"); + + // 代码逻辑说明: VUEN-2245 【漏洞】发现新漏洞待处理20220906 + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; + Object code = redisUtil.get(redisKey); + + String username = jsonObject.getString("username"); + //未设置用户名,则用手机号作为用户名 + if(oConvertUtils.isEmpty(username)){ + username = phone; + } + //未设置密码,则随机生成一个密码 + String password = jsonObject.getString("password"); + if(oConvertUtils.isEmpty(password)){ + password = RandomUtil.randomString(8); + } + String email = jsonObject.getString("email"); + SysUser sysUser1 = sysUserService.getUserByName(username); + if (sysUser1 != null) { + result.setMessage("用户名已注册"); + result.setSuccess(false); + return result; + } + SysUser sysUser2 = sysUserService.getUserByPhone(phone); + if (sysUser2 != null) { + result.setMessage("该手机号已注册"); + result.setSuccess(false); + return result; + } + + if(oConvertUtils.isNotEmpty(email)){ + SysUser sysUser3 = sysUserService.getUserByEmail(email); + if (sysUser3 != null) { + result.setMessage("邮箱已被注册"); + result.setSuccess(false); + return result; + } + } + if(null == code){ + result.setMessage("手机验证码失效,请重新获取"); + result.setSuccess(false); + return result; + } + if (!smscode.equals(code.toString())) { + result.setMessage("手机验证码错误"); + result.setSuccess(false); + return result; + } + + String realname = jsonObject.getString("realname"); + if(oConvertUtils.isEmpty(realname)){ + realname = username; + } + + try { + user.setCreateTime(new Date());// 设置创建时间 + String salt = oConvertUtils.randomGen(8); + String passwordEncode = PasswordUtil.encrypt(username, password, salt); + user.setSalt(salt); + user.setUsername(username); + user.setRealname(realname); + user.setPassword(passwordEncode); + user.setEmail(email); + user.setPhone(phone); + user.setStatus(CommonConstant.USER_UNFREEZE); + user.setDelFlag(CommonConstant.DEL_FLAG_0); + user.setActivitiSync(CommonConstant.ACT_SYNC_1); + user.setLastPwdUpdateTime(new Date()); + sysUserService.addUserWithRole(user,"");//默认临时角色 test + result.success("注册成功"); + } catch (Exception e) { + result.error500("注册失败"); + } + return result; + } + +// /** +// * 根据用户名或手机号查询用户信息 +// * @param +// * @return +// */ +// @GetMapping("/querySysUser") +// public Result> querySysUser(SysUser sysUser) { +// String phone = sysUser.getPhone(); +// String username = sysUser.getUsername(); +// Result> result = new Result>(); +// Map map = new HashMap(); +// if (oConvertUtils.isNotEmpty(phone)) { +// SysUser user = sysUserService.getUserByPhone(phone); +// if(user!=null) { +// map.put("username",user.getUsername()); +// map.put("phone",user.getPhone()); +// result.setSuccess(true); +// result.setResult(map); +// return result; +// } +// } +// if (oConvertUtils.isNotEmpty(username)) { +// SysUser user = sysUserService.getUserByName(username); +// if(user!=null) { +// map.put("username",user.getUsername()); +// map.put("phone",user.getPhone()); +// result.setSuccess(true); +// result.setResult(map); +// return result; +// } +// } +// result.setSuccess(false); +// result.setMessage("验证失败"); +// return result; +// } + + /** + * 用户手机号验证 + */ + @PostMapping("/phoneVerification") + public Result> phoneVerification(@RequestBody JSONObject jsonObject) { + Result> result = new Result>(); + String phone = jsonObject.getString("phone"); + String smscode = jsonObject.getString("smscode"); + // 代码逻辑说明: VUEN-2245 【漏洞】发现新漏洞待处理20220906 + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; + Object code = redisUtil.get(redisKey); + // 代码逻辑说明: 【issues/8567】严重:修改密码存在水平越权问题。--- + if (null == code) { + result.setMessage("短信验证码失效!"); + result.setSuccess(false); + return result; + } + String smsCode = ""; + if (code.toString().contains("code")) { + smsCode = JSONObject.parseObject(code.toString()).getString("code"); + } else { + smsCode = code.toString(); + } + if (!smscode.equals(smsCode)) { + result.setMessage("手机验证码错误"); + result.setSuccess(false); + return result; + } + //设置有效时间 + redisUtil.set(redisKey, code,600); + + //新增查询用户名 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUser::getPhone,phone); + SysUser user = sysUserService.getOne(query); + Map map = new HashMap(5); + map.put("smscode",smscode); + if(null == user){ + //前端根据文字做判断用户是否存在判断,不能修改 + result.setMessage("用户信息不存在"); + result.setSuccess(false); + return result; + } + map.put("username",user.getUsername()); + result.setResult(map); + result.setSuccess(true); + return result; + } + + /** + * 用户更改密码 + */ + @GetMapping("/passwordChange") + public Result passwordChange(@RequestParam(name="username")String username, + @RequestParam(name="password")String password, + @RequestParam(name="smscode")String smscode, + @RequestParam(name="phone") String phone) { + Result result = new Result(); + if(oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(password) || oConvertUtils.isEmpty(smscode) || oConvertUtils.isEmpty(phone) ) { + result.setMessage("重置密码失败!"); + result.setSuccess(false); + return result; + } + + SysUser sysUser=new SysUser(); + // 代码逻辑说明: VUEN-2245 【漏洞】发现新漏洞待处理20220906 + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; + Object object= redisUtil.get(redisKey); + if(null==object) { + result.setMessage("短信验证码失效!"); + result.setSuccess(false); + return result; + } + + // 代码逻辑说明: 【issues/8567】严重:修改密码存在水平越权问题。--- + String redisUsername = ""; + if(object.toString().contains("code")){ + JSONObject jsonObject = JSONObject.parseObject(object.toString()); + object = jsonObject.getString("code"); + redisUsername = jsonObject.getString("username"); + } + //验证是否为当前用户的 + if(oConvertUtils.isNotEmpty(redisUsername) && !username.equals(redisUsername)){ + result.setMessage("此验证码不是当前用户的!"); + result.setSuccess(false); + return result; + } + + if(!smscode.equals(object.toString())) { + result.setMessage("短信验证码不匹配!"); + result.setSuccess(false); + return result; + } + sysUser = this.sysUserService.getOne(new LambdaQueryWrapper().eq(SysUser::getUsername,username).eq(SysUser::getPhone,phone)); + if (sysUser == null) { + result.setMessage("当前用户和绑定的手机号不匹配,无法修改密码!"); + result.setSuccess(false); + return result; + } else { + String salt = oConvertUtils.randomGen(8); + sysUser.setSalt(salt); + String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(), password, salt); + sysUser.setPassword(passwordEncode); + sysUser.setLastPwdUpdateTime(new Date()); + this.sysUserService.updateById(sysUser); + // 代码逻辑说明: [VUEN-234]密码重置添加敏感日志------------ + baseCommonService.addLog("重置 "+username+" 的密码,操作人: " +sysUser.getUsername() ,CommonConstant.LOG_TYPE_2, 2); + result.setSuccess(true); + result.setMessage("密码重置完成!"); + //修改完密码后清空redis + redisUtil.removeAll(redisKey); + return result; + } + } + + + /** + * 根据TOKEN获取用户的部分信息(返回的数据是可供表单设计器使用的数据) + * + * @return + */ + @GetMapping("/getUserSectionInfoByToken") + public Result getUserSectionInfoByToken(HttpServletRequest request, @RequestParam(name = "token", required = false) String token) { + try { + String username = null; + // 如果没有传递token,就从header中获取token并获取用户信息 + if (oConvertUtils.isEmpty(token)) { + username = JwtUtil.getUserNameByToken(request); + } else { + username = JwtUtil.getUsername(token); + } + + log.debug(" ------ 通过令牌获取部分用户信息,当前用户: " + username); + + // 根据用户名查询用户信息 + SysUser sysUser = sysUserService.getUserByName(username); + //update-begin---author:zhangdaihao ---date:2026-04-15 for:【issue/9518】校验外部传入token的签名,防止越权----------- + if (oConvertUtils.isNotEmpty(token)) { + if (sysUser == null || !JwtUtil.verify(token, username, sysUser.getPassword())) { + return Result.error(401, "token校验失败"); + } + } + //update-end---author:zhangdaihao ---date:2026-04-15 for:【issue/9518】校验外部传入token的签名,防止越权----------- + Map map = new HashMap(); + map.put("sysUserId", sysUser.getId()); + map.put("sysUserCode", sysUser.getUsername()); // 当前登录用户登录账号 + map.put("sysUserName", sysUser.getRealname()); // 当前登录用户真实名称 + map.put("sysOrgCode", sysUser.getOrgCode()); // 当前登录用户部门编号 + + // 【QQYUN-12930】设置部门名称 + if (oConvertUtils.isNotEmpty(sysUser.getOrgCode())) { + SysDepart sysDepart = sysDepartService.lambdaQuery().select(SysDepart::getDepartName).eq(SysDepart::getOrgCode, sysUser.getOrgCode()).one(); + if (sysDepart != null) { + map.put("sysOrgName", sysDepart.getDepartName()); // 当前登录用户部门名称 + } + } + + log.debug(" ------ 通过令牌获取部分用户信息,已获取的用户信息: " + map); + + return Result.ok(map); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error(500, "查询失败:" + e.getMessage()); + } + } + + /** + * 【APP端接口】获取用户列表 根据用户名和真实名 模糊匹配 + * @param keyword + * @param pageNo + * @param pageSize + * @return + */ + @GetMapping("/appUserList") + public Result appUserList(@RequestParam(name = "keyword", required = false) String keyword, + @RequestParam(name = "username", required = false) String username, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name = "syncFlow", required = false) String syncFlow) { + try { + //TODO 从查询效率上将不要用mp的封装的page分页查询 建议自己写分页语句 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + if(oConvertUtils.isNotEmpty(syncFlow)){ + query.eq(SysUser::getActivitiSync, CommonConstant.ACT_SYNC_1); + } + query.eq(SysUser::getDelFlag,CommonConstant.DEL_FLAG_0); + if(oConvertUtils.isNotEmpty(username)){ + if(username.contains(",")){ + query.in(SysUser::getUsername,username.split(",")); + }else{ + query.eq(SysUser::getUsername,username); + } + }else{ + query.and(i -> i.like(SysUser::getUsername, keyword).or().like(SysUser::getRealname, keyword)); + } + Page page = new Page<>(pageNo, pageSize); + IPage res = this.sysUserService.page(page, query); + return Result.ok(res); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error(500, "查询失败:" + e.getMessage()); + } + + } + + /** + * 获取被逻辑删除的用户列表,无分页 + * + * @return logicDeletedUserList + */ + @GetMapping("/recycleBin") + public Result getRecycleBin() { + List logicDeletedUserList = sysUserService.queryLogicDeleted(); + if (logicDeletedUserList.size() > 0) { + // 批量查询用户的所属部门 + // step.1 先拿到全部的 userIds + List userIds = logicDeletedUserList.stream().map(SysUser::getId).collect(Collectors.toList()); + // step.2 通过 userIds,一次性查询用户的所属部门名字 + Map useDepNames = sysUserService.getDepNamesByUserIds(userIds); + logicDeletedUserList.forEach(item -> item.setOrgCode(useDepNames.get(item.getId()))); + } + return Result.ok(logicDeletedUserList); + } + + /** + * 还原被逻辑删除的用户 + * + * @param jsonObject + * @return + */ + @RequestMapping(value = "/putRecycleBin", method = RequestMethod.PUT) + public Result putRecycleBin(@RequestBody JSONObject jsonObject, HttpServletRequest request) { + String userIds = jsonObject.getString("userIds"); + if (StringUtils.isNotBlank(userIds)) { + SysUser updateUser = new SysUser(); + updateUser.setUpdateBy(JwtUtil.getUserNameByToken(request)); + updateUser.setUpdateTime(new Date()); + sysUserService.revertLogicDeleted(Arrays.asList(userIds.split(",")), updateUser); + } + return Result.ok("还原成功"); + } + + /** + * 彻底删除用户 + * + * @param userIds 被删除的用户ID,多个id用半角逗号分割 + * @return + */ + @RequiresPermissions("system:user:deleteRecycleBin") + @RequestMapping(value = "/deleteRecycleBin", method = RequestMethod.DELETE) + public Result deleteRecycleBin(@RequestParam("userIds") String userIds) { + if (StringUtils.isNotBlank(userIds)) { + sysUserService.removeLogicDeleted(Arrays.asList(userIds.split(","))); + } + return Result.ok("删除成功"); + } + + + /** + * 移动端修改用户信息 + * @param jsonObject + * @return + */ + @RequiresPermissions("system:user:app:edit") + @RequestMapping(value = "/appEdit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result appEdit(HttpServletRequest request,@RequestBody JSONObject jsonObject) { + Result result = new Result(); + try { + String username = JwtUtil.getUserNameByToken(request); + SysUser sysUser = sysUserService.getUserByName(username); + baseCommonService.addLog("移动端编辑用户,id: " +jsonObject.getString("id") ,CommonConstant.LOG_TYPE_2, 2); + String realname=jsonObject.getString("realname"); + String avatar=jsonObject.getString("avatar"); + String sex=jsonObject.getString("sex"); + String phone=jsonObject.getString("phone"); + String email=jsonObject.getString("email"); + Date birthday=jsonObject.getDate("birthday"); + SysUser userPhone = sysUserService.getUserByPhone(phone); + if(sysUser==null) { + result.error500("未找到对应用户!"); + }else { + if(userPhone!=null){ + String userPhonename = userPhone.getUsername(); + if(!userPhonename.equals(username)){ + result.error500("手机号已存在!"); + return result; + } + } + if(StringUtils.isNotBlank(realname)){ + sysUser.setRealname(realname); + } + if(StringUtils.isNotBlank(avatar)){ + sysUser.setAvatar(avatar); + } + if(StringUtils.isNotBlank(sex)){ + sysUser.setSex(Integer.parseInt(sex)); + } + if(StringUtils.isNotBlank(phone)){ + sysUser.setPhone(phone); + } + if(StringUtils.isNotBlank(email)){ + // 代码逻辑说明: [VUEN-1528]积木官网邮箱重复,应该提示准确------------ + LambdaQueryWrapper emailQuery = new LambdaQueryWrapper<>(); + emailQuery.eq(SysUser::getEmail,email); + long count = sysUserService.count(emailQuery); + if (!email.equals(sysUser.getEmail()) && count!=0) { + result.error500("保存失败,邮箱已存在!"); + return result; + } + sysUser.setEmail(email); + } + if(null != birthday){ + sysUser.setBirthday(birthday); + } + sysUser.setUpdateTime(new Date()); + sysUserService.updateById(sysUser); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("保存失败!"); + } + return result; + } + /** + * 移动端保存设备信息 + * @param clientId + * @return + */ + @RequestMapping(value = "/saveClientId", method = RequestMethod.GET) + public Result saveClientId(HttpServletRequest request,@RequestParam(value = "clientId",required = false)String clientId) { + Result result = new Result(); + try { + String username = JwtUtil.getUserNameByToken(request); + SysUser sysUser = sysUserService.getUserByName(username); + if(sysUser==null) { + result.error500("未找到对应用户!"); + }else { + sysUserService.updateClientId(clientId,sysUser.getId()); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败!"); + } + return result; + } + /** + * 根据userid获取用户信息和部门员工信息 + * + * @return Result + */ + @GetMapping("/queryChildrenByUsername") + public Result queryChildrenByUsername(@RequestParam("userId") String userId) { + //获取用户信息 + Map map=new HashMap(5); + SysUser sysUser = sysUserService.getById(userId); + String username = sysUser.getUsername(); + Integer identity = sysUser.getUserIdentity(); + map.put("sysUser",sysUser); + if(identity!=null && identity==2){ + //获取部门用户信息 + String departIds = sysUser.getDepartIds(); + if(StringUtils.isNotBlank(departIds)){ + List departIdList = Arrays.asList(departIds.split(",")); + List childrenUser = sysUserService.queryByDepIds(departIdList,username); + map.put("children",childrenUser); + } + } + return Result.ok(map); + } + /** + * 移动端查询部门用户信息 + * @param departId + * @return + */ + @GetMapping("/appQueryByDepartId") + public Result> appQueryByDepartId(@RequestParam(name="departId", required = false) String departId) { + Result> result = new Result>(); + List list=new ArrayList (); + list.add(departId); + List childrenUser = sysUserService.queryByDepIds(list,null); + result.setResult(childrenUser); + return result; + } + /** + * 移动端查询用户信息(通过用户名模糊查询) + * @param keyword + * @return + */ + @GetMapping("/appQueryUser") + public Result> appQueryUser(@RequestParam(name = "keyword", required = false) String keyword, + @RequestParam(name = "username", required = false) String username, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest request) { + Result> result = new Result>(); + LambdaQueryWrapper queryWrapper =new LambdaQueryWrapper(); + // 外部模拟登陆临时账号,列表不显示 + queryWrapper.ne(SysUser::getUsername,"_reserve_user_external"); + //增加 username传参 + if(oConvertUtils.isNotEmpty(username)){ + if(username.contains(",")){ + queryWrapper.in(SysUser::getUsername,username.split(",")); + }else{ + queryWrapper.eq(SysUser::getUsername,username); + } + }else if(StringUtils.isNotBlank(keyword)){ + queryWrapper.and(i -> i.like(SysUser::getUsername, keyword).or().like(SysUser::getRealname, keyword)); + } + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + String tenantId = oConvertUtils.getString(TokenUtils.getTenantIdByRequest(request),"-1"); + // 代码逻辑说明: [QQYUN-3371]租户逻辑改造,改成关系表------------ + List userIds = userTenantService.getUserIdsByTenantId(Integer.valueOf(tenantId)); + if (oConvertUtils.listIsNotEmpty(userIds)) { + queryWrapper.in(SysUser::getId, userIds); + } + } + //------------------------------------------------------------------------------------------------ + Page page = new Page<>(pageNo, pageSize); + + // 代码逻辑说明: JHHB-812 【移动端】人员按照排序展示 选择人员,通讯录等 123正序排 + queryWrapper.orderByAsc(SysUser::getSort); + queryWrapper.orderByDesc(SysUser::getCreateTime); + + IPage pageList = this.sysUserService.page(page, queryWrapper); + //批量查询用户的所属部门 + //step.1 先拿到全部的 useids + //step.2 通过 useids,一次性查询用户的所属部门名字 + List userIds = pageList.getRecords().stream().map(SysUser::getId).collect(Collectors.toList()); + if(userIds!=null && userIds.size()>0){ + Map useDepNames = sysUserService.getDepNamesByUserIds(userIds); + pageList.getRecords().forEach(item->{ + item.setOrgCodeTxt(useDepNames.get(item.getId())); + }); + } + result.setResult(pageList.getRecords()); + return result; + } + + /** + * 根据用户名修改手机号[该方法未使用] + * @param json + * @return + */ + @RequestMapping(value = "/updateMobile", method = RequestMethod.PUT) + public Result changMobile(@RequestBody JSONObject json,HttpServletRequest request) { + String smscode = json.getString("smscode"); + String phone = json.getString("phone"); + Result result = new Result(); + //获取登录用户名 + String username = JwtUtil.getUserNameByToken(request); + if(oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(smscode) || oConvertUtils.isEmpty(phone)) { + result.setMessage("修改手机号失败!"); + result.setSuccess(false); + return result; + } + // 代码逻辑说明: VUEN-2245 【漏洞】发现新漏洞待处理20220906 + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; + Object object= redisUtil.get(redisKey); + if(null==object) { + result.setMessage("短信验证码失效!"); + result.setSuccess(false); + return result; + } + if(!smscode.equals(object.toString())) { + result.setMessage("短信验证码不匹配!"); + result.setSuccess(false); + return result; + } + SysUser user = sysUserService.getUserByName(username); + if(user==null) { + return Result.error("用户不存在!"); + } + user.setPhone(phone); + sysUserService.updateById(user); + return Result.ok("手机号设置成功!"); + } + + + /** + * 根据对象里面的属性值作in查询 属性可能会变 用户组件用到 + * @param sysUser + * @return + */ + @GetMapping("/getMultiUser") + public List getMultiUser(SysUser sysUser){ + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysUser, null); + // 代码逻辑说明: [JTC-297]已冻结用户仍可设置为代理人------------ + queryWrapper.eq("status",Integer.parseInt(CommonConstant.STATUS_1)); + List ls = this.sysUserService.list(queryWrapper); + for(SysUser user: ls){ + user.setPassword(null); + user.setSalt(null); + } + return ls; + } + + /** + * 聊天 创建聊天组件专用 根据用户账号、用户姓名、部门id分页查询 + * @param departId 部门id + * @param keyword 搜索值 + * @return + */ + @GetMapping(value = "/getUserInformation") + public Result> getUserInformation( + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name = "departId", required = false) String departId, + @RequestParam(name="keyword",required=false) String keyword) { + //------------------------------------------------------------------------------------------------ + Integer tenantId = null; + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); + } + //------------------------------------------------------------------------------------------------ + IPage pageList = sysUserDepartService.getUserInformation(tenantId,departId, keyword, pageSize, pageNo); + return Result.OK(pageList); + } + + /** + * 简版流程用户选择组件 + * @param departId 部门id + * @param roleId 角色id + * @param keyword 搜索值 + * @return + */ + @GetMapping(value = "/selectUserList") + public Result> selectUserList( + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name = "departId", required = false) String departId, + @RequestParam(name = "roleId", required = false) String roleId, + @RequestParam(name="keyword",required=false) String keyword, + @RequestParam(name="excludeUserIdList",required = false) String excludeUserIdList, + @RequestParam(name="includeUsernameList",required = false) String includeUsernameList, + HttpServletRequest req) { + //------------------------------------------------------------------------------------------------ + Integer tenantId = null; + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + String tenantStr = TenantContext.getTenant(); + tenantId = oConvertUtils.getInteger(tenantStr, oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(req), -1)); + log.info("---------简流中选择用户接口,通过租户筛选,租户ID={}", tenantId); + } + //------------------------------------------------------------------------------------------------ + IPage pageList = sysUserDepartService.getUserInformation(tenantId, departId,roleId, keyword, pageSize, pageNo,excludeUserIdList,includeUsernameList); + return Result.OK(pageList); + } + + /** + * 获取被逻辑删除的用户列表,无分页【低代码应用专用接口】 + * + * @return List + */ + @GetMapping("/getQuitList") + public Result> getQuitList(HttpServletRequest req) { + Integer tenantId = oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(req),0); + List quitList = sysUserService.getQuitList(tenantId); + if (null != quitList && quitList.size() > 0) { + // 批量查询用户的所属部门 + // step.1 先拿到全部的 userIds + List userIds = quitList.stream().map(SysUser::getId).collect(Collectors.toList()); + // step.2 通过 userIds,一次性查询用户的所属部门名字 + Map useDepNames = sysUserService.getDepNamesByUserIds(userIds); + quitList.forEach(item -> item.setOrgCode(useDepNames.get(item.getId()))); + } + return Result.ok(quitList); + } + + /** + * 更新刪除状态和离职状态【低代码应用专用接口】 + * @param jsonObject + * @return Result + */ + @PutMapping("/putCancelQuit") + public Result putCancelQuit(@RequestBody JSONObject jsonObject, HttpServletRequest request){ + String userIds = jsonObject.getString("userIds"); + String usernames = jsonObject.getString("usernames"); + Integer tenantId = oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(request),0); + //将状态改成未删除 + if (StringUtils.isNotBlank(userIds)) { + userTenantService.putCancelQuit(Arrays.asList(userIds.split(SymbolConstant.COMMA)),tenantId); + } + return Result.ok("取消离职成功"); + } + + /** + * 获取用户信息(vue3用户设置专用)【低代码应用专用接口】 + * @return + */ + @GetMapping("/login/setting/getUserData") + public Result getUserData(HttpServletRequest request) { + String username = JwtUtil.getUserNameByToken(request); + SysUser user = sysUserService.getUserByName(username); + if(user==null) { + return Result.error("未找到该用户数据"); + } + + //获取用户id通过职位数据 + List sysPositionList = sysPositionService.getPositionList(user.getId()); + if(null != sysPositionList && sysPositionList.size()>0){ + StringBuilder nameBuilder = new StringBuilder(); + StringBuilder idBuilder = new StringBuilder(); + String verticalBar = " | "; + for (SysPosition sysPosition:sysPositionList){ + nameBuilder.append(sysPosition.getName()).append(verticalBar); + idBuilder.append(sysPosition.getId()).append(SymbolConstant.COMMA); + } + String names = nameBuilder.toString(); + if(oConvertUtils.isNotEmpty(names)){ + names = names.substring(0,names.lastIndexOf(verticalBar)); + user.setPostText(names); + } + //拼接职位id + String ids = idBuilder.toString(); + if(oConvertUtils.isNotEmpty(ids)){ + ids = ids.substring(0,ids.lastIndexOf(SymbolConstant.COMMA)); + user.setPost(ids); + } + } + return Result.ok(user); + } + + /** + * 用户编辑(vue3用户设置专用)【低代码应用专用接口】 + * @param sysUser + * @return + */ + @PostMapping("/login/setting/userEdit") + @RequiresPermissions("system:user:setting:edit") + public Result userEdit(@RequestBody SysUser sysUser, HttpServletRequest request) { + String username = JwtUtil.getUserNameByToken(request); + SysUser user = sysUserService.getById(sysUser.getId()); + if(user==null) { + return Result.error("未找到该用户数据"); + } + if(!username.equals(user.getUsername())){ + return Result.error("只能修改自己的数据"); + } + sysUserService.updateById(sysUser); + return Result.ok("更新个人信息成功"); + } + + /** + * 批量修改 【low-app】 + * @param jsonObject + * @return + */ + @PutMapping("/batchEditUsers") + public Result batchEditUsers(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + try { + sysUserService.batchEditUsers(jsonObject); + result.setSuccess(true); + result.setMessage("操作成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 根据关键词搜索部门和用户【low-app】 + * @param keyword + * @return + */ + @GetMapping("/searchByKeyword") + public Result searchByKeyword(@RequestParam(name="keyword",required=false) String keyword) { + DepartAndUserInfo info = sysUserService.searchByKeyword(keyword); + return Result.ok(info); + } + + /** + * 编辑部门前获取部门相关信息 【low-app】 + * @param id + * @return + */ + @GetMapping("/getUpdateDepartInfo") + public Result getUpdateDepartInfo(@RequestParam(name="id",required=false) String id) { + UpdateDepartInfo info = sysUserService.getUpdateDepartInfo(id); + return Result.ok(info); + } + + /** + * 编辑部门 【low-app】 + * @param updateDepartInfo + * @return + */ + @PutMapping("/doUpdateDepartInfo") + public Result doUpdateDepartInfo(@RequestBody UpdateDepartInfo updateDepartInfo) { + sysUserService.doUpdateDepartInfo(updateDepartInfo); + return Result.ok(); + } + + /** + * 设置负责人 取消负责人 + * @param json + * @return + */ + @PutMapping("/changeDepartChargePerson") + public Result changeDepartChargePerson(@RequestBody JSONObject json) { + sysUserService.changeDepartChargePerson(json); + return Result.ok(); + } + + /** + * 修改租户下的用户【低代码应用专用接口】 + * @param sysUser + * @param req + * @return + */ + @RequestMapping(value = "/editTenantUser", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result editTenantUser(@RequestBody SysUser sysUser,HttpServletRequest req){ + Result result = new Result<>(); + String tenantId = TokenUtils.getTenantIdByRequest(req); + if(oConvertUtils.isEmpty(tenantId)){ + return result.error500("无权修改他人信息!"); + } + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getTenantId,Integer.valueOf(tenantId)); + query.eq(SysUserTenant::getUserId,sysUser.getId()); + SysUserTenant one = userTenantService.getOne(query); + if(null == one){ + return result.error500("非当前租户下的用户,不允许修改!"); + } + String departs = req.getParameter("selecteddeparts"); + sysUserService.editTenantUser(sysUser,tenantId,departs,null); + return Result.ok("修改成功"); + } + + /** + * 切换租户时 需要修改 loginTenantId + * QQYUN-4491 【应用】一些小问题 1、上次选中登录的租户,下次登录未记忆 + * @param sysUser + * @return + */ + @PutMapping("/changeLoginTenantId") + public Result changeLoginTenantId(@RequestBody SysUser sysUser){ + Result result = new Result<>(); + Integer tenantId = sysUser.getLoginTenantId(); + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String userId = loginUser.getId(); + + // 判断 指定的租户ID是不是当前登录用户的租户 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getTenantId, tenantId); + query.eq(SysUserTenant::getUserId, userId); + SysUserTenant one = userTenantService.getOne(query); + if(null == one){ + return result.error500("非租户下的用户,不允许修改!"); + } + + // 修改 loginTenantId + LambdaQueryWrapper update = new LambdaQueryWrapper() + .eq(SysUser::getId, userId); + SysUser updateUser = new SysUser(); + updateUser.setLoginTenantId(tenantId); + sysUserService.update(updateUser, update); + return Result.ok(); + } + + /** + * 应用用户导出 + * @param request + * @return + */ + @RequestMapping(value = "/exportAppUser") + public ModelAndView exportAppUser(HttpServletRequest request) { + return sysUserService.exportAppUser(request); + } + + /** + * 应用用户导入 + * @param request + * @return + */ + @RequestMapping(value = "/importAppUser", method = RequestMethod.POST) + public Result importAppUser(HttpServletRequest request, HttpServletResponse response)throws IOException { + return sysUserService.importAppUser(request); + } + + /** + * 更改手机号(敲敲云个人设置专用) + * + * @param json + * @param request + */ + @PutMapping("/changePhone") + public Result changePhone(@RequestBody JSONObject json, HttpServletRequest request){ + //获取登录用户名 + String username = JwtUtil.getUserNameByToken(request); + sysUserService.changePhone(json,username); + return Result.ok("修改手机号成功!"); + } + + /** + * 发送短信验证码接口(修改手机号) + * + * @param jsonObject + * @return + */ + @PostMapping(value = "/sendChangePhoneSms") + public Result sendChangePhoneSms(@RequestBody JSONObject jsonObject, HttpServletRequest request) { + //获取登录用户名 + String username = JwtUtil.getUserNameByToken(request); + String ipAddress = IpUtils.getIpAddr(request); + sysUserService.sendChangePhoneSms(jsonObject, username, ipAddress); + return Result.ok("发送验证码成功!"); + } + + /** + * 发送注销用户手机号验证密码[敲敲云专用] + * + * @param jsonObject + * @return + */ + @PostMapping(value = "/sendLogOffPhoneSms") + public Result sendLogOffPhoneSms(@RequestBody JSONObject jsonObject, HttpServletRequest request) { + Result result = new Result<>(); + //获取登录用户名 + String username = JwtUtil.getUserNameByToken(request); + String name = jsonObject.getString("username"); + if (oConvertUtils.isEmpty(name) || !name.equals(username)) { + result.setSuccess(false); + result.setMessage("发送验证码失败,用户不匹配!"); + return result; + } + String ipAddress = IpUtils.getIpAddr(request); + sysUserService.sendLogOffPhoneSms(jsonObject, username, ipAddress); + result.setSuccess(true); + result.setMessage("发送验证码成功!"); + return result; + } + + /** + * 没有绑定手机号 直接修改密码 + * @param oldPassword + * @param password + * @return + */ + @PutMapping("/updatePasswordNotBindPhone") + public Result updatePasswordNotBindPhone(@RequestParam(value="oldPassword") String oldPassword, + @RequestParam(value="password") String password, + @RequestParam(value="username") String username){ + sysUserService.updatePasswordNotBindPhone(oldPassword, password, username); + return Result.OK("修改密码成功!"); + } + + /** + * 根据部门岗位选择用户【部门岗位选择用户专用】 + * @return + */ + @GetMapping("/queryDepartPostUserPageList") + public Result> queryDepartPostUserPageList( @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + @RequestParam(name = "departId", required = false) String departId, + @RequestParam(name="realname",required=false) String realname, + @RequestParam(name="username",required=false) String username, + @RequestParam(name="isMultiTranslate",required=false) String isMultiTranslate, + @RequestParam(name="id",required = false) String id){ + String[] arr = new String[]{departId, realname, username, id}; + SqlInjectionUtil.filterContent(arr, SymbolConstant.SINGLE_QUOTATION_MARK); + IPage pageList = sysUserDepartService.queryDepartPostUserPageList(departId, username, realname, pageSize, pageNo,id,isMultiTranslate); + return Result.OK(pageList); + } + + /** + * 获取上传文件的进度 + * + * @param fileKey + * @param type + * @return + */ + @GetMapping("/getUploadFileProgress") + public Result getUploadFileProgress(@RequestParam(name = "fileKey") String fileKey, + @RequestParam("type") String type){ + Double progress = ImportSysUserCache.getImportSysUserMap(fileKey, type); + if(progress == 100){ + ImportSysUserCache.removeImportLowAppMap(fileKey); + } + return Result.ok(progress); + } + + /** + * 验证当前登录用户是否仍使用系统默认初始密码。 + * 返回值说明: + * yes_{URL编码后的默认密码} -> 用户当前密码为默认初始密码,前端需弹出强制修改提示 + * no -> 用户密码不是默认密码,或未开启默认密码检测开关 + */ + @GetMapping("/verifyIzDefaultPwd") + public Result verifyIzDefaultPwd() throws UnsupportedEncodingException { + // 未配置 Firewall 或已关闭默认密码检测开关 (enableDefaultPwdCheck=false) 时,直接返回 "no" 表示无需提示 + if (GhbBaseConfig.getFirewall() == null || Boolean.FALSE.equals((GhbBaseConfig.getFirewall().getEnableDefaultPwdCheck()))) { + return Result.OK("no"); + } + + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + SysUser user = sysUserService.getById(sysUser.getId()); + String passwordEncode = PasswordUtil.encrypt(user.getUsername(), PasswordConstant.DEFAULT_PASSWORD, user.getSalt()); + if(passwordEncode.equals(user.getPassword())){ + String encode = URLEncoder.encode(PasswordConstant.DEFAULT_PASSWORD, "UTF-8"); + return Result.OK("yes_" + encode); + } + return Result.OK("no"); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUserOnlineController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUserOnlineController.java new file mode 100644 index 0000000..d84e593 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/SysUserOnlineController.java @@ -0,0 +1,130 @@ +package com.ghb.base.modules.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.LoginUser; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.service.impl.SysBaseApiImpl; +import com.ghb.base.modules.system.vo.SysUserOnlineVO; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @Description: 在线用户 + * @Author: chenli + * @Date: 2020-06-07 + * @Version: V1.0 + */ +@RestController +@RequestMapping("/sys/online") +@Slf4j +public class SysUserOnlineController { + + @Autowired + private RedisUtil redisUtil; + @Autowired + public RedisTemplate redisTemplate; + @Autowired + public ISysUserService userService; + @Autowired + private SysBaseApiImpl sysBaseApi; + @Resource + private BaseCommonService baseCommonService; + + @RequiresPermissions("system:online:list") + @RequestMapping(value = "/list", method = RequestMethod.GET) + public Result> list(@RequestParam(name="username", required=false) String username, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,@RequestParam(name="pageSize", defaultValue="10") Integer pageSize) { + Collection keys = redisUtil.scan(CommonConstant.PREFIX_USER_TOKEN + "*"); + List onlineList = new ArrayList(); + for (String key : keys) { + String token = (String)redisUtil.get(key); + if (StringUtils.isNotEmpty(token)) { + SysUserOnlineVO online = new SysUserOnlineVO(); + online.setToken(token); + //TODO 改成一次性查询 + LoginUser loginUser = sysBaseApi.getUserByName(JwtUtil.getUsername(token)); + if (loginUser != null && !"_reserve_user_external".equals(loginUser.getUsername())) { + //验证用户名是否与传过来的用户名相同 + boolean isMatchUsername=true; + //判断用户名是否为空,并且当前循环的用户不包含传过来的用户名,那么就设成false + if(oConvertUtils.isNotEmpty(username) && !loginUser.getUsername().contains(username)){ + isMatchUsername = false; + } + if(isMatchUsername){ + BeanUtils.copyProperties(loginUser, online); + onlineList.add(online); + } + } + } + } + Collections.reverse(onlineList); + + Page page = new Page(pageNo, pageSize); + int count = onlineList.size(); + List pages = new ArrayList<>(); + // 计算当前页第一条数据的下标 + int currId = pageNo > 1 ? (pageNo - 1) * pageSize : 0; + for (int i = 0; i < pageSize && i < count - currId; i++) { + pages.add(onlineList.get(currId + i)); + } + page.setSize(pageSize); + page.setCurrent(pageNo); + page.setTotal(count); + // 计算分页总页数 + page.setPages(count % 10 == 0 ? count / 10 : count / 10 + 1); + page.setRecords(pages); + + Result> result = new Result>(); + result.setSuccess(true); + result.setResult(page); + return result; + } + + /** + * 强退用户 + */ + @RequiresPermissions("system:online:forceLogout") + @RequestMapping(value = "/forceLogout",method = RequestMethod.POST) + public Result forceLogout(@RequestBody SysUserOnlineVO online) { + //用户退出逻辑 + if(oConvertUtils.isEmpty(online.getToken())) { + return Result.error("退出登录失败!"); + } + String username = JwtUtil.getUsername(online.getToken()); + LoginUser sysUser = sysBaseApi.getUserByName(username); + if(sysUser!=null) { + baseCommonService.addLog("强制: "+sysUser.getRealname()+"退出成功!", CommonConstant.LOG_TYPE_1, null,sysUser); + log.info(" 强制 "+sysUser.getRealname()+"退出成功! "); + //清空用户登录Token缓存 + redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + online.getToken()); + //清空用户登录Shiro权限缓存 + redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId()); + //清空用户的缓存信息(包括部门信息),例如sys:cache:user:: + redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername())); + //调用shiro的logout + SecurityUtils.getSubject().logout(); + return Result.ok("退出登录成功!"); + }else { + return Result.error("Token无效!"); + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/ThirdAppController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/ThirdAppController.java new file mode 100644 index 0000000..f0f33b0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/ThirdAppController.java @@ -0,0 +1,592 @@ +package com.ghb.base.modules.system.controller; + +import cn.hutool.core.collection.CollectionUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.jeecg.dingtalk.api.core.response.Response; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.MessageTypeEnum; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.TokenUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysThirdAccount; +import com.ghb.base.modules.system.entity.SysThirdAppConfig; +import com.ghb.base.modules.system.service.ISysThirdAccountService; +import com.ghb.base.modules.system.service.ISysThirdAppConfigService; +import com.ghb.base.modules.system.service.impl.ThirdAppDingtalkServiceImpl; +import com.ghb.base.modules.system.service.impl.ThirdAppWechatEnterpriseServiceImpl; +import com.ghb.base.modules.system.vo.thirdapp.JwSysUserDepartVo; +import com.ghb.base.modules.system.vo.thirdapp.JwUserDepartVo; +import com.ghb.base.modules.system.vo.thirdapp.SyncInfoVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 第三方App对接 + * @author: Ghb-boot + */ +@Slf4j +@RestController("thirdAppController") +@RequestMapping("/sys/thirdApp") +public class ThirdAppController { + + @Autowired + ThirdAppWechatEnterpriseServiceImpl wechatEnterpriseService; + @Autowired + ThirdAppDingtalkServiceImpl dingtalkService; + + @Autowired + private ISysThirdAppConfigService appConfigService; + + @Autowired + private ISysThirdAccountService sysThirdAccountService; + + /** + * 获取启用的系统 + */ + @GetMapping("/getEnabledType") + public Result getEnabledType() { + Map enabledMap = new HashMap(5); + int tenantId; + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + tenantId = oConvertUtils.getInt(TenantContext.getTenant(), -1); + } else { + tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + } + //查询当前租户下的第三方配置 + List list = appConfigService.getThirdConfigListByThirdType(tenantId); + //钉钉是否已配置 + boolean dingConfig = false; + //企业微信是否已配置 + boolean qywxConfig = false; + if(null != list && list.size()>0){ + for (SysThirdAppConfig config:list) { + if(MessageTypeEnum.DD.getType().equals(config.getThirdType())){ + dingConfig = true; + continue; + } + if(MessageTypeEnum.QYWX.getType().equals(config.getThirdType())){ + qywxConfig = true; + continue; + } + } + } + enabledMap.put("wechatEnterprise", qywxConfig); + enabledMap.put("dingtalk", dingConfig); + return Result.OK(enabledMap); + } + + /** + * 同步本地[用户]到【企业微信】 + * + * @param ids + * @return + */ + @GetMapping("/sync/wechatEnterprise/user/toApp") + public Result syncWechatEnterpriseUserToApp(@RequestParam(value = "ids", required = false) String ids) { + //获取企业微信配置 + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.QYWX.getType()); + if (null != config) { + // 代码逻辑说明: [QQYUN-3440]通过租户模式隔离 ------------ + SyncInfoVo syncInfo = wechatEnterpriseService.syncLocalUserToThirdApp(ids); + if (syncInfo.getFailInfo().size() == 0) { + return Result.OK("同步成功", syncInfo); + } else { + return Result.error("同步失败", syncInfo); + } + } + return Result.error("企业微信尚未配置,请配置企业微信"); + } + + /** + * 同步【企业微信】[用户]到本地 + * + * @param ids 作废 + * @return + */ + @GetMapping("/sync/wechatEnterprise/user/toLocal") + public Result syncWechatEnterpriseUserToLocal(@RequestParam(value = "ids", required = false) String ids) { + return Result.error("由于企业微信接口调整,同步到本地功能已失效"); + +// if (thirdAppConfig.isWechatEnterpriseEnabled()) { +// SyncInfoVo syncInfo = wechatEnterpriseService.syncThirdAppUserToLocal(); +// if (syncInfo.getFailInfo().size() == 0) { +// return Result.OK("同步成功", syncInfo); +// } else { +// return Result.error("同步失败", syncInfo); +// } +// } +// return Result.error("企业微信同步功能已禁用"); + } + + /** + * 同步本地[部门]到【企业微信】 + * + * @param ids + * @return + */ + @GetMapping("/sync/wechatEnterprise/depart/toApp") + public Result syncWechatEnterpriseDepartToApp(@RequestParam(value = "ids", required = false) String ids) { + //获取企业微信配置 + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.QYWX.getType()); + if (null != config) { + SyncInfoVo syncInfo = wechatEnterpriseService.syncLocalDepartmentToThirdApp(ids); + if (syncInfo.getFailInfo().size() == 0) { + return Result.OK("同步成功", null); + } else { + return Result.error("同步失败", syncInfo); + } + } + return Result.error("企业微信尚未配置,请配置企业微信"); + } + + /** + * 同步【企业微信】[部门]到本地 + * + * @param ids + * @return + */ + @GetMapping("/sync/wechatEnterprise/depart/toLocal") + public Result syncWechatEnterpriseDepartToLocal(@RequestParam(value = "ids", required = false) String ids) { + return Result.error("由于企业微信接口调整,企业微信同步本地部门失效"); +// //获取企业微信配置 +// Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); +// SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.QYWX.getType()); +// if (null != config) { +// SyncInfoVo syncInfo = wechatEnterpriseService.syncThirdAppDepartmentToLocal(ids); +// if (syncInfo.getFailInfo().size() == 0) { +// return Result.OK("同步成功", syncInfo); +// } else { +// return Result.error("同步失败", syncInfo); +// } +// } +// return Result.error("企业微信尚未配置,请配置企业微信"); + } + + /** + * 同步本地[部门]到【钉钉】 + * + * @param ids + * @return + */ + @GetMapping("/sync/dingtalk/depart/toApp") + public Result syncDingtalkDepartToApp(@RequestParam(value = "ids", required = false) String ids) { + //获取钉钉配置 + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.DD.getType()); + if (null != config) { + SyncInfoVo syncInfo = dingtalkService.syncLocalDepartmentToThirdApp(ids); + if (syncInfo.getFailInfo().size() == 0) { + return Result.OK("同步成功", null); + } else { + return Result.error("同步失败", syncInfo); + } + } + return Result.error("钉钉尚未配置,请配置钉钉"); + } + +// /** +// * 同步【钉钉】[部门]到本地 +// * +// * @param ids +// * @return +// */ +// @GetMapping("/sync/dingtalk/depart/toLocal") +// public Result syncDingtalkDepartToLocal(@RequestParam(value = "ids", required = false) String ids) { +// //获取钉钉配置 +// Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); +// SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.DD.getType()); +// if (null!= config) { +// SyncInfoVo syncInfo = dingtalkService.syncThirdAppDepartmentToLocal(ids); +// if (syncInfo.getFailInfo().size() == 0) { +// return Result.OK("同步成功", syncInfo); +// } else { +// return Result.error("同步失败", syncInfo); +// } +// } +// return Result.error("钉钉尚未配置,请配置钉钉"); +// } + + /** + * 同步本地[用户]到【钉钉】 + * + * @param ids + * @return + */ + @GetMapping("/sync/dingtalk/user/toApp") + public Result syncDingtalkUserToApp(@RequestParam(value = "ids", required = false) String ids) { + //获取钉钉配置 + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + //根据租户id和第三方类别获取租户数据 + SysThirdAppConfig appConfig = appConfigService.getThirdConfigByThirdType(tenantId,MessageTypeEnum.DD.getType()); + if(null != appConfig){ + SyncInfoVo syncInfo = dingtalkService.syncLocalUserToThirdApp(ids); + if (syncInfo.getFailInfo().size() == 0) { + return Result.OK("同步成功", syncInfo); + } else { + return Result.error("同步失败", syncInfo); + } + } + return Result.error("钉钉尚未配置,请配置钉钉"); + } + +// /** +// * 同步【钉钉】[用户]到本地 +// * +// * @param ids 作废 +// * @return +// */ +// @GetMapping("/sync/dingtalk/user/toLocal") +// public Result syncDingtalkUserToLocal(@RequestParam(value = "ids", required = false) String ids) { +// //获取钉钉配置 +// Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); +// SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.DD.getType()); +// if (null != config) { +// SyncInfoVo syncInfo = dingtalkService.syncThirdAppUserToLocal(); +// if (syncInfo.getFailInfo().size() == 0) { +// return Result.OK("同步成功", syncInfo); +// } else { +// return Result.error("同步失败", syncInfo); +// } +// } +// return Result.error("钉钉尚未配置,请配置钉钉"); +// } + + /** + * 发送消息测试 + * + * @return + */ + @PostMapping("/sendMessageTest") + public Result sendMessageTest(@RequestBody JSONObject params, HttpServletRequest request) { + /* 获取前台传递的参数 */ + // 第三方app的类型 + String app = params.getString("app"); + // 是否发送给全部人 + boolean sendAll = params.getBooleanValue("sendAll"); + // 消息接收者,传sys_user表的username字段,多个用逗号分割 + String receiver = params.getString("receiver"); + // 消息内容 + String content = params.getString("content"); + // 租户id + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); + + String fromUser = JwtUtil.getUserNameByToken(request); + String title = "第三方APP消息测试"; + MessageDTO message = new MessageDTO(fromUser, receiver, title, content); + message.setToAll(sendAll); + // 代码逻辑说明: [QQYUN-3440]钉钉、企业微信通过租户模式隔离 ------------ + String weChatType = MessageTypeEnum.QYWX.getType(); + String dingType = MessageTypeEnum.DD.getType(); + if (weChatType.toUpperCase().equals(app)) { + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, weChatType); + if (null != config) { + JSONObject response = wechatEnterpriseService.sendMessageResponse(message, false); + return Result.OK(response); + } + return Result.error("企业微信尚未配置,请配置企业微信"); + } else if (dingType.toUpperCase().equals(app)) { + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, dingType); + if (null != config) { + Response response = dingtalkService.sendMessageResponse(message, false); + return Result.OK(response); + } + return Result.error("钉钉尚未配置,请配置钉钉"); + } + return Result.error("不识别的第三方APP"); + } + + /** + * 撤回消息测试 + * + * @return + */ + @PostMapping("/recallMessageTest") + public Result recallMessageTest(@RequestBody JSONObject params) { + /* 获取前台传递的参数 */ + // 第三方app的类型 + String app = params.getString("app"); + // 消息id + String msgTaskId = params.getString("msg_task_id"); + //租户id + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(),0); + if (CommonConstant.WECHAT_ENTERPRISE.equals(app)) { + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.QYWX.getType()); + if (null != config) { + return Result.error("企业微信不支持撤回消息"); + } + return Result.error("企业微信尚未配置,请配置企业微信"); + } else if (CommonConstant.DINGTALK.equals(app)) { + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.DD.getType()); + if (null != config) { + Response response = dingtalkService.recallMessageResponse(msgTaskId); + if (response.isSuccess()) { + return Result.OK("撤回成功", response); + } else { + return Result.error("撤回失败:" + response.getErrcode() + "——" + response.getErrmsg(), response); + } + } + return Result.error("钉钉尚未配置,请配置钉钉"); + } + return Result.error("不识别的第三方APP"); + } + + //========================begin 应用低代码钉钉/企业微信同步用户部门专用 ============================= + /** + * 添加第三方app配置 + * + * @param appConfig + * @return + */ + @RequestMapping(value = "/addThirdAppConfig", method = RequestMethod.POST) + public Result addThirdAppConfig(@RequestBody SysThirdAppConfig appConfig) { + Result result = new Result<>(); + //根据当前登录租户id和第三方类别判断是否已经创建 + Integer tenantId = oConvertUtils.isNotEmpty(appConfig.getTenantId()) ? appConfig.getTenantId() : oConvertUtils.getInt(TenantContext.getTenant(), 0); + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, appConfig.getThirdType()); + if (null != config) { + result.error500("操作失败,同一个租户下只允许绑定一个钉钉或者企业微信"); + return result; + } + String clientId = appConfig.getClientId(); + //通过应用key获取第三方配置 + List thirdAppConfigByClientId = appConfigService.getThirdAppConfigByClientId(clientId); + if(CollectionUtil.isNotEmpty(thirdAppConfigByClientId)){ + result.error500("AppKey已存在,请勿重复添加"); + return result; + } + try { + appConfig.setTenantId(oConvertUtils.getInt(TenantContext.getTenant(),0)); + appConfigService.save(appConfig); + result.success("添加成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 编辑第三方app配置 + * + * @param appConfig + * @return + */ + @RequestMapping(value = "/editThirdAppConfig", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result editThirdAppConfig(@RequestBody SysThirdAppConfig appConfig) { + Result result = new Result<>(); + SysThirdAppConfig config = appConfigService.getById(appConfig.getId()); + if (null == config) { + result.error500("数据不存在"); + return result; + } + String clientId = appConfig.getClientId(); + //如果编辑的应用key,和数据库中的不一致,需要判断应用key是否已存在 + if(!clientId.equals(config.getClientId())){ + //通过应用key获取第三方配置 + List thirdAppConfigByClientId = appConfigService.getThirdAppConfigByClientId(clientId); + if(CollectionUtil.isNotEmpty(thirdAppConfigByClientId)){ + result.error500("AppKey已存在,请勿重复添加"); + return result; + } + } + try { + appConfigService.updateById(appConfig); + result.success("修改成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + /** + * 根据id删除第三方配置表 + * @param id + * @return + */ + @DeleteMapping(value = "/deleteThirdAppConfig") + @RequiresPermissions("system:third:config:delete") + public Result deleteThirdAppConfig(@RequestParam(name="id",required=true) String id) { + Result result = new Result<>(); + SysThirdAppConfig config = appConfigService.getById(id); + if (null == config) { + result.error500("数据不存在"); + return result; + } + try { + appConfigService.removeById(id); + result.success("解绑成功!"); + } catch (Exception e) { + log.error(e.getMessage(), e); + result.error500("操作失败"); + } + return result; + } + + + /** + * 根据租户id和第三方类型获取第三方app配置信息 + * + * @param tenantId + * @param thirdType + * @return + */ + @GetMapping("/getThirdConfigByTenantId") + public Result getThirdAppByTenantId(@RequestParam(name = "tenantId", required = false) Integer tenantId, + @RequestParam(name = "thirdType") String thirdType) { + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + if (tenantId == null) { + return Result.error("开启多租户模式,租户ID参数不允许为空!"); + } + } else { + //租户未传递,则采用平台的 + if (tenantId == null) { + tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + } + } + Result result = new Result<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysThirdAppConfig::getThirdType,thirdType); + query.eq(SysThirdAppConfig::getTenantId,tenantId); + SysThirdAppConfig sysThirdAppConfig = appConfigService.getOne(query); + result.setSuccess(true); + result.setResult(sysThirdAppConfig); + return result; + } + + /** + * 同步【钉钉】[部门和用户]到本地 + * + * @param ids + * @return + */ + @GetMapping("/sync/dingtalk/departAndUser/toLocal") + public Result syncDingTalkDepartAndUserToLocal(@RequestParam(value = "ids", required = false) String ids) { + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.DD.getType()); + if (null != config) { + SyncInfoVo syncInfo = dingtalkService.syncThirdAppDepartmentUserToLocal(); + if (syncInfo.getFailInfo().size() == 0) { + return Result.OK("同步成功", syncInfo); + } else { + return Result.error("同步失败", syncInfo); + } + } + return Result.error("钉钉尚未配置,请配置钉钉"); + } + //========================end 应用低代码钉钉/企业微信同步用户部门专用 ======================== + + + //========================begin 应用低代码账号设置第三方账号绑定 ================================ + /** + * 获取第三方账号 + * @param thirdType + * @return + */ + @GetMapping("/getThirdAccountByUserId") + public Result> getThirdAccountByUserId(@RequestParam(name="thirdType") String thirdType){ + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + //根据id查询 + query.eq(SysThirdAccount::getSysUserId,sysUser.getId()); + //扫码登录只有租户为0 + query.eq(SysThirdAccount::getTenantId,CommonConstant.TENANT_ID_DEFAULT_VALUE); + //根据第三方类别查询 + if(oConvertUtils.isNotEmpty(thirdType)){ + query.in(SysThirdAccount::getThirdType, Arrays.asList(thirdType.split(SymbolConstant.COMMA))); + } + List list = sysThirdAccountService.list(query); + return Result.ok(list); + } + + /** + * 绑定第三方账号 + * @return + */ + @PostMapping("/bindThirdAppAccount") + public Result bindThirdAppAccount(@RequestBody SysThirdAccount sysThirdAccount){ + SysThirdAccount thirdAccount = sysThirdAccountService.bindThirdAppAccountByUserId(sysThirdAccount); + return Result.ok(thirdAccount); + } + + /** + * 删除第三方用户信息 + * @param sysThirdAccount + * @return + */ + @DeleteMapping("/deleteThirdAccount") + public Result deleteThirdAccountById(@RequestBody SysThirdAccount sysThirdAccount){ + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + if(!sysUser.getId().equals(sysThirdAccount.getSysUserId())){ + return Result.error("无权修改他人信息"); + } + SysThirdAccount thirdAccount = sysThirdAccountService.getById(sysThirdAccount.getId()); + if(null == thirdAccount){ + return Result.error("未找到改第三方账户信息"); + } + sysThirdAccountService.removeById(thirdAccount.getId()); + return Result.ok("解绑成功"); + } + //========================end 应用低代码账号设置第三方账号绑定 ================================ + + /** + * 获取企业微信绑定的用户信息 + * @param request + * @return + */ + @GetMapping("/getThirdUserByWechat") + public Result getThirdUserByWechat(HttpServletRequest request){ + //获取企业微信配置 + Integer tenantId = oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(request),0); + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(tenantId, MessageTypeEnum.QYWX.getType()); + if (null != config) { + JwSysUserDepartVo list = wechatEnterpriseService.getThirdUserByWechat(tenantId); + return Result.ok(list); + } + return Result.error("企业微信尚未配置,请配置企业微信"); + } + + /** + * 同步企业微信部门和用户到本地 + * @param jwUserDepartJson + * @param request + * @return + */ + @GetMapping("/sync/wechatEnterprise/departAndUser/toLocal") + public Result syncWechatEnterpriseDepartAndUserToLocal(@RequestParam(name = "jwUserDepartJson") String jwUserDepartJson,HttpServletRequest request){ + int tenantId = oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(request), 0); + SyncInfoVo syncInfoVo = wechatEnterpriseService.syncWechatEnterpriseDepartAndUserToLocal(jwUserDepartJson,tenantId); + return Result.ok(syncInfoVo); + } + + /** + * 查询被绑定的企业微信用户 + * @param request + * @return + */ + @GetMapping("/getThirdUserBindByWechat") + public Result> getThirdUserBindByWechat(HttpServletRequest request){ + int tenantId = oConvertUtils.getInt(TokenUtils.getTenantIdByRequest(request), 0); + List jwSysUserDepartVos = wechatEnterpriseService.getThirdUserBindByWechat(tenantId); + return Result.ok(jwSysUserDepartVos); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/ThirdLoginController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/ThirdLoginController.java new file mode 100644 index 0000000..41ec01e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/ThirdLoginController.java @@ -0,0 +1,614 @@ +package com.ghb.base.modules.system.controller; +import org.jeecg.common.util.RedisUtil; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xkcoding.justauth.AuthRequestFactory; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.request.AuthRequest; +import me.zhyd.oauth.utils.AuthStateUtils; +import me.zhyd.oauth.utils.StringUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.enums.MessageTypeEnum; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.util.*; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysThirdAccount; +import com.ghb.base.modules.system.entity.SysThirdAppConfig; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.model.ThirdLoginModel; +import com.ghb.base.modules.system.service.ISysDictService; +import com.ghb.base.modules.system.service.ISysThirdAccountService; +import com.ghb.base.modules.system.service.ISysThirdAppConfigService; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.service.ISysDepartService; +import com.ghb.base.modules.system.service.impl.ThirdAppDingtalkServiceImpl; +import com.ghb.base.modules.system.service.impl.ThirdAppWechatEnterpriseServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Date; +import java.util.List; + +/** + * @Author scott + * @since 2018-12-17 + */ +@Controller +@RequestMapping("/sys/thirdLogin") +@Slf4j +public class ThirdLoginController { + @Autowired + private ISysUserService sysUserService; + @Autowired + private ISysThirdAccountService sysThirdAccountService; + @Autowired + private ISysDictService sysDictService; + @Autowired + private BaseCommonService baseCommonService; + @Autowired + private RedisUtil redisUtil; + @Autowired + private AuthRequestFactory factory; + @Autowired + private ISysDepartService sysDepartService; + + @Autowired + private ThirdAppWechatEnterpriseServiceImpl thirdAppWechatEnterpriseService; + @Autowired + private ThirdAppDingtalkServiceImpl thirdAppDingtalkService; + + @Autowired + private ISysThirdAppConfigService appConfigService; + + @Autowired + public ISysBaseAPI sysBaseAPI; + + @RequestMapping("/render/{source}") + public void render(@PathVariable("source") String source, HttpServletResponse response) throws IOException { + log.info("第三方登录进入render:" + source); + AuthRequest authRequest = factory.get(source); + String authorizeUrl = authRequest.authorize(AuthStateUtils.createState()); + log.info("第三方登录认证地址:" + authorizeUrl); + response.sendRedirect(authorizeUrl); + } + + @RequestMapping("/{source}/callback") + public String loginThird(@PathVariable("source") String source, AuthCallback callback,ModelMap modelMap) { + log.info("第三方登录进入callback:" + source + " params:" + JSONObject.toJSONString(callback)); + AuthRequest authRequest = factory.get(source); + AuthResponse response = authRequest.login(callback); + log.info(JSONObject.toJSONString(response)); + Result result = new Result(); + if(response.getCode()==2000) { + + JSONObject data = JSONObject.parseObject(JSONObject.toJSONString(response.getData())); + String username = data.getString("username"); + String avatar = data.getString("avatar"); + String uuid = data.getString("uuid"); + //构造第三方登录信息存储对象 + ThirdLoginModel tlm = new ThirdLoginModel(source, uuid, username, avatar); + //判断有没有这个人 + // 代码逻辑说明: 修改成查询第三方账户表 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysThirdAccount::getThirdType, source); + // 代码逻辑说明: 【QQYUN-6667】敲敲云,线上解绑重新绑定一直提示这个--- + query.eq(SysThirdAccount::getTenantId, CommonConstant.TENANT_ID_DEFAULT_VALUE); + query.and(q -> q.eq(SysThirdAccount::getThirdUserUuid, uuid).or().eq(SysThirdAccount::getThirdUserId, uuid)); + List thridList = sysThirdAccountService.list(query); + SysThirdAccount user = null; + if(thridList==null || thridList.size()==0) { + //否则直接创建新账号 + user = sysThirdAccountService.saveThirdUser(tlm,CommonConstant.TENANT_ID_DEFAULT_VALUE); + }else { + //已存在 只设置用户名 不设置头像 + user = thridList.get(0); + } + // 生成token + // 代码逻辑说明: 从第三方登录查询是否存在用户id,不存在绑定手机号 + if(oConvertUtils.isNotEmpty(user.getSysUserId())) { + String sysUserId = user.getSysUserId(); + SysUser sysUser = sysUserService.getById(sysUserId); + String token = saveToken(sysUser); + modelMap.addAttribute("token", token); + }else{ + modelMap.addAttribute("token", "绑定手机号,"+""+uuid); + } + }else{ + modelMap.addAttribute("token", "登录失败"); + } + result.setSuccess(false); + result.setMessage("第三方登录异常,请联系管理员"); + return "thirdLogin"; + } + + /** + * 创建新账号 + * @param model + * @return + */ + @PostMapping("/user/create") + @ResponseBody + public Result thirdUserCreate(@RequestBody ThirdLoginModel model) { + log.info("第三方登录创建新账号:" ); + Result res = new Result<>(); + Object operateCode = redisUtil.get(CommonConstant.THIRD_LOGIN_CODE); + if(operateCode==null || !operateCode.toString().equals(model.getOperateCode())){ + res.setSuccess(false); + res.setMessage("校验失败"); + return res; + } + //创建新账号 + // 代码逻辑说明: 修改成从第三方登录查出来的user_id,在查询用户表尽行token + SysThirdAccount user = sysThirdAccountService.saveThirdUser(model,CommonConstant.TENANT_ID_DEFAULT_VALUE); + if(oConvertUtils.isNotEmpty(user.getSysUserId())){ + String sysUserId = user.getSysUserId(); + SysUser sysUser = sysUserService.getById(sysUserId); + // 生成token + String token = saveToken(sysUser); + res.setResult(token); + res.setSuccess(true); + } + return res; + } + + /** + * 绑定账号 需要设置密码 需要走一遍校验 + * @param json + * @return + */ + @PostMapping("/user/checkPassword") + @ResponseBody + public Result checkPassword(@RequestBody JSONObject json) { + Result result = new Result<>(); + Object operateCode = redisUtil.get(CommonConstant.THIRD_LOGIN_CODE); + if(operateCode==null || !operateCode.toString().equals(json.getString("operateCode"))){ + result.setSuccess(false); + result.setMessage("校验失败"); + return result; + } + String username = json.getString("uuid"); + SysUser user = this.sysUserService.getUserByName(username); + if(user==null){ + result.setMessage("用户未找到"); + result.setSuccess(false); + return result; + } + String password = json.getString("password"); + String salt = user.getSalt(); + String passwordEncode = PasswordUtil.encrypt(user.getUsername(), password, salt); + if(!passwordEncode.equals(user.getPassword())){ + result.setMessage("密码不正确"); + result.setSuccess(false); + return result; + } + + sysUserService.updateById(user); + result.setSuccess(true); + // 生成token + String token = saveToken(user); + result.setResult(token); + return result; + } + + private String saveToken(SysUser user) { + // 生成token + String token = JwtUtil.sign(user.getUsername(), user.getPassword(), CommonConstant.CLIENT_TYPE_PC); + redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); + // 设置超时时间 + redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000); + return token; + } + + /** + * 第三方登录回调接口 + * @param token + * @param thirdType + * @return + * @throws Exception + */ + @SuppressWarnings("unchecked") + @RequestMapping(value = "/getLoginUser/{token}/{thirdType}/{tenantId}", method = RequestMethod.GET) + @ResponseBody + public Result getThirdLoginUser(@PathVariable("token") String token,@PathVariable("thirdType") String thirdType,@PathVariable("tenantId") String tenantId) throws Exception { + Result result = new Result(); + String username = JwtUtil.getUsername(token); + // 代码逻辑说明: [QQYUN-11021]三方登录接口通过token获取用户信息漏洞修复------------ + if (!TokenUtils.verifyToken(token, sysBaseAPI, redisUtil)) { + return Result.noauth("token验证失败"); + } + //1. 校验用户是否有效 + SysUser sysUser = sysUserService.getUserByName(username); + result = sysUserService.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + return result; + } + // 代码逻辑说明: 如果真实姓名和头像不存在就取第三方登录的 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysThirdAccount::getSysUserId,sysUser.getId()); + query.eq(SysThirdAccount::getThirdType,thirdType); + query.eq(SysThirdAccount::getTenantId,oConvertUtils.getInt(tenantId,CommonConstant.TENANT_ID_DEFAULT_VALUE)); + // 代码逻辑说明: [QQYUN-4883]钉钉auth登录同一个租户下有同一个用户id------------ + List accountList = sysThirdAccountService.list(query); + SysThirdAccount account = new SysThirdAccount(); + if(CollectionUtil.isNotEmpty(accountList)){ + account = accountList.get(0); + } + if(oConvertUtils.isEmpty(sysUser.getRealname())){ + sysUser.setRealname(account.getRealname()); + } + if(oConvertUtils.isEmpty(sysUser.getAvatar())){ + sysUser.setAvatar(account.getAvatar()); + } + JSONObject obj = new JSONObject(); + //第三方登确定登录租户和部门逻辑 + this.setUserTenantAndDepart(sysUser,obj,result); + //用户登录信息 + obj.put("userInfo", sysUser); + //获取字典缓存【解决 #Ghb-boot/issues/3998】 + obj.put("sysAllDictItems", sysDictService.queryAllDictItems()); + //token 信息 + obj.put("token", token); + result.setResult(obj); + result.setSuccess(true); + result.setCode(200); + baseCommonService.addLog("用户名: " + username + ",登录成功[第三方用户]!", CommonConstant.LOG_TYPE_1, null); + return result; + } + /** + * 第三方绑定手机号返回token + * + * @param jsonObject + * @return + */ + @Operation(summary="手机号登录接口") + @PostMapping("/bindingThirdPhone") + @ResponseBody + public Result bindingThirdPhone(@RequestBody JSONObject jsonObject) { + Result result = new Result(); + String phone = jsonObject.getString("mobile"); + String thirdUserUuid = jsonObject.getString("thirdUserUuid"); + // 校验验证码 + String captcha = jsonObject.getString("captcha"); + // 代码逻辑说明: VUEN-2245 【漏洞】发现新漏洞待处理20220906 + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; + Object captchaCache = redisUtil.get(redisKey); + if (oConvertUtils.isEmpty(captcha) || !captcha.equals(captchaCache)) { + result.setMessage("验证码错误"); + result.setSuccess(false); + return result; + } + //校验用户有效性 + SysUser sysUser = sysUserService.getUserByPhone(phone); + if(sysUser != null){ + // 存在用户,直接绑定 + sysThirdAccountService.updateThirdUserId(sysUser,thirdUserUuid); + }else{ + // 不存在手机号,创建用户 + sysUser = sysThirdAccountService.createUser(phone,thirdUserUuid,CommonConstant.TENANT_ID_DEFAULT_VALUE); + } + String token = saveToken(sysUser); + result.setSuccess(true); + result.setResult(token); + return result; + } + + /** + * 企业微信/钉钉 OAuth2登录 + * + * @param source + * @param state + * @return + */ + @ResponseBody + @GetMapping("/oauth2/{source}/login") + public String oauth2LoginCallback(@PathVariable("source") String source, @RequestParam("state") String state, HttpServletRequest request, HttpServletResponse response, + @RequestParam(value = "tenantId",required = false,defaultValue = "0") String tenantId) throws Exception { + String url; + //应用id为空,说明没有配置lowAppId + if(oConvertUtils.isEmpty(tenantId)){ + return "租户编码未配置"; + } + if (CommonConstant.WECHAT_ENTERPRISE.equalsIgnoreCase(source)) { + //换成第三方app配置表 + SysThirdAppConfig config = appConfigService.getThirdConfigByThirdType(Integer.valueOf(tenantId), MessageTypeEnum.QYWX.getType()); + if(null == config){ + return "还未配置企业微信应用,请配置企业微信应用"; + } + StringBuilder builder = new StringBuilder(); + // 构造企业微信OAuth2登录授权地址 + builder.append("https://open.weixin.qq.com/connect/oauth2/authorize"); + // 企业的CorpID + builder.append("?appid=").append(config.getClientId()); + // 授权后重定向的回调链接地址,请使用urlencode对链接进行处理 + String redirectUri = CommonUtils.getBaseUrl(request) + "/sys/thirdLogin/oauth2/wechat_enterprise/callback?tenantId="+tenantId;; + builder.append("&redirect_uri=").append(URLEncoder.encode(redirectUri, "UTF-8")); + // 返回类型,此时固定为:code + builder.append("&response_type=code"); + // 应用授权作用域。 + // snsapi_base:静默授权,可获取成员的的基础信息(UserId与DeviceId); + builder.append("&scope=snsapi_base"); + // 重定向后会带上state参数,长度不可超过128个字节 + builder.append("&state=").append(state); + // 终端使用此参数判断是否需要带上身份信息 + builder.append("#wechat_redirect"); + url = builder.toString(); + } else if (CommonConstant.DINGTALK.equalsIgnoreCase(source)) { + //换成第三方app配置表 + SysThirdAppConfig appConfig = appConfigService.getThirdConfigByThirdType(Integer.valueOf(tenantId), MessageTypeEnum.DD.getType()); + if(null == appConfig){ + return "还未配置钉钉应用,请配置钉钉应用"; + } + StringBuilder builder = new StringBuilder(); + // 构造钉钉OAuth2登录授权地址 + builder.append("https://login.dingtalk.com/oauth2/auth"); + // 授权通过/拒绝后回调地址。 + // 注意 需要与注册应用时登记的域名保持一致。 + String redirectUri = CommonUtils.getBaseUrl(request) + "/sys/thirdLogin/oauth2/dingtalk/callback?tenantId="+tenantId; + builder.append("?redirect_uri=").append(URLEncoder.encode(redirectUri, "UTF-8")); + // 固定值为code。 + // 授权通过后返回authCode。 + builder.append("&response_type=code"); + // 步骤一中创建的应用详情中获取。 + // 企业内部应用:client_id为应用的AppKey。 + builder.append("&client_id=").append(appConfig.getClientId()); + // 授权范围,授权页面显示的授权信息以应用注册时配置的为准。 + // openid:授权后可获得用户userid + builder.append("&scope=openid"); + // 跟随authCode原样返回。 + builder.append("&state=").append(state); + // 代码逻辑说明: [issues/I5BOUF]oauth2 钉钉无法登录------------ + builder.append("&prompt=").append("consent"); + url = builder.toString(); + } else { + return "不支持的source"; + } + log.info("oauth2 login url:" + url); + response.sendRedirect(url); + return "login…"; + } + + /** + * 企业微信/钉钉 OAuth2登录回调 + * + * @param code + * @param state + * @param response + * @return + */ + @ResponseBody + @GetMapping("/oauth2/{source}/callback") + public String oauth2LoginCallback( + @PathVariable("source") String source, + // 企业微信返回的code + @RequestParam(value = "code", required = false) String code, + // 钉钉返回的code + @RequestParam(value = "authCode", required = false) String authCode, + @RequestParam("state") String state, + @RequestParam(name = "tenantId",defaultValue = "0") String tenantId, + HttpServletResponse response) { + SysUser loginUser; + if (CommonConstant.WECHAT_ENTERPRISE.equalsIgnoreCase(source)) { + log.info("【企业微信】OAuth2登录进入callback:code=" + code + ", state=" + state); + loginUser = thirdAppWechatEnterpriseService.oauth2Login(code,Integer.valueOf(tenantId)); + if (loginUser == null) { + return "登录失败"; + } + } else if (CommonConstant.DINGTALK.equalsIgnoreCase(source)) { + log.info("【钉钉】OAuth2登录进入callback:authCode=" + authCode + ", state=" + state); + loginUser = thirdAppDingtalkService.oauth2Login(authCode,Integer.valueOf(tenantId)); + if (loginUser == null) { + return "登录失败"; + } + } else { + return "不支持的source"; + } + try { + // 代码逻辑说明: 工作流发送消息 点击消息链接跳转办理页面 + String redirect = ""; + if (state.indexOf("?") > 0) { + String[] arr = state.split("\\?"); + state = arr[0]; + if(arr.length>1){ + redirect = arr[1]; + } + } + + String token = saveToken(loginUser); + state += "/oauth2-app/login?oauth2LoginToken=" + URLEncoder.encode(token, "UTF-8") + "&tenantId=" + URLEncoder.encode(tenantId, "UTF-8"); + // 代码逻辑说明: [issues/I5BOUF]oauth2 钉钉无法登录------------ + state += "&thirdType=" + source; + //state += "&thirdType=" + "wechat_enterprise"; + if (redirect != null && redirect.length() > 0) { + state += "&" + redirect; + } + + log.info("OAuth2登录重定向地址: " + state); + try { + response.sendRedirect(state); + return "ok"; + } catch (IOException e) { + e.printStackTrace(); + return "重定向失败"; + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return "解码失败"; + } + } + + /** + * 注册账号并绑定第三方账号 【低代码应用专用接口】 + * @param jsonObject + * @param user + * @return + */ + @ResponseBody + @PutMapping("/registerBindThirdAccount") + public Result registerBindThirdAccount(@RequestBody JSONObject jsonObject, SysUser user) { + //手机号 + String phone = jsonObject.getString("phone"); + //验证码 + String smscode = jsonObject.getString("smscode"); + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE + phone; + Object code = redisUtil.get(redisKey); + //第三方uuid + String thirdUserUuid = jsonObject.getString("thirdUserUuid"); + String username = jsonObject.getString("username"); + //未设置用户名,则用手机号作为用户名 + if (oConvertUtils.isEmpty(username)) { + username = phone; + } + //未设置密码,则随机生成一个密码 + String password = jsonObject.getString("password"); + if (oConvertUtils.isEmpty(password)) { + password = RandomUtil.randomString(8); + } + String email = jsonObject.getString("email"); + SysUser sysUser1 = sysUserService.getUserByName(username); + if (sysUser1 != null) { + return Result.error("用户名已注册"); + } + SysUser sysUser2 = sysUserService.getUserByPhone(phone); + if (sysUser2 != null) { + return Result.error("该手机号已注册"); + } + if (oConvertUtils.isNotEmpty(email)) { + SysUser sysUser3 = sysUserService.getUserByEmail(email); + if (sysUser3 != null) { + return Result.error("邮箱已被注册"); + } + } + if (null == code) { + return Result.error("手机验证码失效,请重新获取"); + } + if (!smscode.equals(code.toString())) { + return Result.error("手机验证码错误"); + } + String realname = jsonObject.getString("realname"); + if (oConvertUtils.isEmpty(realname)) { + realname = username; + } + try { + //保存用户表 + user.setCreateTime(new Date()); + String salt = oConvertUtils.randomGen(8); + String passwordEncode = PasswordUtil.encrypt(username, password, salt); + user.setSalt(salt); + user.setUsername(username); + user.setRealname(realname); + user.setPassword(passwordEncode); + user.setEmail(email); + user.setPhone(phone); + user.setStatus(CommonConstant.USER_UNFREEZE); + user.setDelFlag(CommonConstant.DEL_FLAG_0); + user.setActivitiSync(CommonConstant.ACT_SYNC_1); + sysUserService.addUserWithRole(user, ""); + //保存第三方用户表 + sysThirdAccountService.updateThirdUserId(user, thirdUserUuid); + String token = saveToken(user); + return Result.ok(token); + } catch (Exception e) { + return Result.error("注册失败"); + } + } + + /** + * 设置用户租户和部门信息 + * + * @param sysUser + * @param obj + * @param result + */ + private void setUserTenantAndDepart(SysUser sysUser, JSONObject obj, Result result) { + //1.设置登录租户 + sysUserService.setLoginTenant(sysUser, obj, sysUser.getUsername(), result); + //2.设置登录部门 + String orgCode = sysUser.getOrgCode(); + //部门不为空还是用原来的部门code + if(StringUtils.isEmpty(orgCode)){ + List departs = sysDepartService.queryUserDeparts(sysUser.getId()); + //部门不为空取第一个作为当前登录部门 + if(CollectionUtil.isNotEmpty(departs)){ + orgCode = departs.get(0).getOrgCode(); + sysUser.setOrgCode(orgCode); + this.sysUserService.updateUserDepart(sysUser.getUsername(), orgCode,null); + } + } + } + + /** + * 新版钉钉登录 + * + * @param authCode + * @param state + * @param tenantId + * @param response + * @return + */ + @ResponseBody + @GetMapping("/oauth2/dingding/login") + public String OauthDingDingLogin(@RequestParam(value = "authCode", required = false) String authCode, + @RequestParam("state") String state, + @RequestParam(name = "tenantId",defaultValue = "0") String tenantId, + HttpServletResponse response) { + SysUser loginUser = thirdAppDingtalkService.oauthDingDingLogin(authCode,Integer.valueOf(tenantId)); + try { + String redirect = ""; + if (state.indexOf("?") > 0) { + String[] arr = state.split("\\?"); + state = arr[0]; + if(arr.length>1){ + redirect = arr[1]; + } + } + String token = saveToken(loginUser); + state += "/oauth2-app/login?oauth2LoginToken=" + URLEncoder.encode(token, "UTF-8") + "&tenantId=" + URLEncoder.encode(tenantId, "UTF-8"); + state += "&thirdType=DINGTALK"; + if (redirect != null && redirect.length() > 0) { + state += "&" + redirect; + } + log.info("OAuth2登录重定向地址: " + state); + try { + response.sendRedirect(state); + return "ok"; + } catch (IOException e) { + log.error(e.getMessage(),e); + return "重定向失败"; + } + } catch (UnsupportedEncodingException e) { + log.error(e.getMessage(),e); + return "解码失败"; + } + } + + /** + * 获取企业id和应用id + * @param tenantId + * @return + */ + @ResponseBody + @GetMapping("/get/corpId/clientId") + public Result getCorpIdClientId(@RequestParam(value = "tenantId", defaultValue = "0") String tenantId){ + Result result = new Result<>(); + SysThirdAppConfig sysThirdAppConfig = thirdAppDingtalkService.getCorpIdClientId(Integer.valueOf(tenantId)); + result.setSuccess(true); + result.setResult(sysThirdAppConfig); + return result; + } +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/WechatVerifyController.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/WechatVerifyController.java new file mode 100644 index 0000000..6d6c70e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/controller/WechatVerifyController.java @@ -0,0 +1,43 @@ +package com.ghb.base.modules.system.controller; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import com.ghb.base.modules.system.util.XssUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.PrintWriter; + +/** + * @Description: 企业微信证书验证 + * @author: wangshuai + * @date: 2023/12/6 10:42 + */ +@RestController +@Slf4j +public class WechatVerifyController { + + /** + * 企业微信验证 + */ + @RequestMapping(value = "/WW_verify_{code}.txt") + public void mpVerify(@PathVariable("code") String code, HttpServletResponse response) { + if(StringUtils.isEmpty(code)){ + log.error("企业微信证书验证失败!(code为空)"); + return; + } + try { + PrintWriter writer = response.getWriter(); + code = XssUtils.scriptXss(code); + writer.write(code); + writer.close(); + } catch (Exception e) { + log.error("企业微信证书验证失败!"); + log.error(e.getMessage(), e); + e.printStackTrace(); + } + } +} + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAnnouncement.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAnnouncement.java new file mode 100644 index 0000000..d21395d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAnnouncement.java @@ -0,0 +1,193 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; + +/** + * @Description: 系统通告表 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version: V1.0 + */ +@Data +@TableName("sys_announcement") +public class SysAnnouncement implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private java.lang.String id; + /** + * 标题 + */ + @Excel(name = "标题", width = 15) + private java.lang.String titile; + /** + * 内容 + */ + @Excel(name = "内容", width = 30) + private java.lang.String msgContent; + /** + * 开始时间 + */ + @Excel(name = "开始时间", width = 15, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date startTime; + /** + * 结束时间 + */ + @Excel(name = "结束时间", width = 15, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date endTime; + /** + * 发布人 + */ + @Excel(name = "发布人", width = 15) + @Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname") + private java.lang.String sender; + /** + * 优先级(L低,M中,H高) + */ + @Excel(name = "优先级", width = 15, dicCode = "priority") + @Dict(dicCode = "priority") + private java.lang.String priority; + + /** + * 消息类型1:通知公告2:系统消息 + */ + @Excel(name = "消息类型", width = 15, dicCode = "msg_category") + @Dict(dicCode = "msg_category") + private java.lang.String msgCategory; + /** + * 通告对象类型(USER:指定用户,ALL:全体用户) + */ + @Excel(name = "通告对象类型", width = 15, dicCode = "msg_type") + @Dict(dicCode = "msg_type") + private java.lang.String msgType; + /** + * 发布状态(0未发布,1已发布,2已撤销) + */ + @Excel(name = "发布状态", width = 15, dicCode = "send_status") + @Dict(dicCode = "send_status") + private java.lang.String sendStatus; + /** + * 发布时间 + */ + @Excel(name = "发布时间", width = 15, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date sendTime; + /** + * 撤销时间 + */ + @Excel(name = "撤销时间", width = 15, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date cancelTime; + /** + * 删除状态(0,正常,1已删除) + */ + private java.lang.String delFlag; + /** + * 创建人 + */ + private java.lang.String createBy; + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date createTime; + /** + * 更新人 + */ + private java.lang.String updateBy; + /** + * 更新时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private java.util.Date updateTime; + /** + * 指定用户 + **/ + private java.lang.String userIds; + /** + * 业务类型(email:邮件 bpm:流程 tenant_invite:租户邀请) + */ + private java.lang.String busType; + /** + * 业务id + */ + private java.lang.String busId; + /** + * 打开方式 组件:component 路由:url + */ + private java.lang.String openType; + /** + * 组件/路由 地址 + */ + private java.lang.String openPage; + /** + * 摘要/扩展业务参数 + * + * 示例: + * 1 摘要值 + * 放假安排 + * 2 跳转流程的参数值 + * {"taskDetail":true,"procInsId":"1706547306004377602","taskId":"task630958764530507776"} + */ + private java.lang.String msgAbstract; + /** + * 钉钉task_id,用于撤回消息 + */ + private String dtTaskId; + + /** + * 阅读状态 1表示已经阅读 + */ + private transient String readFlag; + + /** + * 标星状态 1表示标星 + */ + private transient String starFlag; + + /** + * 发送记录ID + */ + private transient String sendId; + + /**租户ID*/ + private java.lang.Integer tenantId; + + /** + * 枚举:com.ghb.base.common.constant.enums.NoticeTypeEnum + * 通知类型(system:系统消息、file:知识库、flow:流程、plan:日程计划、meeting:会议) + */ + private String noticeType; + /**附件字段*/ + private java.lang.String files; + /**访问次数*/ + private java.lang.Integer visitsNum; + /**是否置顶(0否 1是)*/ + private java.lang.Integer izTop; + /**是否审批(0否 1是)*/ + private java.lang.String izApproval; + /**流程状态*/ + private java.lang.String bpmStatus; + /**消息归类*/ + private java.lang.String msgClassify; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAnnouncementSend.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAnnouncementSend.java new file mode 100644 index 0000000..e9251ac --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAnnouncementSend.java @@ -0,0 +1,53 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * @Description: 用户通告阅读标记表 + * @Author: Ghb-boot + * @Date: 2019-02-21 + * @Version: V1.0 + */ +@Data +@TableName("sys_announcement_send") +public class SysAnnouncementSend implements Serializable { + private static final long serialVersionUID = 1L; + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + private java.lang.String id; + /**通告id*/ + private java.lang.String anntId; + /**用户id*/ + private java.lang.String userId; + /**阅读状态(0未读,1已读)*/ + private java.lang.Integer readFlag; + /**阅读时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date readTime; + /**创建人*/ + private java.lang.String createBy; + /**创建时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date createTime; + /**更新人*/ + private java.lang.String updateBy; + /**更新时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date updateTime; + + /** + * 是否标星 当值为1是标星消息 + */ + private String starFlag; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAppVersion.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAppVersion.java new file mode 100644 index 0000000..5d47c15 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysAppVersion.java @@ -0,0 +1,88 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; + +/** + * @Description: app系统配置 + * @Author: Ghb-boot + * @Date: 2021-07-07 + * @Version: V1.0 + * + * e3e3NcxzbUiGa53YYVXxWc8ADo5ISgQGx/gaZwERF91oAryDlivjqBv3wqRArgChupi+Y/Gg/swwGEyL0PuVFg== + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="app系统配置") +public class SysAppVersion implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + /**创建人*/ + @Schema(description = "创建人") + private String createBy; + /**创建日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private java.util.Date createTime; + /**更新人*/ + @Schema(description = "更新人") + private String updateBy; + /**更新日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新日期") + private java.util.Date updateTime; + /**所属部门*/ + @Schema(description = "所属部门") + private String sysOrgCode; + /**标题*/ + @Excel(name = "标题", width = 15) + @Schema(description = "标题") + private String appTitle; + /**logo*/ + @Excel(name = "logo", width = 15) + @Schema(description = "logo") + private String appLogo; + /**首页轮播图*/ + @Excel(name = "首页轮播图", width = 15) + @Schema(description = "首页轮播图") + private String carouselImgJson; + /**首页菜单图*/ + @Excel(name = "首页菜单图", width = 15) + @Schema(description = "首页菜单图") + private String routeImgJson; + /**app版本*/ + @Schema(description = "版本") + private String appVersion; + /**版本编码*/ + @Schema(description = "版本编码") + private Integer versionNum; + /**app下载路径*/ + @Schema(description = "app下载路径") + private String downloadUrl; + /**热更新路径*/ + @Schema(description = "热更新路径") + private String wgtUrl; + /**热更新路径*/ + @Schema(description = "桌面端下载路径") + private String webDownloadUrl; + /**更新内容*/ + @Schema(description = "更新内容") + private String updateNote; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysCategory.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysCategory.java new file mode 100644 index 0000000..c0d1ecd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysCategory.java @@ -0,0 +1,69 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; + +/** + * @Description: 分类字典 + * @Author: Ghb-boot + * @Date: 2019-05-29 + * @Version: V1.0 + */ +@Data +@TableName("sys_category") +public class SysCategory implements Serializable,Comparable{ + private static final long serialVersionUID = 1L; + + /**主键*/ + @TableId(type = IdType.ASSIGN_ID) + private java.lang.String id; + /**父级节点*/ + private java.lang.String pid; + /**类型名称*/ + @Excel(name = "类型名称", width = 15) + private java.lang.String name; + /**类型编码*/ + @Excel(name = "类型编码", width = 15) + private java.lang.String code; + /**创建人*/ + private java.lang.String createBy; + /**创建日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date createTime; + /**更新人*/ + private java.lang.String updateBy; + /**更新日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date updateTime; + /**所属部门*/ + private java.lang.String sysOrgCode; + /**是否有子节点*/ + @Excel(name = "是否有子节点(1:有)", width = 15) + private java.lang.String hasChild; + + /**租户ID*/ + private java.lang.Integer tenantId; + + @Override + public int compareTo(SysCategory o) { + //比较条件我们定的是按照code的长度升序 + // <0:当前对象比传入对象小。 + // =0:当前对象等于传入对象。 + // >0:当前对象比传入对象大。 + int s = this.code.length() - o.code.length(); + return s; + } + @Override + public String toString() { + return "SysCategory [code=" + code + ", name=" + name + "]"; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysCheckRule.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysCheckRule.java new file mode 100644 index 0000000..ababf53 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysCheckRule.java @@ -0,0 +1,88 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @Description: 编码校验规则 + * @Author: Ghb-boot + * @Date: 2020-02-04 + * @Version: V1.0 + */ +@Data +@TableName("sys_check_rule") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="编码校验规则") +public class SysCheckRule { + + /** + * 主键id + */ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private String id; + /** + * 规则名称 + */ + @Excel(name = "规则名称", width = 15) + @Schema(description = "规则名称") + private String ruleName; + /** + * 规则Code + */ + @Excel(name = "规则Code", width = 15) + @Schema(description = "规则Code") + private String ruleCode; + /** + * 规则JSON + */ + @Excel(name = "规则JSON", width = 15) + @Schema(description = "规则JSON") + private String ruleJson; + /** + * 规则描述 + */ + @Excel(name = "规则描述", width = 15) + @Schema(description = "规则描述") + private String ruleDescription; + /** + * 更新人 + */ + @Excel(name = "更新人", width = 15) + @Schema(description = "更新人") + private String updateBy; + /** + * 更新时间 + */ + @Excel(name = "更新时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private Date updateTime; + /** + * 创建人 + */ + @Excel(name = "创建人", width = 15) + @Schema(description = "创建人") + private String createBy; + /** + * 创建时间 + */ + @Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private Date createTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysComment.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysComment.java new file mode 100644 index 0000000..641a4d8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysComment.java @@ -0,0 +1,88 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * @Description: 系统评论回复表 + * @Author: Ghb-boot + * @Date: 2022-07-19 + * @Version: V1.0 + */ +@Data +@TableName("sys_comment") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="系统评论回复表") +public class SysComment implements Serializable { + private static final long serialVersionUID = 1L; + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private String id; + /**表名*/ + @Excel(name = "表名", width = 15) + @Schema(description = "表名") + private String tableName; + /**数据id*/ + @Excel(name = "数据id", width = 15) + @Schema(description = "数据id") + private String tableDataId; + /**来源用户id*/ + @Excel(name = "来源用户id", width = 15) + @Schema(description = "来源用户id") + @Dict(dictTable = "sys_user", dicCode = "id", dicText = "realname") + private String fromUserId; + /**发送给用户id(允许为空)*/ + @Excel(name = "发送给用户id(允许为空)", width = 15) + @Schema(description = "发送给用户id(允许为空)") + @Dict(dictTable = "sys_user", dicCode = "id", dicText = "realname") + private String toUserId; + /**评论id(允许为空,不为空时,则为回复)*/ + @Excel(name = "评论id(允许为空,不为空时,则为回复)", width = 15) + @Schema(description = "评论id(允许为空,不为空时,则为回复)") + @Dict(dictTable = "sys_comment", dicCode = "id", dicText = "comment_content") + private String commentId; + /**回复内容*/ + @Excel(name = "回复内容", width = 15) + @Schema(description = "回复内容") + private String commentContent; + /**创建人*/ + @Schema(description = "创建人") + private String createBy; + /**创建日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private Date createTime; + /**更新人*/ + @Schema(description = "更新人") + private String updateBy; + /**更新日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新日期") + private Date updateTime; + + /** + * 不是数据库字段,用于评论跳转 + */ + @TableField(exist = false) + private String tableId; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDataLog.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDataLog.java new file mode 100644 index 0000000..0799757 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDataLog.java @@ -0,0 +1,110 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.config.mqtoken.UserTokenContext; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.system.vo.LoginUser; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.util.StringUtils; + +import java.io.Serializable; +import java.util.Date; + +/** + * @Description: 系统数据日志 + * @author: Ghb-boot + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Slf4j +public class SysDataLog implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + /** + * id + */ + private String id; + + /** + * 创建人登录名称 + */ + private String createBy; + + /** + * 创建人真实名称 + */ + private String createName; + + /** + * 创建日期 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新人登录名称 + */ + private String updateBy; + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + /** + * 更新日期 + */ + private Date updateTime; + + /** + * 表名 + */ + private String dataTable; + + /** + * 数据ID + */ + private String dataId; + + /** + * 数据内容 + */ + private String dataContent; + + /** + * 版本号 + */ + private String dataVersion; + + + /** + * 类型,用于表单评论记录日志 区分数据 + */ + private String type; + + /** + * 通过 loginUser 设置 createName + */ + public void autoSetCreateName() { + try { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + this.setCreateName(sysUser.getRealname()); + } catch (Exception e) { + // QQYUN-13669 进一步优化:解决某些异步场景下获取用户信息为空的问题 + String token = UserTokenContext.getToken(); + if (StringUtils.hasText(token)) { + this.setCreateName(JwtUtil.getUsername(token)); + } else { + log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage()); + } + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDataSource.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDataSource.java new file mode 100644 index 0000000..21de7de --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDataSource.java @@ -0,0 +1,124 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * @Description: 多数据源管理 + * @Author: Ghb-boot + * @Date: 2019-12-25 + * @Version: V1.0 + */ +@Data +@TableName("sys_data_source") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="多数据源管理") +public class SysDataSource { + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private java.lang.String id; + /** + * 数据源编码 + */ + @Excel(name = "数据源编码", width = 15) + @Schema(description = "数据源编码") + private java.lang.String code; + /** + * 数据源名称 + */ + @Excel(name = "数据源名称", width = 15) + @Schema(description = "数据源名称") + private java.lang.String name; + /** + * 描述 + */ + @Excel(name = "备注", width = 15) + @Schema(description = "备注") + private java.lang.String remark; + /** + * 数据库类型 + */ + @Dict(dicCode = "database_type") + @Excel(name = "数据库类型", width = 15, dicCode = "database_type") + @Schema(description = "数据库类型") + private java.lang.String dbType; + /** + * 驱动类 + */ + @Excel(name = "驱动类", width = 15) + @Schema(description = "驱动类") + private java.lang.String dbDriver; + /** + * 数据源地址 + */ + @Excel(name = "数据源地址", width = 15) + @Schema(description = "数据源地址") + private java.lang.String dbUrl; + /** + * 数据库名称 + */ + @Excel(name = "数据库名称", width = 15) + @Schema(description = "数据库名称") + private java.lang.String dbName; + /** + * 用户名 + */ + @Excel(name = "用户名", width = 15) + @Schema(description = "用户名") + private java.lang.String dbUsername; + /** + * 密码 + */ + @Excel(name = "密码", width = 15) + @Schema(description = "密码") + private java.lang.String dbPassword; + /** + * 创建人 + */ + @Schema(description = "创建人") + private java.lang.String createBy; + /** + * 创建日期 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private java.util.Date createTime; + /** + * 更新人 + */ + @Schema(description = "更新人") + private java.lang.String updateBy; + /** + * 更新日期 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新日期") + private java.util.Date updateTime; + /** + * 所属部门 + */ + @Excel(name = "所属部门", width = 15) + @Schema(description = "所属部门") + private java.lang.String sysOrgCode; + + /**租户ID*/ + @Schema(description = "租户ID") + private java.lang.Integer tenantId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepart.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepart.java new file mode 100644 index 0000000..d2bfa2a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepart.java @@ -0,0 +1,171 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; + +/** + *

+ * 部门表 + *

+ * + * @Author Steve + * @Since 2019-01-22 + */ +@Data +@TableName("sys_depart") +public class SysDepart implements Serializable { + private static final long serialVersionUID = 1L; + + /**ID*/ + @TableId(type = IdType.ASSIGN_ID) + private String id; + /**父机构ID*/ + private String parentId; + /**机构/部门名称*/ + @Excel(name="机构/部门名称",width=15) + private String departName; + /**机构/部门路径名称(非持久化字段)*/ + @TableField(exist = false) + private String departPathName; + /**英文名*/ + @Excel(name="英文名",width=15) + private String departNameEn; + /**缩写*/ + private String departNameAbbr; + /**排序*/ + @Excel(name="排序",width=15) + private Integer departOrder; + /**描述*/ + @Excel(name="描述",width=15) + private String description; + /**机构类别 1=公司,2=组织机构,3=岗位 4=子公司*/ + @Excel(name="机构类别",width=15,dicCode="org_category") + private String orgCategory; + /**机构类型*/ + private String orgType; + /**机构编码*/ + @Excel(name="机构编码",width=15) + private String orgCode; + /**手机号*/ + @Excel(name="手机号",width=15) + private String mobile; + /**传真*/ + @Excel(name="传真",width=15) + private String fax; + /**地址*/ + @Excel(name="地址",width=15) + private String address; + /**备注*/ + @Excel(name="备注",width=15) + private String memo; + /**状态(1启用,0不启用)*/ + @Dict(dicCode = "depart_status") + private String status; + /**删除状态(0,正常,1已删除)*/ + @Dict(dicCode = "del_flag") + private String delFlag; + /**对接企业微信的ID*/ + private String qywxIdentifier; + /**对接钉钉的部门ID*/ + private String dingIdentifier; + /**创建人*/ + private String createBy; + /**创建日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date createTime; + /**更新人*/ + private String updateBy; + /**更新日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date updateTime; + /**租户ID*/ + private java.lang.Integer tenantId; + + /**是否有叶子节点: 1是0否*/ + private Integer izLeaf; + + //update-begin---author:wangshuai ---date:20200308 for:[JTC-119]在部门管理菜单下设置部门负责人,新增字段负责人ids和旧的负责人ids + /**部门负责人的ids*/ + @TableField(exist = false) + private String directorUserIds; + /**旧的部门负责人的ids(用于比较删除和新增)*/ + @TableField(exist = false) + private String oldDirectorUserIds; + //update-end---author:wangshuai ---date:20200308 for:[JTC-119]新增字段负责人ids和旧的负责人ids + + /** + * 职级id + */ + @Excel(name="职级",width=15,dictTable = "sys_position", dicCode = "id", dicText = "name") + @Dict(dictTable = "sys_position", dicCode = "id", dicText = "name") + private String positionId; + + /** + * 部门岗位id + */ + @Excel(name="上级岗位",width=15,dictTable = "sys_depart", dicCode = "id", dicText = "depart_name") + @Dict(dictTable = "sys_depart", dicCode = "id", dicText = "depart_name") + private String depPostParentId; + + /** + * 重写equals方法 + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + SysDepart depart = (SysDepart) o; + return Objects.equals(id, depart.id) && + Objects.equals(parentId, depart.parentId) && + Objects.equals(departName, depart.departName) && + Objects.equals(departNameEn, depart.departNameEn) && + Objects.equals(departNameAbbr, depart.departNameAbbr) && + Objects.equals(departOrder, depart.departOrder) && + Objects.equals(description, depart.description) && + Objects.equals(orgCategory, depart.orgCategory) && + Objects.equals(orgType, depart.orgType) && + Objects.equals(orgCode, depart.orgCode) && + Objects.equals(mobile, depart.mobile) && + Objects.equals(fax, depart.fax) && + Objects.equals(address, depart.address) && + Objects.equals(memo, depart.memo) && + Objects.equals(status, depart.status) && + Objects.equals(delFlag, depart.delFlag) && + Objects.equals(createBy, depart.createBy) && + Objects.equals(createTime, depart.createTime) && + Objects.equals(updateBy, depart.updateBy) && + Objects.equals(tenantId, depart.tenantId) && + Objects.equals(updateTime, depart.updateTime); + } + + /** + * 重写hashCode方法 + */ + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), id, parentId, departName, + departNameEn, departNameAbbr, departOrder, description,orgCategory, + orgType, orgCode, mobile, fax, address, memo, status, + delFlag, createBy, createTime, updateBy, updateTime, tenantId); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartPermission.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartPermission.java new file mode 100644 index 0000000..75fd15b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartPermission.java @@ -0,0 +1,55 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableField; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * @Description: 部门权限表 + * @Author: Ghb-boot + * @Date: 2020-02-11 + * @Version: V1.0 + */ +@Data +@TableName("sys_depart_permission") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="部门权限表") +public class SysDepartPermission { + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private java.lang.String id; + /**部门id*/ + @Excel(name = "部门id", width = 15) + @Schema(description = "部门id") + private java.lang.String departId; + /**权限id*/ + @Excel(name = "权限id", width = 15) + @Schema(description = "权限id") + private java.lang.String permissionId; + /**数据规则id*/ + @Schema(description = "数据规则id") + private java.lang.String dataRuleIds; + + public SysDepartPermission() { + + } + + public SysDepartPermission(String departId, String permissionId) { + this.departId = departId; + this.permissionId = permissionId; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRole.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRole.java new file mode 100644 index 0000000..c7291ec --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRole.java @@ -0,0 +1,75 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableField; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ghb.base.common.aspect.annotation.Dict; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * @Description: 部门角色 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +@Data +@TableName("sys_depart_role") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="部门角色") +public class SysDepartRole { + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private java.lang.String id; + /**部门id*/ + @Excel(name = "部门id", width = 15) + @Schema(description = "部门id") + @Dict(dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + private java.lang.String departId; + /**部门角色名称*/ + @Excel(name = "部门角色名称", width = 15) + @Schema(description = "部门角色名称") + private java.lang.String roleName; + /**部门角色编码*/ + @Excel(name = "部门角色编码", width = 15) + @Schema(description = "部门角色编码") + private java.lang.String roleCode; + /**描述*/ + @Excel(name = "描述", width = 15) + @Schema(description = "描述") + private java.lang.String description; + /**创建人*/ + @Excel(name = "创建人", width = 15) + @Schema(description = "创建人") + private java.lang.String createBy; + /**创建时间*/ + @Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private java.util.Date createTime; + /**更新人*/ + @Excel(name = "更新人", width = 15) + @Schema(description = "更新人") + private java.lang.String updateBy; + /**更新时间*/ + @Excel(name = "更新时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private java.util.Date updateTime; + + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRolePermission.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRolePermission.java new file mode 100644 index 0000000..527d1bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRolePermission.java @@ -0,0 +1,67 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableField; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * @Description: 部门角色权限 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +@Data +@TableName("sys_depart_role_permission") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="部门角色权限") +public class SysDepartRolePermission { + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private java.lang.String id; + /**部门id*/ + @Excel(name = "部门id", width = 15) + @Schema(description = "部门id") + private java.lang.String departId; + /**角色id*/ + @Excel(name = "角色id", width = 15) + @Schema(description = "角色id") + private java.lang.String roleId; + /**权限id*/ + @Excel(name = "权限id", width = 15) + @Schema(description = "权限id") + private java.lang.String permissionId; + /**dataRuleIds*/ + @Excel(name = "dataRuleIds", width = 15) + @Schema(description = "dataRuleIds") + private java.lang.String dataRuleIds; + /** 操作时间 */ + @Excel(name = "操作时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "操作时间") + private java.util.Date operateDate; + /** 操作ip */ + private java.lang.String operateIp; + + public SysDepartRolePermission() { + } + + public SysDepartRolePermission(String roleId, String permissionId) { + this.roleId = roleId; + this.permissionId = permissionId; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRoleUser.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRoleUser.java new file mode 100644 index 0000000..ac2b001 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDepartRoleUser.java @@ -0,0 +1,52 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableField; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * @Description: 部门角色人员信息 + * @Author: Ghb-boot + * @Date: 2020-02-13 + * @Version: V1.0 + */ +@Data +@TableName("sys_depart_role_user") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="部门角色人员信息") +public class SysDepartRoleUser { + + /**主键id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private java.lang.String id; + /**用户id*/ + @Excel(name = "用户id", width = 15) + @Schema(description = "用户id") + private java.lang.String userId; + /**角色id*/ + @Excel(name = "角色id", width = 15) + @Schema(description = "角色id") + private java.lang.String droleId; + + public SysDepartRoleUser() { + + } + + public SysDepartRoleUser(String userId, String droleId) { + this.userId = userId; + this.droleId = droleId; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDict.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDict.java new file mode 100644 index 0000000..258855d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDict.java @@ -0,0 +1,90 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 字典表 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysDict implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * [预留字段,暂时无用] + * 字典类型,0 string,1 number类型,2 boolean + * 前端js对stirng类型和number类型 boolean 类型敏感,需要区分。在select 标签匹配的时候会用到 + * 默认为string类型 + */ + private Integer type; + + /** + * 字典名称 + */ + private String dictName; + + /** + * 字典编码 + */ + private String dictCode; + + /** + * 描述 + */ + private String description; + + /** + * 删除状态 + */ + @TableLogic + private Integer delFlag; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /**租户ID*/ + private java.lang.Integer tenantId; + + /** 关联的低代码应用ID */ + private java.lang.String lowAppId; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDictItem.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDictItem.java new file mode 100644 index 0000000..8a317e6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysDictItem.java @@ -0,0 +1,86 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + *

+ * + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysDictItem implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 字典id + */ + private String dictId; + + /** + * 字典项文本 + */ + @Excel(name = "字典项文本", width = 20) + private String itemText; + + /** + * 字典项值 + */ + @Excel(name = "字典项值", width = 30) + private String itemValue; + + /** + * 描述 + */ + @Excel(name = "描述", width = 40) + private String description; + + /** + * 排序 + */ + @Excel(name = "排序", width = 15,type=4) + private Integer sortOrder; + + + /** + * 状态(1启用 0不启用) + */ + @Dict(dicCode = "dict_item_status") + private Integer status; + + private String createBy; + + private Date createTime; + + private String updateBy; + + private Date updateTime; + + /** + * 字典项颜色 + */ + private String itemColor; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysFillRule.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysFillRule.java new file mode 100644 index 0000000..31c17d5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysFillRule.java @@ -0,0 +1,86 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * @Description: 填值规则 + * @Author: Ghb-boot + * @Date: 2019-11-07 + * @Version: V1.0 + */ +@Data +@TableName("sys_fill_rule") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="填值规则") +public class SysFillRule { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键ID") + private java.lang.String id; + /** + * 规则名称 + */ + @Excel(name = "规则名称", width = 15) + @Schema(description = "规则名称") + private java.lang.String ruleName; + /** + * 规则Code + */ + @Excel(name = "规则Code", width = 15) + @Schema(description = "规则Code") + private java.lang.String ruleCode; + /** + * 规则实现类 + */ + @Excel(name = "规则实现类", width = 15) + @Schema(description = "规则实现类") + private java.lang.String ruleClass; + /** + * 规则参数 + */ + @Excel(name = "规则参数", width = 15) + @Schema(description = "规则参数") + private java.lang.String ruleParams; + /** + * 修改人 + */ + @Excel(name = "修改人", width = 15) + @Schema(description = "修改人") + private java.lang.String updateBy; + /** + * 修改时间 + */ + @Excel(name = "修改时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "修改时间") + private java.util.Date updateTime; + /** + * 创建人 + */ + @Excel(name = "创建人", width = 15) + @Schema(description = "创建人") + private java.lang.String createBy; + /** + * 创建时间 + */ + @Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private java.util.Date createTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysFormFile.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysFormFile.java new file mode 100644 index 0000000..5cd2b3f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysFormFile.java @@ -0,0 +1,59 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; +import java.util.Date; + +/** + * @Description: 表单评论文件 + * @Author: Ghb-boot + * @Date: 2022-07-21 + * @Version: V1.0 + */ +@Data +@TableName("sys_form_file") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="表单评论文件") +public class SysFormFile { + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private String id; + /**表名*/ + @Excel(name = "表名", width = 15) + @Schema(description = "表名") + private String tableName; + /**数据id*/ + @Excel(name = "数据id", width = 15) + @Schema(description = "数据id") + private String tableDataId; + /**关联文件id*/ + @Excel(name = "关联文件id", width = 15) + @Schema(description = "关联文件id") + private String fileId; + /**文档类型(folder:文件夹 excel:excel doc:word pp:ppt image:图片 archive:其他文档 video:视频)*/ + @Excel(name = "文档类型(folder:文件夹 excel:excel doc:word pp:ppt image:图片 archive:其他文档 video:视频)", width = 15) + @Schema(description = "文档类型(folder:文件夹 excel:excel doc:word pp:ppt image:图片 archive:其他文档 video:视频)") + private String fileType; + /**创建人登录名称*/ + @Excel(name = "创建人登录名称", width = 15) + @Schema(description = "创建人登录名称") + private String createBy; + /**创建日期*/ + @Excel(name = "创建日期", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private Date createTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysGatewayRoute.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysGatewayRoute.java new file mode 100644 index 0000000..5bef4dd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysGatewayRoute.java @@ -0,0 +1,118 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * @Description: gateway路由管理 + * @Author: Ghb-boot + * @Date: 2020-05-26 + * @Version: V1.0 + */ +@Data +@TableName("sys_gateway_route") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="gateway路由管理") +public class SysGatewayRoute implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + + /**routerKEy*/ + @Schema(description = "路由ID") + private String routerId; + + /**服务名*/ + @Excel(name = "服务名", width = 15) + @Schema(description = "服务名") + private String name; + + /**服务地址*/ + @Excel(name = "服务地址", width = 15) + @Schema(description = "服务地址") + private String uri; + + /** + * 断言配置 + */ + private String predicates; + + /** + * 过滤配置 + */ + private String filters; + + /**是否忽略前缀0-否 1-是*/ + @Excel(name = "忽略前缀", width = 15) + @Schema(description = "忽略前缀") + @Dict(dicCode = "yn") + private Integer stripPrefix; + + /**是否重试0-否 1-是*/ + @Excel(name = "是否重试", width = 15) + @Schema(description = "是否重试") + @Dict(dicCode = "yn") + private Integer retryable; + + /**是否为保留数据:0-否 1-是*/ + @Excel(name = "保留数据", width = 15) + @Schema(description = "保留数据") + @Dict(dicCode = "yn") + private Integer persistable; + + /**是否在接口文档中展示:0-否 1-是*/ + @Excel(name = "在接口文档中展示", width = 15) + @Schema(description = "在接口文档中展示") + @Dict(dicCode = "yn") + private Integer showApi; + + /**状态 1有效 0无效*/ + @Excel(name = "状态", width = 15) + @Schema(description = "状态") + @Dict(dicCode = "yn") + private Integer status; + + /**创建人*/ + @Schema(description = "创建人") + private String createBy; + /**创建日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private Date createTime; + + /** + * 删除状态(0未删除,1已删除) + */ + @TableLogic + private Integer delFlag; + /* *//**更新人*//* + @Schema(description = "更新人") + private String updateBy; + *//**更新日期*//* + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新日期") + private Date updateTime; + *//**所属部门*//* + @Schema(description = "所属部门") + private String sysOrgCode;*/ +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysLog.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysLog.java new file mode 100644 index 0000000..a334692 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysLog.java @@ -0,0 +1,130 @@ +package com.ghb.base.modules.system.entity; + +import java.util.Date; + +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.io.Serializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 系统日志表 + *

+ * + * @Author zhangweijian + * @since 2018-12-26 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + @Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 耗时 + */ + @Excel(name = "耗时(毫秒)", width = 15) + private Long costTime; + + /** + * IP + */ + @Excel(name = "IP", width = 15) + private String ip; + + /** + * 请求参数 + */ + private String requestParam; + + /** + * 请求类型 + */ + private String requestType; + + /** + * 请求路径 + */ + private String requestUrl; + /** + * 请求方法 + */ + private String method; + + /** + * 操作人用户名称 + */ + private String username; + /** + * 操作人用户账户 + */ + @Excel(name = "操作人", width = 15) + private String userid; + /** + * 操作详细日志 + */ + @Excel(name = "日志内容", width = 50) + private String logContent; + + /** + * 日志类型(1登录日志,2操作日志) + */ + @Dict(dicCode = "log_type") + private Integer logType; + + /** + * 操作类型(1查询,2添加,3修改,4删除,5导入,6导出) + */ + @Dict(dicCode = "operate_type") + private Integer operateType; + + /** + * 客户终端类型 pc:电脑端 app:手机端 h5:移动网页端 + */ + @Excel(name = "客户端类型", width = 15, dicCode = "client_type") + @Dict(dicCode = "client_type") + private String clientType; + + /** + * 租户ID + */ + private Integer tenantId; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPackPermission.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPackPermission.java new file mode 100644 index 0000000..6efd89d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPackPermission.java @@ -0,0 +1,59 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @Description: 产品包菜单关系表 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +@Data +@TableName("sys_tenant_pack_perms") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="产品包菜单关系表") +public class SysPackPermission implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键编号*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键编号") + private String id; + /**租户产品包名称*/ + @Excel(name = "租户产品包名称", width = 15) + @Schema(description = "租户产品包名称") + private String packId; + /**菜单id*/ + @Excel(name = "菜单id", width = 15) + @Schema(description = "菜单id") + private String permissionId; + /**创建人*/ + @Schema(description = "创建人") + private String createBy; + /**创建时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "创建时间") + private Date createTime; + /**更新人*/ + @Schema(description = "更新人") + private String updateBy; + /**更新时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "更新时间") + private Date updateTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPermission.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPermission.java new file mode 100644 index 0000000..94126fa --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPermission.java @@ -0,0 +1,187 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; +import com.ghb.base.modules.system.constant.DefIndexConst; + +/** + *

+ * 菜单权限表 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysPermission implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 父id + */ + private String parentId; + + /** + * 菜单名称 + */ + private String name; + + /** + * 菜单权限编码,例如:“sys:schedule:list,sys:schedule:info”,多个逗号隔开 + */ + private String perms; + /** + * 权限策略1显示2禁用 + */ + private String permsType; + + /** + * 菜单图标 + */ + private String icon; + + /** + * 组件 + */ + private String component; + + /** + * 组件名字 + */ + private String componentName; + + /** + * 路径 + */ + private String url; + /** + * 一级菜单跳转地址 + */ + private String redirect; + + /** + * 菜单排序 + */ + private Double sortNo; + + /** + * 类型(0:一级菜单;1:子菜单 ;2:按钮权限) + */ + @Dict(dicCode = "menu_type") + private Integer menuType; + + /** + * 是否叶子节点: 1:是 0:不是 + */ + @TableField(value="is_leaf") + private boolean leaf; + + /** + * 是否路由菜单: 0:不是 1:是(默认值1) + */ + @TableField(value="is_route") + private boolean route; + + + /** + * 是否缓存页面: 0:不是 1:是(默认值1) + */ + @TableField(value="keep_alive") + private boolean keepAlive; + + /** + * 描述 + */ + private String description; + + /** + * 创建人 + */ + private String createBy; + + /** + * 删除状态 0正常 1已删除 + */ + private Integer delFlag; + + /** + * 是否配置菜单的数据权限 1是0否 默认0 + */ + private Integer ruleFlag; + + /** + * 是否隐藏路由菜单: 0否,1是(默认值0) + */ + private boolean hidden; + + /** + * 是否隐藏Tab: 0否,1是(默认值0) + */ + private boolean hideTab; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /**按钮权限状态(0无效1有效)*/ + private java.lang.String status; + + /**alwaysShow*/ + private boolean alwaysShow; + + /*update_begin author:wuxianquan date:20190908 for:实体增加字段 */ + /** 外链菜单打开方式 0/内部打开 1/外部打开 */ + private boolean internalOrExternal; + /*update_end author:wuxianquan date:20190908 for:实体增加字段 */ + + public SysPermission() { + + } + public SysPermission(boolean index) { + if(index) { + this.id = "9502685863ab87f0ad1134142788a385"; + this.name = DefIndexConst.DEF_INDEX_NAME; + this.component = DefIndexConst.DEF_INDEX_COMPONENT; + this.componentName = "dashboard-analysis"; + this.url = DefIndexConst.DEF_INDEX_URL; + this.icon="home"; + this.menuType=0; + this.sortNo=0.0; + this.ruleFlag=0; + this.delFlag=0; + this.alwaysShow=false; + this.route=true; + this.keepAlive=true; + this.leaf=true; + this.hidden=false; + } + + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPermissionDataRule.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPermissionDataRule.java new file mode 100644 index 0000000..f6f549b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPermissionDataRule.java @@ -0,0 +1,83 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 菜单权限规则表 + *

+ * + * @Author huangzhilin + * @since 2019-03-29 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysPermissionDataRule implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 对应的菜单id + */ + private String permissionId; + + /** + * 规则名称 + */ + private String ruleName; + + /** + * 字段 + */ + private String ruleColumn; + + /** + * 条件 + */ + private String ruleConditions; + + /** + * 规则值 + */ + private String ruleValue; + + /** + * 状态值 1有效 0无效 + */ + private String status; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 创建人 + */ + private String createBy; + + /** + * 修改时间 + */ + private Date updateTime; + + /** + * 修改人 + */ + private String updateBy; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPosition.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPosition.java new file mode 100644 index 0000000..cbf8f70 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysPosition.java @@ -0,0 +1,91 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * @Description: 职务级别 + * @Author: Ghb-boot + * @Date: 2019-09-19 + * @Version: V1.0 + */ +@Data +@TableName("sys_position") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="职务级别表") +public class SysPosition { + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private java.lang.String id; + /** + * 职务编码 + */ + @Excel(name = "职务编码", width = 15) + @Schema(description = "职务编码") + private java.lang.String code; + /** + * 职务级别名称 + */ + @Excel(name = "职务级别名称", width = 15) + @Schema(description = "职务级别名称") + private java.lang.String name; + /** + * 职级 + */ + //@Excel(name = "职级", width = 15,dicCode ="position_rank") + @Schema(description = "职务等级") + private java.lang.Integer postLevel; + /** + * 公司id + */ + @Schema(description = "公司id") + private java.lang.String companyId; + /** + * 创建人 + */ + @Schema(description = "创建人") + private java.lang.String createBy; + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private java.util.Date createTime; + /** + * 修改人 + */ + @Schema(description = "修改人") + private java.lang.String updateBy; + /** + * 修改时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "修改时间") + private java.util.Date updateTime; + /** + * 组织机构编码 + */ + @Schema(description = "组织机构编码") + private java.lang.String sysOrgCode; + + /**租户ID*/ + @Schema(description = "租户ID") + private java.lang.Integer tenantId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRole.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRole.java new file mode 100644 index 0000000..259f7ef --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRole.java @@ -0,0 +1,83 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Date; + +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 角色表 + *

+ * + * @Author scott + * @since 2018-12-19 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysRole implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 角色名称 + */ + @Excel(name="角色名",width=15) + private String roleName; + + /** + * 角色编码 + */ + @Excel(name="角色编码",width=15) + private String roleCode; + + /** + * 描述 + */ + @Excel(name="描述",width=60) + private String description; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /**租户ID*/ + private java.lang.Integer tenantId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRoleIndex.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRoleIndex.java new file mode 100644 index 0000000..ea286e1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRoleIndex.java @@ -0,0 +1,101 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableField; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ghb.base.common.aspect.annotation.Dict; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * @Description: 角色首页配置 + * @Author: liusq + * @Date: 2022-03-25 + * @Version: V1.0 + */ +@Data +@TableName("sys_role_index") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="角色首页配置") +public class SysRoleIndex { + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private java.lang.String id; + /**角色编码*/ + @Excel(name = "角色编码", width = 15) + @Schema(description = "角色编码") + private java.lang.String roleCode; + /**路由地址*/ + @Excel(name = "路由地址", width = 15) + @Schema(description = "路由地址") + private java.lang.String url; + /**路由地址*/ + @Excel(name = "路由地址", width = 15) + @Schema(description = "组件") + private java.lang.String component; + /** + * 是否路由菜单: 0:不是 1:是(默认值1) + */ + @Excel(name = "是否路由菜单", width = 15) + @Schema(description = "是否路由菜单") + @TableField(value="is_route") + private Boolean route; + /**优先级*/ + @Excel(name = "优先级", width = 15) + @Schema(description = "优先级") + private java.lang.Integer priority; + /**路由地址*/ + @Excel(name = "状态", width = 15) + @Schema(description = "状态") + private java.lang.String status; + /**创建人登录名称*/ + @Excel(name = "创建人登录名称", width = 15) + @Schema(description = "创建人登录名称") + private java.lang.String createBy; + /**创建日期*/ + @Excel(name = "创建日期", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private java.util.Date createTime; + /**更新人登录名称*/ + @Excel(name = "更新人登录名称", width = 15) + @Schema(description = "更新人登录名称") + private java.lang.String updateBy; + /**更新日期*/ + @Excel(name = "更新日期", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新日期") + private java.util.Date updateTime; + /**所属部门*/ + @Excel(name = "所属部门", width = 15) + @Schema(description = "所属部门") + private java.lang.String sysOrgCode; + + /**关联类型(ROLE:角色 USER:表示用户)*/ + @Schema(description = "关联类型") + @Excel(name = "关联类型", width = 15, dicCode = "relation_type") + @Dict(dicCode = "relation_type") + private java.lang.String relationType; + + + public SysRoleIndex() { + + } + public SysRoleIndex(String componentUrl){ + this.component = componentUrl; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRolePermission.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRolePermission.java new file mode 100644 index 0000000..ac973ad --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysRolePermission.java @@ -0,0 +1,71 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; + +/** + *

+ * 角色权限表 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysRolePermission implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 角色id + */ + private String roleId; + + /** + * 权限id + */ + private String permissionId; + + /** + * 数据权限 + */ + private String dataRuleIds; + + /** + * 操作时间 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date operateDate; + + /** + * 操作ip + */ + private String operateIp; + + public SysRolePermission() { + } + + public SysRolePermission(String roleId, String permissionId) { + this.roleId = roleId; + this.permissionId = permissionId; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTableWhiteList.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTableWhiteList.java new file mode 100644 index 0000000..422e176 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTableWhiteList.java @@ -0,0 +1,80 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * @Description: 系统表白名单 + * @Author: Ghb-boot + * @Date: 2023-09-12 + * @Version: V1.0 + */ +@Data +@TableName("sys_table_white_list") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="系统表白名单") +public class SysTableWhiteList { + + /** + * 主键id + */ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private java.lang.String id; + /** + * 允许的表名 + */ + @Excel(name = "允许的表名", width = 15) + @Schema(description = "允许的表名") + private java.lang.String tableName; + /** + * 允许的字段名,多个用逗号分割 + */ + @Excel(name = "允许的字段名", width = 15) + @Schema(description = "允许的字段名") + private java.lang.String fieldName; + /** + * 状态,1=启用,0=禁用 + */ + @Excel(name = "状态", width = 15) + @Schema(description = "状态") + private java.lang.String status; + /** + * 创建人 + */ + @Excel(name = "创建人", width = 15) + @Schema(description = "创建人") + private java.lang.String createBy; + /** + * 创建时间 + */ + @Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private java.util.Date createTime; + /** + * 更新人 + */ + @Excel(name = "更新人", width = 15) + @Schema(description = "更新人") + private java.lang.String updateBy; + /** + * 更新时间 + */ + @Excel(name = "更新时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private java.util.Date updateTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenant.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenant.java new file mode 100644 index 0000000..4ac5213 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenant.java @@ -0,0 +1,137 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.springframework.format.annotation.DateTimeFormat; +import java.io.Serializable; +import java.util.Date; + +/** + * 租户信息 + * @author: Ghb-boot + */ +@Data +@TableName("sys_tenant") +public class SysTenant implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 编码 + */ + private Integer id; + + /** + * 名称 + */ + private String name; + + + /** + * 创建人 + */ + @Dict(dictTable ="sys_user",dicText = "realname",dicCode = "username") + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 开始时间 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date beginDate; + + /** + * 结束时间 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date endDate; + + /** + * 状态 1正常 0冻结 + */ + @Dict(dicCode = "tenant_status") + private Integer status; + + /** + * 所属行业 + */ + @Dict(dicCode = "trade") + private String trade; + + /** + * 公司规模 + */ + @Dict(dicCode = "company_size") + private String companySize; + + /** + * 公司地址 + */ + private String companyAddress; + + /** + * 公司logo + */ + private String companyLogo; + + /** + * 门牌号 + */ + private String houseNumber; + + /** + * 工作地点 + */ + private String workPlace; + + /** + * 二级域名(暂时无用,预留字段) + */ + private String secondaryDomain; + + /** + * 登录背景图片(暂时无用,预留字段) + */ + private String loginBkgdImg; + + /** + * 职级 + */ + @Dict(dicCode = "company_rank") + private String position; + + /** + * 部门 + */ + @Dict(dicCode = "company_department") + private String department; + + @TableLogic + private Integer delFlag; + + /**更新人登录名称*/ + private String updateBy; + + /**更新日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 允许申请管理员 1允许 0不允许 + */ + private Integer applyStatus; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenantPack.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenantPack.java new file mode 100644 index 0000000..c58e5b3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenantPack.java @@ -0,0 +1,98 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @Description: 租户产品包 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +@Data +@TableName("sys_tenant_pack") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="租户产品包") +public class SysTenantPack implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private String id; + /**租户id*/ + @Excel(name = "租户id", width = 15) + @Schema(description = "租户id") + private Integer tenantId; + /**产品包名*/ + @Excel(name = "产品包名", width = 15) + @Schema(description = "产品包名") + private String packName; + /**开启状态(0 未开启 1开启)*/ + @Excel(name = "开启状态(0 未开启 1开启)", width = 15) + @Schema(description = "开启状态(0 未开启 1开启)") + private String status; + /**备注*/ + @Excel(name = "备注", width = 15) + @Schema(description = "备注") + private String remarks; + /**创建人*/ + @Schema(description = "创建人") + private String createBy; + /**创建时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "创建时间") + private Date createTime; + /**更新人*/ + @Schema(description = "更新人") + private String updateBy; + /**更新时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "更新时间") + private Date updateTime; + /**产品包类型(default 默认产品包 custom 自定义产品包)*/ + @Excel(name = "产品包类型", width = 15) + @Schema(description = "产品包类型") + private String packType; + + /** + * 是否自动分配给用户(0 否 1是) + */ + @Excel(name = "是否自动分配给用户(0 否 1是)", width = 15) + @Schema(description = "是否自动分配给用户") + private String izSysn; + + /**菜单id 临时字段用于新增编辑菜单id传递*/ + @TableField(exist = false) + private String permissionIds; + + + /** + * 编码 + */ + private String packCode; + + public SysTenantPack(){ + + } + + public SysTenantPack(Integer tenantId, String packName, String packCode){ + this.tenantId = tenantId; + this.packCode = packCode; + this.packName = packName; + this.status = "1"; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenantPackUser.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenantPackUser.java new file mode 100644 index 0000000..ac21e7b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysTenantPackUser.java @@ -0,0 +1,93 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; + +/** + * @Description: 租户产品包用户关系表 + * @Author: Ghb-boot + * @Date: 2023-02-16 + * @Version: V1.0 + */ +@Data +@TableName("sys_tenant_pack_user") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="租户产品包用户关系表") +public class SysTenantPackUser implements Serializable { + private static final long serialVersionUID = 1L; + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "id") + private java.lang.String id; + /**租户产品包ID*/ + @Excel(name = "租户产品包ID", width = 15) + @Schema(description = "租户产品包ID") + private java.lang.String packId; + /**用户ID*/ + @Excel(name = "用户ID", width = 15) + @Schema(description = "用户ID") + private java.lang.String userId; + /**租户ID*/ + @Excel(name = "租户ID", width = 15) + @Schema(description = "租户ID") + private java.lang.Integer tenantId; + /**创建人*/ + @Schema(description = "创建人") + private java.lang.String createBy; + /**创建时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "创建时间") + private java.util.Date createTime; + /**更新人*/ + @Schema(description = "更新人") + private java.lang.String updateBy; + /**更新时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "更新时间") + private java.util.Date updateTime; + + private transient String realname; + + private transient String packName; + + private transient String packCode; + + /** + * 状态(申请状态0 正常状态1) + */ + private Integer status; + + public SysTenantPackUser(){ + + } + public SysTenantPackUser(Integer tenantId, String packId, String userId) { + this.packId = packId; + this.userId = userId; + this.tenantId = tenantId; + this.status = 1; + } + + public SysTenantPackUser(SysTenantPackUser param, String userId, String realname) { + this.userId = userId; + this.realname = realname; + this.packId = param.getPackId(); + this.tenantId = param.getTenantId(); + this.packName = param.getPackName(); + this.status = 1; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysThirdAccount.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysThirdAccount.java new file mode 100644 index 0000000..025986f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysThirdAccount.java @@ -0,0 +1,83 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * @Description: 第三方登录账号表 + * @Author: Ghb-boot + * @Date: 2020-11-17 + * @Version: V1.0 + */ +@Data +@TableName("sys_third_account") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="第三方登录账号表") +public class SysThirdAccount { + + /**编号*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "编号") + private java.lang.String id; + /**第三方登录id*/ + @Excel(name = "第三方登录id", width = 15) + @Schema(description = "第三方登录id") + private java.lang.String sysUserId; + /**登录来源*/ + @Excel(name = "登录来源", width = 15) + @Schema(description = "登录来源") + private java.lang.String thirdType; + /**头像*/ + @Excel(name = "头像", width = 15) + @Schema(description = "头像") + private java.lang.String avatar; + /**状态(1-正常,2-冻结)*/ + @Excel(name = "状态(1-正常,2-冻结)", width = 15) + @Schema(description = "状态(1-正常,2-冻结)") + private java.lang.Integer status; + /**删除状态(0-正常,1-已删除)*/ + @Excel(name = "删除状态(0-正常,1-已删除)", width = 15) + @Schema(description = "删除状态(0-正常,1-已删除)") + private java.lang.Integer delFlag; + /**真实姓名*/ + @Excel(name = "真实姓名", width = 15) + @Schema(description = "真实姓名") + private java.lang.String realname; + /**第三方用户uuid*/ + @Excel(name = "第三方用户uuid", width = 15) + @Schema(description = "第三方用户uuid") + private java.lang.String thirdUserUuid; + /**第三方用户账号*/ + @Excel(name = "第三方用户账号", width = 15) + @Schema(description = "第三方用户账号") + private java.lang.String thirdUserId; + /**创建人*/ + @Excel(name = "创建人", width = 15) + private java.lang.String createBy; + /**创建日期*/ + @Excel(name = "创建日期", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date createTime; + /**修改人*/ + @Excel(name = "修改人", width = 15) + private java.lang.String updateBy; + /**修改日期*/ + @Excel(name = "修改日期", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date updateTime; + + /**租户id*/ + private java.lang.Integer tenantId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysThirdAppConfig.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysThirdAppConfig.java new file mode 100644 index 0000000..2ea57d9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysThirdAppConfig.java @@ -0,0 +1,80 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; +import java.util.Date; + +/** + * @Description: 第三方配置表 + * @Author: Ghb-boot + * @Date: 2023-02-03 + * @Version: V1.0 + */ +@Data +@TableName("sys_third_app_config") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="第三方配置表") +public class SysThirdAppConfig { + + /**编号*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "编号") + private String id; + + /**租户id*/ + @Excel(name = "租户id", width = 15) + @Schema(description = "租户id") + private Integer tenantId; + + /**钉钉/企业微信第三方企业应用标识*/ + @Excel(name = "钉钉/企业微信第三方企业应用标识", width = 15) + @Schema(description = "钉钉/企业微信第三方企业应用标识") + private String agentId; + + /**钉钉/企业微信 应用id*/ + @Excel(name = "钉钉/企业微信 应用id", width = 15) + @Schema(description = "钉钉/企业微信 应用id") + private String clientId; + + /**钉钉/企业微信应用id对应的秘钥*/ + @Excel(name = "钉钉/企业微信应用id对应的秘钥", width = 15) + @Schema(description = "钉钉/企业微信应用id对应的秘钥") + private String clientSecret; + + /**钉钉企业id*/ + @Excel(name = "钉钉企业id", width = 15) + @Schema(description = "钉钉企业id") + private String corpId; + + /**第三方类别(dingtalk 钉钉 wechat_enterprise 企业微信)*/ + @Excel(name = "第三方类别(dingtalk 钉钉 wechat_enterprise 企业微信)", width = 15) + @Schema(description = "第三方类别(dingtalk 钉钉 wechat_enterprise 企业微信)") + private String thirdType; + + /**是否启用(0-否,1-是)*/ + @Excel(name = "是否启用(0-否,1-是)", width = 15) + @Schema(description = "是否启用(0-否,1-是)") + private Integer status; + + /**创建日期*/ + @Excel(name = "创建日期", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /**修改日期*/ + @Excel(name = "修改日期", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUgroup.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUgroup.java new file mode 100644 index 0000000..d4912d2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUgroup.java @@ -0,0 +1,62 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; + +/** + * @Description: 用户组表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +@Data +@TableName("sys_ugroup") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="用户组表") +public class SysUgroup implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private java.lang.String id; + /**角色名称*/ + @Excel(name = "用户组名称", width = 15) + @Schema(description = "用户组名称") + private java.lang.String groupName; + /**描述*/ + @Excel(name = "描述", width = 15) + @Schema(description = "描述") + private java.lang.String description; + /**创建人*/ + @Schema(description = "创建人") + private java.lang.String createBy; + /**创建时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private java.util.Date createTime; + /**更新人*/ + @Schema(description = "更新人") + private java.lang.String updateBy; + /**更新时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private java.util.Date updateTime; + /**租户ID*/ + @Excel(name = "租户ID", width = 15) + @Schema(description = "租户ID") + private java.lang.Integer tenantId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUgroupUser.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUgroupUser.java new file mode 100644 index 0000000..5bc644f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUgroupUser.java @@ -0,0 +1,52 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecgframework.poi.excel.annotation.Excel; + +import java.io.Serializable; + +/** + * @Description: 用户组关系表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +@Data +@TableName("sys_ugroup_user") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="用户组关系表") +public class SysUgroupUser implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private java.lang.String id; + /**用户id*/ + @Excel(name = "用户id", width = 15) + @Schema(description = "用户id") + private java.lang.String userId; + /**用户组id*/ + @Excel(name = "用户组id", width = 15) + @Schema(description = "用户组id") + private java.lang.String groupId; + /**租户ID*/ + @Excel(name = "租户ID", width = 15) + @Schema(description = "租户ID") + private java.lang.Integer tenantId; + + public SysUgroupUser() { + } + + public SysUgroupUser(String userId, String groupId) { + this.userId = userId; + this.groupId = groupId; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUser.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUser.java new file mode 100644 index 0000000..3c4380d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUser.java @@ -0,0 +1,275 @@ +package com.ghb.base.modules.system.entity; + +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serializable; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 用户表 + *

+ * + * @Author scott + * @since 2018-12-20 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysUser implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 登录账号 + */ + @Excel(name = "登录账号", width = 15) + private String username; + + /** + * 真实姓名 + */ + @Excel(name = "真实姓名", width = 15) + private String realname; + + /** + * 密码 + */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + + /** + * md5密码盐 + */ + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String salt; + + /** + * 头像 + */ + @Excel(name = "头像", width = 15,type = 2) + private String avatar; + + /** + * 生日 + */ + @Excel(name = "生日", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date birthday; + + /** + * 性别(1:男 2:女) + */ + @Excel(name = "性别", width = 15,dicCode="sex") + @Dict(dicCode = "sex") + private Integer sex; + + /** + * 电子邮件 + */ + @Excel(name = "电子邮件", width = 15) + private String email; + + /** + * 电话 + */ + @Excel(name = "电话", width = 15) + private String phone; + + /** + * 登录选择部门编码 + */ + private String orgCode; + /** + * 登录选择租户ID + */ + private Integer loginTenantId; + + /**部门名称*/ + private transient String orgCodeTxt; + + /** + * 状态(1:正常 2:冻结 ) + */ + @Excel(name = "状态", width = 15,dicCode="user_status") + @Dict(dicCode = "user_status") + private Integer status; + + /** + * 删除状态(0,正常,1已删除) + */ + @Excel(name = "删除状态", width = 15,dicCode="del_flag") + @TableLogic + private Integer delFlag; + + /** + * 工号,唯一键 + */ + @Excel(name = "工号", width = 15) + private String workNo; + + /** + * 职务,关联职务表 + */ + @Excel(name = "职务", width = 15) + @Dict(dictTable ="sys_position",dicText = "name",dicCode = "id") + @TableField(exist = false) + private String post; + + /** + * 座机号 + */ + @Excel(name = "座机号", width = 15) + private String telephone; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + /** + * 同步工作流引擎1同步0不同步 + */ + private Integer activitiSync; + + /** + * 身份(0 普通成员 1 上级) + */ + @Excel(name="(1普通成员 2上级)",width = 15) + private Integer userIdentity; + + /** + * 负责部门 + */ + @Excel(name="负责部门",width = 15,dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + @Dict(dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + private String departIds; + + /** + * 多租户ids临时用,不持久化数据库(数据库字段不存在) + */ + @TableField(exist = false) + private String relTenantIds; + + /**设备id uniapp推送用*/ + private String clientId; + + /** + * 登录首页地址 + */ + @TableField(exist = false) + private String homePath; + + /** + * 职位名称 + */ + @TableField(exist = false) + private String postText; + + /** + * 流程状态 + */ + private String bpmStatus; + + /** + * 是否已经绑定第三方 + */ + @TableField(exist = false) + private boolean izBindThird; + + /** + * 个性签名 + */ + private String sign; + + /** + * 是否开启个性签名 + */ + private Integer signEnable; + + /** + * 主岗位 + */ + @Excel(name="主岗位",width = 15,dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + @Dict(dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + private String mainDepPostId; + + /** + * 兼职岗位 + */ + @Excel(name="兼职岗位",width = 15,dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + @Dict(dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + @TableField(exist = false) + private String otherDepPostId; + + /** + * 职务(字典) + */ + @Excel(name = "职务", width = 15, dicCode = "user_position") + @Dict(dicCode = "user_position") + private String positionType; + + /** + * 上一次修改密码的时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date lastPwdUpdateTime; + + /** + * 登录时,选择的部门,临时用,不持久化数据库(数据库字段不存在) + */ + @TableField(exist = false) + private String loginOrgCode; + + /** + * 排序 + */ + private Integer sort; + + /** + * 是否隐藏联系方式 0否1是 + */ + private String izHideContact; + + /** + * 所属部门的id + */ + @TableField(exist = false) + private String belongDepIds; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserDepPost.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserDepPost.java new file mode 100644 index 0000000..2175f3c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserDepPost.java @@ -0,0 +1,85 @@ +package com.ghb.base.modules.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * @Description: 部门岗位用户 + * @author: wangshuai + * @date: 2025/9/5 11:45 + */ +@Data +@TableName("sys_user_dep_post") +public class SysUserDepPost implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 主键id + */ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private String id; + /** + * 用户id + */ + @Schema(description = "用户id") + private String userId; + /** + * 部门岗位id + */ + @Schema(description = "部门岗位id") + private String depId; + + /** + * 创建人 + */ + @Schema(description = "创建人") + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建时间") + private Date createTime; + /** + * 更新人 + */ + @Schema(description = "更新人") + private String updateBy; + /** + * 更新时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "更新时间") + private Date updateTime; + /** + * 机构编码 + */ + @Excel(name = "机构编码", width = 15) + @Schema(description = "机构编码") + private String orgCode; + + public SysUserDepPost(String id, String userId, String depId) { + super(); + this.id = id; + this.userId = userId; + this.depId = depId; + } + + public SysUserDepPost(String userId, String departId) { + this.userId = userId; + this.depId = departId; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserDepart.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserDepart.java new file mode 100644 index 0000000..64426dd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserDepart.java @@ -0,0 +1,38 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +/** + * @Description: 用户部门 + * @author: Ghb-boot + */ +@Data +@TableName("sys_user_depart") +public class SysUserDepart implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键id*/ + @TableId(type = IdType.ASSIGN_ID) + private String id; + /**用户id*/ + private String userId; + /**部门id*/ + private String depId; + public SysUserDepart(String id, String userId, String depId) { + super(); + this.id = id; + this.userId = userId; + this.depId = depId; + } + + public SysUserDepart(String userId, String departId) { + this.userId = userId; + this.depId = departId; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserPosition.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserPosition.java new file mode 100644 index 0000000..aa81fc7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserPosition.java @@ -0,0 +1,54 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import java.util.Date; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: 用户职位关系表 + * @Author: Ghb-boot + * @Date: 2023-02-14 + * @Version: V1.0 + */ +@Schema(description="用户职位关系表") +@Data +@TableName("sys_user_position") +public class SysUserPosition implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private String id; + /**用户id*/ + @Excel(name = "用户id", width = 15) + @Schema(description = "用户id") + private String userId; + /**职位id*/ + @Schema(description = "职位id") + private String positionId; + /**创建人*/ + @Schema(description = "创建人") + private String createBy; + /**创建时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "创建时间") + private Date createTime; + /**修改人*/ + @Schema(description = "修改人") + private String updateBy; + /**修改时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "修改时间") + private Date updateTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserRole.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserRole.java new file mode 100644 index 0000000..8e39986 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserRole.java @@ -0,0 +1,51 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 用户角色表 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysUserRole implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 用户id + */ + private String userId; + + /** + * 角色id + */ + private String roleId; + + /**租户ID*/ + private java.lang.Integer tenantId; + + public SysUserRole() { + } + + public SysUserRole(String userId, String roleId) { + this.userId = userId; + this.roleId = roleId; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserTenant.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserTenant.java new file mode 100644 index 0000000..ec0edaa --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/entity/SysUserTenant.java @@ -0,0 +1,63 @@ +package com.ghb.base.modules.system.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @Description: sys_user_tenant_relation + * @Author: Ghb-boot + * @Date: 2022-12-23 + * @Version: V1.0 + */ +@Data +@TableName("sys_user_tenant") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description="sys_user_tenant") +public class SysUserTenant implements Serializable { + private static final long serialVersionUID = 1L; + + /**主键id*/ + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键id") + private String id; + /**用户id*/ + @Excel(name = "用户id", width = 15) + @Schema(description = "用户id") + private String userId; + /**租户id*/ + @Excel(name = "租户id", width = 15) + @Schema(description = "租户id") + private Integer tenantId; + /**状态(1 正常 2 冻结 3 待审核 4 拒绝)*/ + @Excel(name = "状态(1 正常 2 冻结 3 待审核 4 拒绝)", width = 15) + @Schema(description = "状态(1 正常 2 冻结 3 待审核 4 拒绝)") + private String status; + /**创建人登录名称*/ + @Schema(description = "创建人登录名称") + private String createBy; + /**创建日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "创建日期") + private Date createTime; + /**更新人登录名称*/ + @Schema(description = "更新人登录名称") + private String updateBy; + /**更新日期*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + @Schema(description = "更新日期") + private Date updateTime; +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/excelstyle/ExcelExportSysUserStyle.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/excelstyle/ExcelExportSysUserStyle.java new file mode 100644 index 0000000..244cfa5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/excelstyle/ExcelExportSysUserStyle.java @@ -0,0 +1,34 @@ +package com.ghb.base.modules.system.excelstyle; + +import org.apache.poi.ss.usermodel.*; +import org.jeecgframework.poi.excel.export.styler.ExcelExportStylerDefaultImpl; + +/** + * @Description: 导入用户获取标题头部样式 覆盖默认样式 + * + * @author: wangshuai + * @date: 2025/8/28 14:05 + */ +public class ExcelExportSysUserStyle extends ExcelExportStylerDefaultImpl { + + public ExcelExportSysUserStyle(Workbook workbook) { + super(workbook); + } + + /** + * 获取标题样式 + * + * @param color + * @return + */ + public CellStyle getHeaderStyle(short color) { + CellStyle titleStyle = this.workbook.createCellStyle(); + Font font = this.workbook.createFont(); + font.setFontHeightInPoints((short)12); + titleStyle.setFont(font); + titleStyle.setAlignment(HorizontalAlignment.LEFT); + titleStyle.setVerticalAlignment(VerticalAlignment.CENTER); + titleStyle.setWrapText(true); + return titleStyle; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/job/UserUpadtePwdJob.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/job/UserUpadtePwdJob.java new file mode 100644 index 0000000..1f5d3fc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/job/UserUpadtePwdJob.java @@ -0,0 +1,68 @@ +package com.ghb.base.modules.system.job; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.common.constant.enums.NoticeTypeEnum; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.service.ISysUserService; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.springframework.beans.factory.annotation.Autowired; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; + +/** +* @Description: 用户更新提醒job +* +* @author: wangshuai +* @date: 2025/9/13 16:20 +*/ +@Slf4j +public class UserUpadtePwdJob implements Job { + + @Autowired + private ISysBaseAPI sysBaseAPI; + + @Autowired + private ISysUserService userService; + + @Override + public void execute(JobExecutionContext context) { + //获取当前时间5个月前的时间 + // 获取当前日期 + Calendar calendar = Calendar.getInstance(); + // 减去5个月 + calendar.add(Calendar.MONTH, -5); + // 格式化输出 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String formattedDate = sdf.format(calendar.getTime()); + String startTime = formattedDate + " 00:00:00"; + String endTime = formattedDate + " 23:59:59"; + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.between(SysUser::getLastPwdUpdateTime, startTime, endTime); + queryWrapper.select(SysUser::getUsername,SysUser::getRealname); + List list = userService.list(queryWrapper); + if (CollectionUtil.isNotEmpty(list)){ + for (SysUser sysUser : list) { + this.sendSysMessage(sysUser.getUsername(), sysUser.getRealname()); + } + } + } + + + /** + * 发送系统消息 + */ + private void sendSysMessage(String username, String realname) { + String fromUser = "system"; + String title = "尊敬的"+realname+"您的密码已经5个月未修改了,请修改密码"; + MessageDTO messageDTO = new MessageDTO(fromUser, username, title, title); + messageDTO.setNoticeType(NoticeTypeEnum.NOTICE_TYPE_PLAN.getValue()); + sysBaseAPI.sendSysAnnouncement(messageDTO); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysAnnouncementMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysAnnouncementMapper.java new file mode 100644 index 0000000..a00daaf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysAnnouncementMapper.java @@ -0,0 +1,60 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.Date; +import java.util.List; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysAnnouncement; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +/** + * @Description: 系统通告表 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version: V1.0 + */ +public interface SysAnnouncementMapper extends BaseMapper { + + /** + * 通过消息类型和用户id获取系统通告 + * @param page + * @param userId 用户id + * @param msgCategory 消息类型 + * @return + */ + List querySysCementListByUserId(Page page, @Param("userId")String userId,@Param("msgCategory")String msgCategory, + @Param("tenantId")Integer tenantId, @Param("beginDate")Date beginDate); + + /** + * 获取用户未读消息数量 + * + * @param userId 用户id + * @param noticeType + * @return + */ + Integer getUnreadMessageCountByUserId(@Param("userId") String userId, @Param("beginDate") Date beginDate, @Param("noticeType") String noticeType); + + /** + * 分页查询全部消息列表 + * @param page + * @param userId + * @param fromUser + * @param beginDate + * @param endDate + * @param noticeType + * @return + */ + List queryAllMessageList(Page page, @Param("userId")String userId, @Param("fromUser")String fromUser, @Param("starFlag")String starFlag, @Param("busType")String busType, @Param("msgCategory")String msgCategory, @Param("beginDate")Date beginDate, @Param("endDate")Date endDate, @Param("noticeType") String noticeType); + + /** + * 查询用户未阅读的通知公告 + * @param currDate + * @param userId + * @return + */ + List getNotSendedAnnouncementlist(@Param("currDate") Date currDate, @Param("userId")String userId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysAnnouncementSendMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysAnnouncementSendMapper.java new file mode 100644 index 0000000..93bec10 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysAnnouncementSendMapper.java @@ -0,0 +1,74 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysAnnouncementSend; +import com.ghb.base.modules.system.model.AnnouncementSendModel; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +/** + * @Description: 用户通告阅读标记表 + * @Author: Ghb-boot + * @Date: 2019-02-21 + * @Version: V1.0 + */ +public interface SysAnnouncementSendMapper extends BaseMapper { + + /** + * 获取我的消息 + * @param announcementSendModel + * @param page + * @return + */ + public List getMyAnnouncementSendList(Page page,@Param("announcementSendModel") AnnouncementSendModel announcementSendModel); + + /** + * 获取一条记录 + * @param sendId + * @return + */ + AnnouncementSendModel getOne(@Param("sendId") String sendId); + + + /** + * 修改为已读消息 + */ + void updateReaded(@Param("userId") String userId, @Param("annoceIdList") List annoceIdList); + + /** + * 清除所有未读消息 + * @param userId + */ + void clearAllUnReadMessage(@Param("userId") String userId); + + /** + * 根据用户id和通告阅读表的id获取当前用户已阅读的数量 + * + * @param id + * @param userId + */ + @Select("select count(1) from sys_announcement_send where id=#{id} and user_id = #{userId} and read_flag = 1") + long getReadCountByUserId(@Param("id") String id, @Param("userId") String userId); + + /** + * 根据用户id和阅读表的id获取所有阅读的数据 + * + * @param ids + * @param userId + * @return + */ + List getReadAnnSendByUserId(@Param("ids") List ids, @Param("userId") String userId); + + /** + * 根据业务id、业务类型和用户id获取未读消息 + * @param busId + * @param busType + * @param userId + * @return + */ + List getUnReadAnnByBusAndUserId(@Param("busId")String busId, @Param("busType")String busType, @Param("userId")String userId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCategoryMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCategoryMapper.java new file mode 100644 index 0000000..6510efc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCategoryMapper.java @@ -0,0 +1,51 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; +import java.util.Map; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysCategory; +import com.ghb.base.modules.system.model.TreeSelectModel; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 分类字典 + * @Author: Ghb-boot + * @Date: 2019-05-29 + * @Version: V1.0 + */ +public interface SysCategoryMapper extends BaseMapper { + + /** + * 根据父级ID查询树节点数据 + * @param pid + * @param query + * @return + */ + public List queryListByPid(@Param("pid") String pid,@Param("query") Map query); + + /** + * 通过code查询分类字典表 + * @param code + * @return + */ + @Select("SELECT ID FROM sys_category WHERE CODE = #{code,jdbcType=VARCHAR}") + public String queryIdByCode(@Param("code") String code); + + /** + * 获取分类字典最大的code + * @param page + * @return + */ + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT code FROM sys_category WHERE code IS NOT NULL AND pid=#{categoryPid} ORDER BY code DESC") + List getMaxCategoryCodeByPage(@Param("page") Page page,@Param("categoryPid") String categoryPid); + + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT code FROM sys_category WHERE ID = #{id}") + SysCategory selectSysCategoryById(@Param("id") String id); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCheckRuleMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCheckRuleMapper.java new file mode 100644 index 0000000..d430eb1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCheckRuleMapper.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysCheckRule; + +/** + * @Description: 编码校验规则 + * @Author: Ghb-boot + * @Date: 2020-02-04 + * @Version: V1.0 + */ +public interface SysCheckRuleMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCommentMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCommentMapper.java new file mode 100644 index 0000000..943d63c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysCommentMapper.java @@ -0,0 +1,40 @@ +package com.ghb.base.modules.system.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysComment; +import com.ghb.base.modules.system.vo.SysCommentFileVo; +import com.ghb.base.modules.system.vo.SysCommentVO; +import com.ghb.base.modules.system.vo.UserAvatar; + +import java.util.List; +import java.util.Set; + +/** + * @Description: 系统评论回复表 + * @Author: Ghb-boot + * @Date: 2022-07-19 + * @Version: V1.0 + */ +public interface SysCommentMapper extends BaseMapper { + + List queryCommentList(@Param("tableName") String tableName, @Param("formDataId") String formDataId); + + /** + * 根据表名和数据id查询表单文件 + * + * @param tableName + * @param formDataId + * @return + */ + List queryFormFileList(@Param("tableName") String tableName, @Param("formDataId") String formDataId); + + /** + * 根据用户名获取用户信息 + * @param idSet + * @return + */ + List queryUserAvatarList(@Param("idSet") Set idSet); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDataLogMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDataLogMapper.java new file mode 100644 index 0000000..b6afbbe --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDataLogMapper.java @@ -0,0 +1,21 @@ +package com.ghb.base.modules.system.mapper; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysDataLog; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 系统数据日志Mapper接口 + * @author: Ghb-boot + */ +public interface SysDataLogMapper extends BaseMapper{ + /** + * 通过表名及数据Id获取最大版本 + * @param tableName + * @param dataId + * @return + */ + public String queryMaxDataVer(@Param("tableName") String tableName,@Param("dataId") String dataId); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDataSourceMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDataSourceMapper.java new file mode 100644 index 0000000..d0dea52 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDataSourceMapper.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysDataSource; + +/** + * @Description: 多数据源管理 + * @Author: Ghb-boot + * @Date: 2019-12-25 + * @Version: V1.0 + */ +public interface SysDataSourceMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartMapper.java new file mode 100644 index 0000000..799c405 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartMapper.java @@ -0,0 +1,331 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.model.SysUserSysDepPostModel; +import com.ghb.base.modules.system.vo.SysDepartExportVo; +import com.ghb.base.modules.system.vo.SysDepartPositionVo; +import com.ghb.base.modules.system.vo.SysUserDepVo; +import com.ghb.base.modules.system.vo.lowapp.ExportDepartVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + *

+ * 部门 Mapper 接口 + *

+ * + * @Author: Steve + * @Since: 2019-01-22 + */ +public interface SysDepartMapper extends BaseMapper { + + /** + * 根据用户ID查询部门集合 + * @param userId 用户id + * @return List + */ + public List queryUserDeparts(@Param("userId") String userId); + + /** + * 根据用户名查询部门 + * + * @param username + * @return + */ + public List queryDepartsByUsername(@Param("username") String username); + + /** + * 根据 userId 查询部门 + * + * @param userId + * @return + */ + public List queryDepartsByUserId(@Param("userId") String userId); + + /** + * 根据 userIds 查询部门ID + * + * @param userIds 用户ID列表 + * @return + */ + List> queryDepartIdsByUserIds(@Param("userIds") Collection userIds); + + /** + * 通过部门编码获取部门id + * @param orgCode 部门编码 + * @return String + */ + @Select("select id from sys_depart where org_code=#{orgCode}") + public String queryDepartIdByOrgCode(@Param("orgCode") String orgCode); + + /** + * 通过部门id,查询部门下的用户的账号 + * @param departIds 部门ID集合 + * @return String + */ + public List queryUserAccountByDepartIds(@Param("departIds") List departIds); + + /** + * 通过部门id 查询部门id,父id + * @param departId 部门id + * @return + */ + @Select("select id,parent_id from sys_depart where id=#{departId}") + public SysDepart getParentDepartId(@Param("departId") String departId); + + /** + * 根据部门Id查询,当前和下级所有部门IDS + * @param departId + * @return + */ + List getSubDepIdsByDepId(@Param("departId") String departId); + + /** + * 根据部门编码获取部门下所有IDS + * @param orgCodes + * @return + */ + List getSubDepIdsByOrgCodes(@org.apache.ibatis.annotations.Param("orgCodes") String[] orgCodes); + + /** + * 根据parent_id查询下级部门 + * @param parentId 父id + * @return List + */ + List queryTreeListByPid(@Param("parentId") String parentId); + /** + * 根据id下级部门数量 + * @param parentId + * @return + */ + @Select("SELECT count(*) FROM sys_depart where del_flag ='0' AND parent_id = #{parentId,jdbcType=VARCHAR}") + Integer queryCountByPid(@Param("parentId")String parentId); + /** + * 根据OrgCod查询所属公司信息 + * @param orgCode + * @return + */ + SysDepart queryCompByOrgCode(@Param("orgCode")String orgCode); + /** + * 根据id下级部门 + * @param parentId + * @return + */ + @Select("SELECT * FROM sys_depart where del_flag ='0' AND parent_id = #{parentId,jdbcType=VARCHAR}") + List queryDeptByPid(@Param("parentId")String parentId); + + /** + * 通过父级id和租户id查询部门 + * @param parentId + * @param tenantId + * @return + */ + @InterceptorIgnore(tenantLine = "true") + List queryBookDepTreeSync(@Param("parentId") String parentId, @Param("tenantId") Integer tenantId, @Param("departName") String departName); + + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT * FROM sys_depart where id = #{id,jdbcType=VARCHAR}") + SysDepart getDepartById(@Param("id") String id); + + @InterceptorIgnore(tenantLine = "true") + List getMaxCodeDepart(@Param("page") Page page, @Param("parentId") String parentId); + + /** + * 修改部门状态字段: 是否子节点 + * @param id 部门id + * @param leaf 叶子节点 + * @return int + */ + @Update("UPDATE sys_depart SET iz_leaf=#{leaf} WHERE id = #{id}") + int setMainLeaf(@Param("id") String id, @Param("leaf") Integer leaf); + + /** + * 获取租户id和部门父id获取的部门数据 + * @param tenantId + * @param parentId + * @return + */ + List getDepartList(@Param("parentId") String parentId, @Param("tenantId") Integer tenantId); + + /** + * 根据部门名称和租户id获取部门数据 + * @param departName + * @param tenantId + * @return + */ + List getDepartByName(@Param("departName")String departName, @Param("tenantId")Integer tenantId,@Param("parentId") String parentId); + + /** + * 根据部门id获取用户id和部门名称 + * @param userList + * @return + */ + List getUserDepartByTenantUserId(@Param("userList") List userList, @Param("tenantId") Integer tenantId); + + /** + * 根据部门名称和租户id获取分页部门数据 + * @param page + * @param departName + * @param tenantId + * @param parentId + * @return + */ + List getDepartPageByName(@Param("page") Page page, @Param("departName") String departName, @Param("tenantId") Integer tenantId, @Param("parentId") String parentId); + + /** + * 获取租户id和部门父id获取的部门数据 + * @param tenantId + * @param parentId + * @return + */ + List getSysDepartList(@Param("parentId") String parentId,@Param("tenantId") Integer tenantId, List idList); + + /** + * 根据多个部门id获取部门数据 + * + * @param departIds + * @return + */ + List getDepartByIds(List departIds); + + /** + * 根据用户id获取部门数据 + * + * @param userList + * @return + */ + @InterceptorIgnore(tenantLine = "true") + List getUserDepartByUserId(@Param("userList")List userList); + + /** + * 根据父级id/职级/部门id获取部门岗位信息 + * + * @param parentId + * @param postLevel + * @param departId + */ + List getDepartPositionByParentId(@Param("parentId") String parentId, @Param("postLevel") Integer postLevel, @Param("departId") String departId); + + /** + * 根据父级id获取部门中的数据 + * @param parentId + * @return + */ + @Select("select id, depart_name, parent_id, iz_leaf, org_category, org_code, depart_order from sys_depart where parent_id = #{parentId} order by depart_order,create_time desc") + List getDepartByParentId(@Param("parentId") String parentId); + + /** + * 根据部门id查询部门信息 + + * @param departId + * @return 部门岗位信息 + */ + SysDepartPositionVo getDepartPostByDepartId(@Param("departId") String departId); + + /** + * 根据父级部门id查询部门信息 + + * @param orgCode + * @return 部门岗位信息 + */ + List getDepartPostByOrgCode(@Param("orgCode") String orgCode); + + /** + * 根据部门id获取部门code + * @param idList + * @return + */ + List getDepCodeByDepIds(@Param("idList") List idList); + + /** + * 根据父级部门id和职务名称查找部门id + * + * @param parentId + * @param postName + * @return + */ + String getDepIdByDepIdAndPostName(@Param("parentId") String parentId, @Param("postName") String postName); + + /** + * 根据部门id 获取职级名称 + * + * @param depId + * @return + */ + String getPostNameByPostId(@Param("depId") String depId); + + /** + * 根据部门code获取部门数据 + * + * @param orgCode + * @return + */ + @Select("select depart_name, id, iz_leaf, org_category, parent_id, org_code from sys_depart where org_code = #{orgCode} order by depart_order,create_time desc") + SysDepart queryDepartByOrgCode(@Param("orgCode") String orgCode); + + /** + * 根据部门父id获取部门岗位数据 + * + * @param parentIds + * @return + */ + List getDepartPositionByParentIds(@Param("parentIds") List parentIds); + + /** + * 根据用户id集合获取用户的兼职岗位信息 + * + * @param userIdList + * @return + */ + List getDepartOtherPostByUserIds(@Param("userIdList") List userIdList); + + /** + * 获取没有父级id的部门数据 + * + * @return + */ + @Select("select id, org_code, depart_order from sys_depart where parent_id is null or parent_id = '' order by depart_order,create_time desc") + List getDepartNoParent(); + + /** + * 根据父级id统计子节点数量 + * + * @param parentId + * @return + */ + @Select("select count(1) from sys_depart where parent_id = #{parentId}") + long countByParentId(@Param("parentId") String parentId); + + /** + * 根据用户名和分类查询 + * @param username + * @param category + * @return + */ + List queryDeptByUserAndCategory(@Param("username")String username, @Param("category")String category); + + /** + * 获取负责部门 + * + * @param page + * @param departId + * @return + */ + List getDepartmentHead(@Param("page") Page page, @Param("departId") String departId); + + /** + *获取所有部门 + * @param departId + * @return + */ + List getAllDepartPost(@Param("departId")String departId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartPermissionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartPermissionMapper.java new file mode 100644 index 0000000..8d0b266 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartPermissionMapper.java @@ -0,0 +1,17 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysDepartPermission; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 部门权限表 + * @Author: Ghb-boot + * @Date: 2020-02-11 + * @Version: V1.0 + */ +public interface SysDepartPermissionMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRoleMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRoleMapper.java new file mode 100644 index 0000000..66e3fa1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRoleMapper.java @@ -0,0 +1,23 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysDepartRole; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 部门角色 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +public interface SysDepartRoleMapper extends BaseMapper { + /** + * 根据用户id,部门id查询可授权所有部门角色 + * @param orgCode + * @param userId + * @return + */ + public List queryDeptRoleByDeptAndUser(@Param("orgCode") String orgCode, @Param("userId") String userId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRolePermissionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRolePermissionMapper.java new file mode 100644 index 0000000..058d51b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRolePermissionMapper.java @@ -0,0 +1,18 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysDepartRolePermission; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 部门角色权限 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +public interface SysDepartRolePermissionMapper extends BaseMapper { + + void deleteByRoleIds(@Param("ids")List ids); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRoleUserMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRoleUserMapper.java new file mode 100644 index 0000000..de8d705 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDepartRoleUserMapper.java @@ -0,0 +1,18 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysDepartRoleUser; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 部门角色人员信息 + * @Author: Ghb-boot + * @Date: 2020-02-13 + * @Version: V1.0 + */ +public interface SysDepartRoleUserMapper extends BaseMapper { + + void deleteByRoleIds(@Param("ids")List ids); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDictItemMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDictItemMapper.java new file mode 100644 index 0000000..6efd99b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDictItemMapper.java @@ -0,0 +1,26 @@ +package com.ghb.base.modules.system.mapper; + +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysDictItem; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** + *

+ * Mapper 接口 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +public interface SysDictItemMapper extends BaseMapper { + + /** + * 通过字典id查询字典项 + * @param mainId 字典id + * @return + */ + @Select("SELECT * FROM sys_dict_item WHERE DICT_ID = #{mainId} order by sort_order asc, item_value asc") + public List selectItemsByMainId(String mainId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDictMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDictMapper.java new file mode 100644 index 0000000..6a6976e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysDictMapper.java @@ -0,0 +1,217 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import com.ghb.base.common.system.vo.DictModel; +import com.ghb.base.common.system.vo.DictModelMany; +import com.ghb.base.common.system.vo.DictQuery; +import com.ghb.base.modules.system.entity.SysDict; +import com.ghb.base.modules.system.model.DuplicateCheckVo; +import com.ghb.base.modules.system.model.TreeSelectModel; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 字典表 Mapper 接口 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +public interface SysDictMapper extends BaseMapper { + + /** + * 重复检查SQL + * @param duplicateCheckVo + * @return + */ + @Deprecated + public Long duplicateCheckCountSql(DuplicateCheckVo duplicateCheckVo); + + /** + * 重复校验 sql语句 + * @param duplicateCheckVo + * @return + */ + @Deprecated + public Long duplicateCheckCountSqlNoDataId(DuplicateCheckVo duplicateCheckVo); + + /** + * 通过字典code获取字典数据 + * @param code 字典code + * @return List + */ + public List queryDictItemsByCode(@Param("code") String code); + + /** + * 查询有效的数据字典项 + * @param code + * @return + */ + List queryEnableDictItemsByCode(@Param("code") String code); + + + /** + * 通过多个字典code获取字典数据 + * + * @param dictCodeList + * @return + */ + public List queryDictItemsByCodeList(@Param("dictCodeList") List dictCodeList); + + /** + * 通过字典code获取字典数据 + * @param code + * @param key + * @return + */ + public String queryDictTextByKey(@Param("code") String code,@Param("key") String key); + + /** + * 可通过多个字典code查询翻译文本 + * @param dictCodeList 多个字典code + * @param keys 数据列表 + * @return + */ + List queryManyDictByKeys(@Param("dictCodeList") List dictCodeList, @Param("keys") List keys); + + /** + * 查询系统所有字典项 + * @return + */ + public List queryAllDictItems(List tenantIdList); + + /** + * 查询所有部门 作为字典信息 id -->value,departName -->text + * @return + */ + public List queryAllDepartBackDictModel(); + + /** + * 查询所有用户 作为字典信息 username -->value,realname -->text + * @return + */ + public List queryAllUserBackDictModel(); + + /** + * 根据表名、显示字段名、存储字段名 查询树 + * @param table + * @param text + * @param code + * @param pid + * @param hasChildField + * @param query + * @param pidField + * @return + */ + @Deprecated + List queryTreeList(@Param("query") Map query, @Param("table") String table, @Param("text") String text, @Param("code") String code, + @Param("pidField") String pidField, @Param("pid") String pid, @Param("hasChildField") String hasChildField, + @Param("converIsLeafVal") int converIsLeafVal); + + /** + * 删除 + * @param id + */ + @Select("delete from sys_dict where id = #{id}") + public void deleteOneById(@Param("id") String id); + + /** + * 查询被逻辑删除的数据 + * @return + */ + @Select("select * from sys_dict where del_flag = 1") + public List queryDeleteList(); + + /** + * 修改状态值 + * @param delFlag + * @param id + */ + @Update("update sys_dict set del_flag = #{flag,jdbcType=INTEGER} where id = #{id,jdbcType=VARCHAR}") + public void updateDictDelFlag(@Param("flag") int delFlag, @Param("id") String id); + + + /** + * 分页查询字典表数据 + * @param page + * @param query + * @return + */ + @Deprecated + public Page queryDictTablePageList(Page page, @Param("query") DictQuery query); + + + /** + * 查询 字典表数据 支持查询条件 分页 + * @param page + * @param table + * @param text + * @param code + * @param filterSql + * @return + */ + @Deprecated + IPage queryPageTableDictWithFilter(Page page, @Param("table") String table, @Param("text") String text, @Param("code") String code, @Param("filterSql") String filterSql); + + /** + * 查询 字典表数据 支持查询条件 查询所有 + * @param table + * @param text + * @param code + * @param filterSql + * @return + */ + @Deprecated + List queryTableDictWithFilter(@Param("table") String table, @Param("text") String text, @Param("code") String code, @Param("filterSql") String filterSql); + + /** + * 查询字典表的数据 + * @param table 表名 + * @param text 显示字段名 + * @param code 存储字段名 + * @param filterSql 条件sql + * @param codeValues 存储字段值 作为查询条件in + * @return + */ + @Deprecated + List queryTableDictByKeysAndFilterSql(@Param("table") String table, @Param("text") String text, @Param("code") String code, @Param("filterSql") String filterSql, + @Param("codeValues") List codeValues); + + /** + * 根据应用id获取字典列表和详情 + * @param lowAppId + * @param tenantId + * @return + */ + @InterceptorIgnore(tenantLine = "true") + List getDictListByLowAppId(@Param("lowAppId") String lowAppId, @Param("tenantId") Integer tenantId); + + /** + * 查询被逻辑删除的数据(根据租户id) + * @return + */ + @Select("select * from sys_dict where del_flag = 1 and tenant_id = #{tenantId}") + List queryDeleteListBtTenantId(@Param("tenantId") Integer tenantId); + + /** + * 还原被逻辑删除的数据(根据id) + * @param ids + * @return + */ + int revertLogicDeleted(@Param("ids") List ids); + + /** + * 彻底删除的数据(根据ids) + * @param ids + * @return + */ + int removeLogicDeleted(@Param("ids")List ids); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysFillRuleMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysFillRuleMapper.java new file mode 100644 index 0000000..9c6913a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysFillRuleMapper.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysFillRule; + +/** + * @Description: 填值规则 + * @Author: Ghb-boot + * @Date: 2019-11-07 + * @Version: V1.0 + */ +public interface SysFillRuleMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysFormFileMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysFormFileMapper.java new file mode 100644 index 0000000..461a47a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysFormFileMapper.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysFormFile; + +/** + * @Description: 表单评论文件 + * @Author: Ghb-boot + * @Date: 2022-07-21 + * @Version: V1.0 + */ +public interface SysFormFileMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysGatewayRouteMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysGatewayRouteMapper.java new file mode 100644 index 0000000..13de253 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysGatewayRouteMapper.java @@ -0,0 +1,35 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysGatewayRoute; + +import java.util.List; + +/** + * @Description: gateway路由管理 + * @Author: Ghb-boot + * @Date: 2020-05-26 + * @Version: V1.0 + */ +public interface SysGatewayRouteMapper extends BaseMapper { + /** + * 还原逻辑删除 + * @param ids + */ + int revertLogicDeleted(@Param("ids") List ids); + + /** + *彻底删除 + * @param ids + */ + int deleteLogicDeleted(@Param("ids") List ids); + + /** + * 查询删除的列表 + * @return + */ + @Select("select * from sys_gateway_route where del_flag = 1") + List queryDeleteList(); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysLogMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysLogMapper.java new file mode 100644 index 0000000..e54b83b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysLogMapper.java @@ -0,0 +1,58 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysLog; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 系统日志表 Mapper 接口 + *

+ * + * @Author zhangweijian + * @since 2018-12-26 + */ +public interface SysLogMapper extends BaseMapper { + + /** + * 清空所有日志记录 + */ + public void removeAll(); + + /** + * 获取系统总访问次数 + * + * @return Long + */ + Long findTotalVisitCount(); + + /** + * 获取系统今日访问次数 + * @param dayStart 开始时间 + * @param dayEnd 结束时间 + * @return Long + */ + Long findTodayVisitCount(@Param("dayStart") Date dayStart, @Param("dayEnd") Date dayEnd); + + /** + * 获取系统今日访问 IP数 + * @param dayStart 开始时间 + * @param dayEnd 结束时间 + * @return Long + */ + Long findTodayIp(@Param("dayStart") Date dayStart, @Param("dayEnd") Date dayEnd); + + /** + * 首页:根据时间统计访问数量/ip数量 + * @param dayStart + * @param dayEnd + * @param dbType + * @return + */ + List> findVisitCount(@Param("dayStart") Date dayStart, @Param("dayEnd") Date dayEnd, @Param("dbType") String dbType); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPackPermissionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPackPermissionMapper.java new file mode 100644 index 0000000..3517d94 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPackPermissionMapper.java @@ -0,0 +1,31 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysPackPermission; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 产品包菜单关系表 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +public interface SysPackPermissionMapper extends BaseMapper { + + /** + * 通过产品包id获取菜单id + * @param packId + * @return + */ + List getPermissionsByPackId(@Param("packId") String packId); + + /** + * 删除产品包对应的菜单权限 + * + * @param tenantIdList + */ + void deletePackPermByTenantIds(@Param("tenantIdList") List tenantIdList); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPermissionDataRuleMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPermissionDataRuleMapper.java new file mode 100644 index 0000000..be29119 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPermissionDataRuleMapper.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysPermissionDataRule; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 权限规则 Mapper 接口 + *

+ * + * @Author huangzhilin + * @since 2019-04-01 + */ +public interface SysPermissionDataRuleMapper extends BaseMapper { + + /** + * 根据用户名和权限id查询 + * @param username + * @param permissionId + * @return + */ + public List queryDataRuleIds(@Param("username") String username,@Param("permissionId") String permissionId); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPermissionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPermissionMapper.java new file mode 100644 index 0000000..8d244c4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPermissionMapper.java @@ -0,0 +1,94 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import com.ghb.base.modules.system.entity.SysPermission; +import com.ghb.base.modules.system.model.TreeModel; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 菜单权限表 Mapper 接口 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +public interface SysPermissionMapper extends BaseMapper { + /** + * 通过父菜单ID查询子菜单 + * @param parentId + * @return + */ + public List queryListByParentId(@Param("parentId") String parentId); + + /** + * 根据用户查询用户权限 + * @param userId 用户ID + * @return List + */ + public List queryByUser(@Param("userId") String userId); + + //update-begin---author:scott ---date:2026-04-16 for:【pull/9445】开启多租户模式时,获取用户权限时加入tenant_id判断----------- + /** + * 根据用户id和租户id查询用户权限 + * @param userId 用户ID + * @param tenantId 租户ID + * @return List + */ + public List queryByUserWithTenantId(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + //update-end---author:scott ---date:2026-04-16 for:【pull/9445】开启多租户模式时,获取用户权限时加入tenant_id判断----------- + + /** + * 修改菜单状态字段: 是否子节点 + * @param id 菜单id + * @param leaf 叶子节点 + * @return int + */ + @Update("update sys_permission set is_leaf=#{leaf} where id = #{id}") + public int setMenuLeaf(@Param("id") String id,@Param("leaf") int leaf); + + /** + * 切换vue3菜单 + */ + @Update("alter table sys_permission rename to sys_permission_v2") + public void backupVue2Menu(); + @Update("alter table sys_permission_v3 rename to sys_permission") + public void changeVue3Menu(); + + /** + * 获取模糊匹配规则的数据权限URL + * @return List + */ + @Select("SELECT url FROM sys_permission WHERE del_flag = 0 and menu_type = 2 and url like '%*%'") + public List queryPermissionUrlWithStar(); + + + /** + * 根据用户账号查询菜单权限 + * @param sysPermission + * @param username + * @return + */ + public int queryCountByUsername(@Param("username") String username, @Param("permission") SysPermission sysPermission); + + + /** + * 查询部门权限数据 + * @param departId + * @return + */ + List queryDepartPermissionList(@Param("departId") String departId); + + /** + * 根据用户名称和test角色id查询权限 + * @return + */ + @InterceptorIgnore(tenantLine = "true") + List queryPermissionByTestRoleId(); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPositionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPositionMapper.java new file mode 100644 index 0000000..4293681 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysPositionMapper.java @@ -0,0 +1,50 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysPosition; +import com.ghb.base.modules.system.vo.SysPositionVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Description: 职务表 + * @Author: Ghb-boot + * @Date: 2019-09-19 + * @Version: V1.0 + */ +public interface SysPositionMapper extends BaseMapper { + + /** + * 通过用户id获取职位名称 + * @param userId + * @return + */ + List getPositionList(@Param("userId") String userId); + + /** + * 通过职位id获取职位名称 + * @param postList + * @return + */ + List getPositionName(@Param("postList") List postList); + + /** + * 根据职位名称获取职位id + * @param name + * @return + */ + @Select("SELECT id FROM sys_position WHERE name = #{name} AND tenant_id = #{tenantId} ORDER BY create_time DESC") + List getPositionIdByName(@Param("name") String name, @Param("tenantId") Integer tenantId, @Param("page") Page page); + + /** + * 批量通过用户id列表查询职位(含userId字段,用于批量同步场景) + * + * @param userIds 用户id列表 + * @return 职位VO列表(每条记录含userId字段,供调用方分组) + */ + List getPositionListByUserIds(@Param("userIds") List userIds); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRoleIndexMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRoleIndexMapper.java new file mode 100644 index 0000000..18b6ae5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRoleIndexMapper.java @@ -0,0 +1,17 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysRoleIndex; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 角色首页配置 + * @Author: Ghb-boot + * @Date: 2022-03-25 + * @Version: V1.0 + */ +public interface SysRoleIndexMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRoleMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..4de5bb9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRoleMapper.java @@ -0,0 +1,86 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysRole; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.vo.SysUserPositionVo; + +import java.util.List; + +/** + *

+ * 角色表 Mapper 接口 + *

+ * + * @Author scott + * @since 2018-12-19 + */ +public interface SysRoleMapper extends BaseMapper { + /** + * 查询全部的角色(不做租户隔离) + * @param page + * @param role + * @return + */ + @InterceptorIgnore(tenantLine = "true") + List listAllSysRole(@Param("page") Page page, @Param("role") SysRole role); + + /** + * 查询角色是否存在不做租户隔离 + * + * @param roleCode + * @return + */ + @InterceptorIgnore(tenantLine = "true") + SysRole getRoleNoTenant(@Param("roleCode") String roleCode); + + /** + * 根据用户id查询用户拥有的角色Code + * + * @param userId + * @param tenantId + * @return + */ + List getRoleCodeListByUserId(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + + /** + * 删除角色与用户关系 + * @Author scott + * @Date 2019/12/13 16:12 + * @param roleId + */ + @Delete("delete from sys_user_role where role_id = #{roleId}") + void deleteRoleUserRelation(@Param("roleId") String roleId); + + + /** + * 删除角色与权限关系 + * @Author scott + * @param roleId + * @Date 2019/12/13 16:12 + */ + @Delete("delete from sys_role_permission where role_id = #{roleId}") + void deleteRolePermissionRelation(@Param("roleId") String roleId); + + /** + * 根据角色id和当前租户判断当前角色是否存在这个租户中 + * @param id + * @return + */ + @Select("select count(*) from sys_role where id=#{id} and tenant_id=#{tenantId}") + Long getRoleCountByTenantId(@Param("id") String id, @Param("tenantId") Integer tenantId); + + /** + * 根据用户id获取角色信息 + * + * @param userList + * @return + */ + List getUserRoleByUserId(@Param("userList") List userList); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRolePermissionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRolePermissionMapper.java new file mode 100644 index 0000000..7f82f2b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysRolePermissionMapper.java @@ -0,0 +1,16 @@ +package com.ghb.base.modules.system.mapper; + +import com.ghb.base.modules.system.entity.SysRolePermission; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 角色权限表 Mapper 接口 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +public interface SysRolePermissionMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTableWhiteListMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTableWhiteListMapper.java new file mode 100644 index 0000000..827c053 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTableWhiteListMapper.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysTableWhiteList; + +/** + * @Description: 系统表白名单 + * @Author: Ghb-boot + * @Date: 2023-09-12 + * @Version: V1.0 + */ +public interface SysTableWhiteListMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantMapper.java new file mode 100644 index 0000000..d42074e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantMapper.java @@ -0,0 +1,137 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysTenant; +import com.ghb.base.modules.system.vo.tenant.TenantPackUser; +import com.ghb.base.modules.system.vo.tenant.TenantPackUserCount; +import com.ghb.base.modules.system.vo.tenant.UserDepart; +import com.ghb.base.modules.system.vo.tenant.UserPosition; + +import java.util.List; + +/** + * @Description: 租户mapper接口 + * @author: Ghb-boot + */ +public interface SysTenantMapper extends BaseMapper { + + /** + * 获取最大值id + */ + @Select("select MAX(id) id FROM sys_tenant") + Integer getMaxTenantId(); + + /** + * 获取租户回收站的数据假删除 + * @param page + * @param sysTenant + * @return + */ + List getRecycleBinPageList(@Param("page") Page page, @Param("sysTenant") SysTenant sysTenant); + + /** + * 彻底删除租户 + * @param tenantId + */ + Integer deleteByTenantId(@Param("tenantIds") List tenantId); + + /** + * 租户还原 + * @param list + * @return + */ + Integer revertTenantLogic(@Param("tenantIds")List list); + + /** + * 用于统计 租户产品包的人员数量 + * @param tenantId + * @return + */ + List queryTenantPackUserCount(@Param("tenantId") Integer tenantId); + + /** + * 查询人员是不是租户产品包的 超级管理员 + * @param tenantId + * @param userId + * @return + */ + Integer querySuperAdminCount(@Param("tenantId") Integer tenantId, @Param("userId") String userId); + + /** + * 查询人员的产品包编码 + * @param tenantId + * @param userId + * @return + */ + List queryUserPackCode(@Param("tenantId") Integer tenantId, @Param("userId") String userId); + + /** + * 查询产品包关联的用户列表 + * @param tenantId + * @param packId + * @param packUserStatus + * @return + */ + List queryPackUserList(@Param("tenantId") Integer tenantId, @Param("packId") String packId, @Param("packUserStatus") Integer packUserStatus); + + + /** + * 根据用户ID 查询部门 + * @param userIdList + * @return + */ + List queryUserDepartList(@Param("userIdList") List userIdList); + + /** + * 根据用户ID 查询职位 + * @param userIdList + * @return + */ + List queryUserPositionList(@Param("userIdList") List userIdList); + + /** + * 查询产品包关联的用户列表 + * @param page + * @param tenantId + * @param packId + * @param status + * @return + */ + List queryTenantPackUserList(@Param("page") Page page, @Param("tenantId") String tenantId, @Param("packId") String packId, @Param("status") Integer status); + + + /** + * 根据租户ID 查询租户 + * @param id + * @return + */ + @Select("select * from sys_tenant where id = #{id}") + SysTenant querySysTenant(@Param("id") Integer id); + + /** + * 查看是否已经申请过了超级管理员 + * @param userId + * @param tenantId + * @return + */ + Long getApplySuperAdminCount(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + + /** + * 租户是否存在 + * @param tenantId + * @return + */ + @Select("select count(1) from sys_tenant where id = #{tenantId} and del_flag = 0") + Long tenantIzExist(@Param("tenantId") Integer tenantId); + + /** + * 根据用户id获取租户 + * @param userId + * @return + */ + List getTenantListByUserId(@Param("userId") String userId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantPackMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantPackMapper.java new file mode 100644 index 0000000..57c9f81 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantPackMapper.java @@ -0,0 +1,41 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysTenantPack; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 租户产品包 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +public interface SysTenantPackMapper extends BaseMapper { + + /** + * 删除租户产品包 + * + * @param tenantIdList + */ + void deletePackByTenantIds(@Param("tenantIdList") List tenantIdList); + + /** + * 根据租户id和产品包的code获取租户套餐id + * + * @param tenantId + */ + @Select("select id from sys_tenant_pack where tenant_id = #{tenantId} and (pack_code not in('superAdmin','accountAdmin','appAdmin') or pack_code is null) and iz_sysn = '1'") + List getPackIdByPackCodeAndTenantId(@Param("tenantId") Integer tenantId); + + /** + * 是否为拥有管理用户权限【accountAdmin,superAdmin】 + * @param tenantId + * @param userId + * @return + */ + @Select("select count(1) from sys_tenant_pack_user where user_id = #{userId} and tenant_id = #{tenantId} and pack_id in(select id from sys_tenant_pack where tenant_id = #{tenantId} and pack_type = 'custom' and pack_code in('accountAdmin','superAdmin'))") + long izHaveManageUserAuth(@Param("tenantId") String tenantId,@Param("userId") String userId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantPackUserMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantPackUserMapper.java new file mode 100644 index 0000000..46c0966 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysTenantPackUserMapper.java @@ -0,0 +1,68 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysTenantPack; +import com.ghb.base.modules.system.entity.SysTenantPackUser; + +import java.util.List; + +/** + * @Description: 租户产品包用户关系 + * @Author: Ghb-boot + * @Date: 2023-02-16 + * @Version: V1.0 + */ +public interface SysTenantPackUserMapper extends BaseMapper { + + + /** + * 查询租户下 特定角色的人员列表 + * @param tenantId + * @param packCodeList + * @return + */ + @InterceptorIgnore(tenantLine = "true") + List queryTenantPackUserNameList(@Param("tenantId") Integer tenantId, @Param("packCodeList") List packCodeList); + + /** + * 判断当前用户在该租户下是否拥有管理员的权限 + * @param userId + * @param tenantId + * @return + */ + Long izHaveBuyAuth(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + + /** + * 根据租户id 删除租户产品包下的 用户 + * @param tenantId + */ + void deletePackUserByTenantId(@Param("tenantId") Integer tenantId, @Param("userIds") List userIds); + + /** + * 根据多个租户id 删除租户产品包下的 用户 + * @param + */ + void deletePackUserByTenantIds(@Param("tenantIds") List tenantIds); + + /** + * 根据用户id和租户id获取当前租户用户下的产品包id + * + * @param tenantId + * @param userId + * @return + */ + @Select("select pack_id from sys_tenant_pack_user where tenant_id = #{tenantId} and user_id = #{userId}") + List getPackIdByTenantIdAndUserId(@Param("tenantId") Integer tenantId, @Param("userId") String userId); + + /** + * 根据租户id获取用户的产品包列表 + * + * @param tenantId + * @return + */ + @Select("select id,pack_name,pack_code,pack_type from sys_tenant_pack where tenant_id = #{tenantId}") + List getPackListByTenantId(@Param("tenantId") Integer tenantId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysThirdAccountMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysThirdAccountMapper.java new file mode 100644 index 0000000..1144c9a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysThirdAccountMapper.java @@ -0,0 +1,34 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.entity.SysThirdAccount; +import com.ghb.base.modules.system.vo.thirdapp.JwUserDepartVo; + +import java.util.List; + +/** + * @Description: 第三方登录账号表 + * @Author: Ghb-boot + * @Date: 2020-11-17 + * @Version: V1.0 + */ +public interface SysThirdAccountMapper extends BaseMapper { + + /** + * 通过 sysUsername 集合批量查询 + * + * @param sysUsernameArr username集合 + * @param thirdType 第三方类型 + * @return + */ + List selectThirdIdsByUsername(@Param("sysUsernameArr") String[] sysUsernameArr, @Param("thirdType") String thirdType, @Param("tenantId") Integer tenantId); + + /** + * 查询被绑定的用户 + * @param tenantId + * @param thirdType + * @return + */ + List getThirdUserBindByWechat(@Param("tenantId") int tenantId, @Param("thirdType") String thirdType); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysThirdAppConfigMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysThirdAppConfigMapper.java new file mode 100644 index 0000000..15f21f2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysThirdAppConfigMapper.java @@ -0,0 +1,31 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysThirdAppConfig; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Description: 第三方配置表 + * @Author: Ghb-boot + * @Date: 2023-02-03 + * @Version: V1.0 + */ +public interface SysThirdAppConfigMapper extends BaseMapper { + + /** + * 根据租户id获取钉钉/企业微信配置 + * @param tenantId + * @return + */ + List getThirdConfigListByThirdType(@Param("tenantId") int tenantId); + + /** + * 根据租户id和第三方类别获取第三方配置 + * @param tenantId + * @param thirdType + * @return + */ + SysThirdAppConfig getThirdConfigByThirdType(@Param("tenantId") int tenantId, @Param("thirdType") String thirdType); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUgroupMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUgroupMapper.java new file mode 100644 index 0000000..58b1af8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUgroupMapper.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysUgroup; + +/** + * @Description: 用户组表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +public interface SysUgroupMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUgroupUserMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUgroupUserMapper.java new file mode 100644 index 0000000..d945472 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUgroupUserMapper.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.entity.SysUgroupUser; + +/** + * @Description: 用户组关系表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +public interface SysUgroupUserMapper extends BaseMapper { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserDepPostMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserDepPostMapper.java new file mode 100644 index 0000000..80ea665 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserDepPostMapper.java @@ -0,0 +1,25 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysUserDepPost; + +import java.util.List; + +/** + * @Description: 部门岗位用户关联表 Mapper + * @author: wangshuai + * @date: 2025/9/5 12:01 + */ +public interface SysUserDepPostMapper extends BaseMapper { + + /** + * 通过用户id查询部门岗位用户 + * + * @param userId + * @return + */ + @Select("select dep_id from sys_user_dep_post where user_id = #{userId}") + List getDepPostByUserId(@Param("userId") String userId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserDepartMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserDepartMapper.java new file mode 100644 index 0000000..cbe0498 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserDepartMapper.java @@ -0,0 +1,110 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserDepart; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.model.SysUserSysDepPostModel; + +/** + * @Description: 用户部门mapper接口 + * @author: Ghb-boot + */ +public interface SysUserDepartMapper extends BaseMapper{ + + /** + * 通过用户id查询部门用户 + * @param userId 用户id + * @return List + */ + List getUserDepartByUid(@Param("userId") String userId); + + /** + * 查询指定部门下的用户 并且支持用户真实姓名模糊查询 + * @param orgCode + * @param realname + * @return + */ + List queryDepartUserList(@Param("orgCode") String orgCode, @Param("realname") String realname); + + /** + * 根据部门查询部门用户 + * @param page + * @param orgCode + * @param username + * @param realname + * @return + */ + IPage queryDepartUserPageList(Page page, @Param("orgCode") String orgCode, @Param("username") String username, @Param("realname") String realname); + + /** + * 获取用户信息 + * @param page + * @param orgCode + * @param keyword + * @return + */ + IPage getUserInformation(Page page, @Param("orgCode") String orgCode, @Param("keyword") String keyword,@Param("userId") String userId); + + + /** + * 获取用户信息 + * @param page + * @param orgCode + * @param keyword + * @return + */ + IPage getProcessUserList(Page page, @Param("orgCode") String orgCode, @Param("keyword") String keyword, @Param("tenantId") Integer tenantId, @Param("excludeUserIdList") List excludeUserIdList); + + /** + * 获取租户下的部门通过前台传过来的部门id + * @param departIds + * @param tenantId + * @return + */ + List getTenantDepart(@Param("departIds") List departIds, @Param("tenantId") String tenantId); + + /** + * 根据当前租户和用户id查询用户部门数据 + * @param userId + * @param tenantId + * @return + */ + List getTenantUserDepart(@Param("userId") String userId, @Param("tenantId") String tenantId); + + /** + * 根据用户id和租户id,删除用户部门数据 + * @param userId + * @param tenantId + */ + void deleteUserDepart(@Param("userId") String userId, @Param("tenantId") String tenantId); + + /** + * 通过部门id和租户id获取用户 + * @param departId + * @param tenantId + * @return + */ + List getUsersByDepartTenantId(@Param("departId") String departId, @Param("tenantId") Integer tenantId); + + /** + * 根据用户id和部门id获取数量,用于查看用户是否存在用户部门关系表中 + * @param userId + * @param departId + * @return + */ + @Select("SELECT COUNT(*) FROM sys_user_depart WHERE user_id = #{userId} AND dep_id = #{departId}") + Long getCountByDepartIdAndUserId(String userId, String departId); + + /** + * 通过用户id集合获取用户id和部门code + * + * @param userIdList + * @return + */ + List getUserDepPostByUserIds(@Param("userIdList") List userIdList); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..164ad85 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserMapper.java @@ -0,0 +1,294 @@ +package com.ghb.base.modules.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysUser; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.model.SysUserSysDepPostModel; +import com.ghb.base.modules.system.model.SysUserSysDepartModel; +import com.ghb.base.modules.system.vo.SysUserDepVo; + +import java.util.List; + +/** + *

+ * 用户表 Mapper 接口 + *

+ * + * @Author scott + * @since 2018-12-20 + */ +public interface SysUserMapper extends BaseMapper { + /** + * 通过用户账号查询用户信息 + * @param username + * @return + */ + public SysUser getUserByName(@Param("username") String username); + + /** + * 通过用户账号查询用户Id + * @param username + * @return + */ + public String getUserIdByName(@Param("username") String username); + + /** + * 通过用户账号查询用户Id + * @param userIds + * @return + */ + public List getUsernameByIds(@Param("userIds") List userIds); + + /** + * 根据部门Id查询用户信息 + * @param page + * @param departId + * @param username 用户登录账户 + * @return + */ + IPage getUserByDepId(Page page, @Param("departId") String departId, @Param("username") String username); + + /** + * 根据部门和子部门下的所有用户账号 + * + * @param orgCode 部门编码 + * @return + */ + List getUserAccountsByDepCode(@Param("orgCode") String orgCode); + + /** + * 根据用户Ids,查询用户所属部门名称信息 + * @param userIds + * @return + */ + List getDepNamesByUserIds(@Param("userIds")List userIds); + + /** + * 根据部门Ids,查询部门下用户信息 + * @param page + * @param departIds + * @param username 用户登录账户 + * @return + */ + IPage getUserByDepIds(Page page, @Param("departIds") List departIds, @Param("username") String username); + + /** + * 根据角色Id查询用户信息 + * @param page + * @param roleId 角色id + * @param username 用户登录账户 + * @param realname 用户姓名 + * @return + */ + IPage getUserByRoleId(Page page, @Param("roleId") String roleId, @Param("username") String username, @Param("realname") String realname); + + /** + * 根据用户名设置部门ID + * @param username + * @param orgCode + */ + void updateUserDepart(@Param("username") String username,@Param("orgCode") String orgCode, @Param("loginTenantId") Integer loginTenantId); + + /** + * 根据手机号查询用户信息 + * @param phone + * @return + */ + public SysUser getUserByPhone(@Param("phone") String phone); + + + /** + * 根据邮箱查询用户信息 + * @param email + * @return + */ + public SysUser getUserByEmail(@Param("email")String email); + + /** + * 根据 orgCode 查询用户,包括子部门下的用户 + * + * @param page 分页对象, xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象) + * @param orgCode + * @param userParams 用户查询条件,可为空 + * @return + */ + List getUserByOrgCode(IPage page, @Param("orgCode") String orgCode, @Param("userParams") SysUser userParams); + + + /** + * 查询 getUserByOrgCode 的Total + * + * @param orgCode + * @param userParams 用户查询条件,可为空 + * @return + */ + Integer getUserByOrgCodeTotal(@Param("orgCode") String orgCode, @Param("userParams") SysUser userParams); + + /** + * 批量删除角色与用户关系 + * @Author scott + * @Date 2019/12/13 16:10 + * @param roleIdArray + */ + void deleteBathRoleUserRelation(@Param("roleIdArray") String[] roleIdArray); + + /** + * 批量删除角色与权限关系 + * @Author scott + * @Date 2019/12/13 16:10 + * @param roleIdArray + */ + void deleteBathRolePermissionRelation(@Param("roleIdArray") String[] roleIdArray); + + /** + * 查询被逻辑删除的用户 + * @param wrapper + * @return List + */ + List selectLogicDeleted(@Param(Constants.WRAPPER) Wrapper wrapper); + + /** + * 还原被逻辑删除的用户 + * @param userIds 用户id + * @param entity + * @return int + */ + int revertLogicDeleted(@Param("userIds") List userIds, @Param("entity") SysUser entity); + + /** + * 彻底删除被逻辑删除的用户 + * @param userIds 多个用户id + * @return int + */ + int deleteLogicDeleted(@Param("userIds") List userIds); + + /** + * 更新空字符串为null【此写法有sql注入风险,禁止随便用】 + * @param fieldName + * @return int + */ + @Deprecated + int updateNullByEmptyString(@Param("fieldName") String fieldName); + + /** + * 根据部门Ids,查询部门下用户信息 + * @param departIds + * @param username 用户账户名称 + * @return + */ + List queryByDepIds(@Param("departIds")List departIds,@Param("username") String username); + + /** + * 获取用户信息 + * @param page + * @param roleId + * @param keyword + * @param userIdList + * @return + */ + IPage selectUserListByRoleId(Page page, @Param("roleId") String roleId, @Param("keyword") String keyword, @Param("tenantId") Integer tenantId, @Param("excludeUserIdList") List excludeUserIdList); + + /** + * 更新刪除状态和离职状态 + * @param userIds 存放用户id集合 + * @param sysUser + * @return boolean + */ + void updateStatusAndFlag(@Param("userIds") List userIds, @Param("sysUser") SysUser sysUser); + + /** + * 获取租户下的离职列表信息 + * @param tenantId + * @return + */ + List getTenantQuitList(@Param("tenantId") Integer tenantId); + + /** + * 获取租户下的有效用户ids + * @param tenantId + * @return + */ + List getTenantUserIdList(@Param("tenantId") Integer tenantId); + + /** + * 根据部门id和租户id获取用户数据 + * @param departIds + * @param tenantId + * @return + */ + List getUserByDepartsTenantId(@Param("departIds") List departIds,@Param("tenantId") Integer tenantId); + + /** + * 根据用户名和手机号获取用户 + * @param phone + * @param username + * @return + */ + @Select("select id,phone from sys_user where phone = #{phone} and username = #{username}") + SysUser getUserByNameAndPhone(@Param("phone") String phone, @Param("username") String username); + + /** + * 查询部门、岗位下的用户 包括子部门下的用户 + * + * @param page + * @param orgCode + * @param userParams + * @return + */ + List queryDepartPostUserByOrgCode(@Param("page") IPage page, @Param("orgCode") String orgCode, @Param("userParams") SysUser userParams); + + /** + * 根据部门id和用户名获取部门岗位用户分页列表 + * + * @param page + * @param userIdList + * @return + */ + IPage getDepPostListByIdUserName(@Param("page") Page page, @Param("userIdList") List userIdList, @Param("userId") String userId, @Param("userName") String userName, @Param("userNameList") List userNameList); + + /** + * 根据部门id、用户名和真实姓名获取部门岗位用户分页列表 + * + * @param page + * @param username + * @param realname + * @param orgCode + * @return + */ + IPage getDepartPostListByIdUserRealName(@Param("page") Page page, @Param("username") String username, @Param("realname") String realname, @Param("orgCode") String orgCode); + + /** + * 查询部门下的用户包括子部门下的用户 + * + * @param page + * @param orgCode + * @param userParams + * @return + */ + List queryDepartUserByOrgCode(@Param("page") IPage page, @Param("orgCode") String orgCode, @Param("userParams") SysUser userParams); + + /** + * 根据用户名查询用户的主部门信息 + * + * @param username + * @return + */ + SysDepart getMainDepartByUsername(@Param("username") String username); + + + /** + * 根据用户组id获取用户分页列表 + * @param page + * @param groupId + * @param username + * @param realname + * @return + */ + IPage getUserByUgroupId(Page page, @Param("groupId") String groupId, @Param("username") String username, @Param("realname") String realname); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserPositionMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserPositionMapper.java new file mode 100644 index 0000000..1cc7820 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserPositionMapper.java @@ -0,0 +1,86 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserPosition; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import com.ghb.base.modules.system.vo.SysUserPositionVo; + +/** + * @Description: 用户职位关系表 + * @Author: Ghb-boot + * @Date: 2023-02-14 + * @Version: V1.0 + */ +public interface SysUserPositionMapper extends BaseMapper { + + /** + * 获取职位用户列表 + * @param page + * @param positionId + * @return + */ + List getPositionUserList(@Param("page") Page page, @Param("positionId") String positionId); + + /** + * 获取成员是否存在职位中 + * @param userId + * @param positionId + * @return + */ + @Select("SELECT count(*) FROM sys_user_position WHERE user_id = #{userId} and position_id = #{positionId}") + Long getUserPositionCount(@Param("userId") String userId, @Param("positionId") String positionId); + + /** + * 通过职位id删除用户职位关系表 + * @param positionId + */ + @Delete("DELETE FROM sys_user_position WHERE position_id = #{positionId} ") + void removeByPositionId(@Param("positionId") String positionId); + + /** + * 职位列表移除成员 + * @param userIdList + * @param positionId + */ + void removePositionUser(@Param("userIdList") List userIdList, @Param("positionId") String positionId); + + /** + * 根据用户id查询职位id + * @param userId + * @return + */ + List getPositionIdByUserId(@Param("userId") String userId); + + + /** + * 根据用户ID和租户ID获取职位id + * @param userId + * @param tenantId + * @return + */ + @InterceptorIgnore(tenantLine = "true") + List getPositionIdByUserTenantId(@Param("userId")String userId, @Param("tenantId")Integer tenantId); + + /** + * 根据用户id获取用户职位 + * @param userIdList + * @param tenantId + * @return + */ + List getPositionIdByUsersTenantId(@Param("userIdList") List userIdList, @Param("tenantId") Integer tenantId); + + /** + * 根据职位名称和租户id,删除用户职位关系表 + * @param positionNames + * @param tenantId + * @param userId + */ + void deleteUserPosByNameAndTenantId(@Param("positionNames") List positionNames, @Param("tenantId") Integer tenantId, @Param("userId") String userId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserRoleMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..160b7c3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,43 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import com.ghb.base.modules.system.entity.SysUserRole; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 用户角色表 Mapper 接口 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +public interface SysUserRoleMapper extends BaseMapper { + + /** + * 通过用户账号查询角色集合 + * @param username 用户账号名称 + * @return List + */ + @Select("select role_code from sys_role where id in (select role_id from sys_user_role where user_id = (select id from sys_user where username=#{username}))") + List getRoleByUserName(@Param("username") String username); + + /** + * 通过用户账号查询角色集合 + * @param userId 用户id + * @return List + */ + @Select("select role_code from sys_role where id in (select role_id from sys_user_role where user_id = #{userId})") + List getRoleCodeByUserId(@Param("userId") String userId); + + /** + * 通过用户账号查询角色Id集合 + * @param username 用户账号名称 + * @return List + */ + @Select("select id from sys_role where id in (select role_id from sys_user_role where user_id = (select id from sys_user where username=#{username}))") + List getRoleIdByUserName(@Param("username") String username); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserTenantMapper.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserTenantMapper.java new file mode 100644 index 0000000..22f595e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/SysUserTenantMapper.java @@ -0,0 +1,177 @@ +package com.ghb.base.modules.system.mapper; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +import com.ghb.base.modules.system.entity.SysTenant; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserTenant; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.system.vo.SysUserTenantVo; +import com.ghb.base.modules.system.vo.thirdapp.JwUserDepartVo; + +/** + * @Description: sys_user_tenant_relation + * @Author: Ghb-boot + * @Date: 2022-12-23 + * @Version: V1.0 + */ +public interface SysUserTenantMapper extends BaseMapper { + + /** + * 通过租户id获取数据 + * @param page + * @param userTenantId + * @return + */ + List getPageUserList(@Param("page") Page page,@Param("userTenantId") Integer userTenantId,@Param("user") SysUser user); + + /** + * 根据租户id获取用户ids + * @param tenantId + * @return + */ + List getUserIdsByTenantId(@Param("tenantId") Integer tenantId); + + /** + * 通过用户id获取租户ids + * @param userId + * @return + */ + List getTenantIdsByUserId(@Param("userId") String userId); + + + + //============================================================================================================================== + /** + * 通过用户id获取租户列表 + * @param userId + * @return + */ + List getTenantListByUserId(@Param("userId") String userId, @Param("userTenantStatus") List userTenantStatus); + + /** + * 通过状态、当前登录人的用户名,租户id,查询用户id + * @param tenantId + * @param statusList + * @param username + * @return + */ + List getUserIdsByCreateBy(@Param("tenantId") Integer tenantId, @Param("userTenantStatus") List statusList, @Param("username") String username); + + /** + * 联查用户和租户审核状态 + * @param page + * @param status + * @param tenantId + * @return + */ + List getUserTenantPageList(@Param("page") Page page, @Param("status") List status, @Param("user") SysUser user, @Param("tenantId") Integer tenantId); + + /** + * 根据用户id获取租户id,没有状态值(如获取租户已经存在,只不过是被拒绝或者审批中) + * @param userId + * @return + */ + List getTenantIdsNoStatus(@Param("userId") String userId); + //============================================================================================================================== + + /** + * 统计一个人创建了多少个租户 + * + * @param userId + * @return + */ + Integer countCreateTenantNum(String userId); + + /** + * 取消离职 + * @param userIds + * @param tenantId + */ + void putCancelQuit(@Param("userIds") List userIds, @Param("tenantId") Integer tenantId); + + /** + * 判断当前用户是否已在该租户下面 + * @param userId + * @param tenantId + */ + Integer userTenantIzExist(@Param("userId") String userId, @Param("tenantId") int tenantId); + + /** + * 查询未被注销的租户 + * @param userId + * @return + */ + List getTenantNoCancel(@Param("userId") String userId); + + /** + * 根据用户id获取我的租户 + * @param page + * @param userId + * @param userTenantStatus + * @return + */ + List getTenantPageListByUserId(@Param("page") Page page, @Param("userId") String userId, @Param("userTenantStatus") List userTenantStatus,@Param("sysUserTenantVo") SysUserTenantVo sysUserTenantVo); + + /** + * 同意加入租户 + * @param userId + * @param tenantId + */ + @Update("update sys_user_tenant set status = '1' where user_id = #{userId} and tenant_id = #{tenantId}") + void agreeJoinTenant(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + + /** + * 拒绝加入租户 + * @param userId + * @param tenantId + */ + @Delete("delete from sys_user_tenant where user_id = #{userId} and tenant_id = #{tenantId}") + void refuseJoinTenant(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + + /** + * 根据用户id和租户id获取用户租户中间表信息 + * + * @param userId + * @param tenantId + * @return + */ + @Select("select id,user_id,tenant_id,create_by,status from sys_user_tenant where user_id = #{userId} and tenant_id = #{tenantId}") + SysUserTenant getUserTenantByTenantId(@Param("userId") String userId, @Param("tenantId") Integer tenantId); + + /** + * 删除租户下的用户 + * + * @param tenantIds + */ + void deleteUserByTenantId(@Param("tenantIds") List tenantIds); + + /** + * 获取租户下的成员数量 + * + * @param tenantId + * @param tenantStatus + * @return + */ + Long getUserCount(Integer tenantId, String tenantStatus); + + /** + * 根据租户id和名称获取用户数据 + * @param tenantId + * @return + */ + List getUsersByTenantIdAndName(@Param("tenantId") Integer tenantId); + + /** + * 根据多个用户id获取租户id + * + * @param userIds + * @return + */ + List getTenantIdsByUserIds(@Param("userIds") List userIds); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysAnnouncementMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysAnnouncementMapper.xml new file mode 100644 index 0000000..5199753 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysAnnouncementMapper.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysAnnouncementSendMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysAnnouncementSendMapper.xml new file mode 100644 index 0000000..483a077 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysAnnouncementSendMapper.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_announcement_send set read_flag = 1 + where user_id = #{userId} + and annt_id in + + #{id} + + + + + + + update sys_announcement_send set read_flag = 1 + where user_id = #{userId} and read_flag = 0 + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCategoryMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCategoryMapper.xml new file mode 100644 index 0000000..5e3736a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCategoryMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCheckRuleMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCheckRuleMapper.xml new file mode 100644 index 0000000..c6cf424 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCheckRuleMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCommentMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCommentMapper.xml new file mode 100644 index 0000000..8da4d86 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysCommentMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDataLogMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDataLogMapper.xml new file mode 100644 index 0000000..84a9130 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDataLogMapper.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDataSourceMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDataSourceMapper.xml new file mode 100644 index 0000000..3435cc7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDataSourceMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartMapper.xml new file mode 100644 index 0000000..bd346fe --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartMapper.xml @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartPermissionMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartPermissionMapper.xml new file mode 100644 index 0000000..7f89bf7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartPermissionMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRoleMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRoleMapper.xml new file mode 100644 index 0000000..662a872 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRoleMapper.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRolePermissionMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRolePermissionMapper.xml new file mode 100644 index 0000000..d9a8956 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRolePermissionMapper.xml @@ -0,0 +1,12 @@ + + + + + + DELETE FROM sys_depart_role_permission + WHERE role_id IN + + #{roleId} + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRoleUserMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRoleUserMapper.xml new file mode 100644 index 0000000..ca9423a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDepartRoleUserMapper.xml @@ -0,0 +1,12 @@ + + + + + + DELETE FROM sys_depart_role_user + WHERE drole_id IN + + #{roleId} + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDictItemMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDictItemMapper.xml new file mode 100644 index 0000000..6acf5d2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDictItemMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDictMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDictMapper.xml new file mode 100644 index 0000000..58da88e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysDictMapper.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(1) FROM ${tableName} WHERE ${fieldName} = #{fieldVal} + + + + + + + + + + + + + select ${text} as "text", ${code} as "value" from ${table} + + where ${filterSql} + + + + + + + + + + + + + + + + + + UPDATE + sys_dict + SET + del_flag = 0 + WHERE + del_flag = 1 + AND id IN + + #{dictId} + + + + + + DELETE FROM sys_dict + WHERE + del_flag = 1 + AND id IN + + #{dictId} + + + + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysFillRuleMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysFillRuleMapper.xml new file mode 100644 index 0000000..75372c0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysFillRuleMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysGatewayRouteMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysGatewayRouteMapper.xml new file mode 100644 index 0000000..6feff41 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysGatewayRouteMapper.xml @@ -0,0 +1,30 @@ + + + + + + + UPDATE + sys_gateway_route + SET + del_flag = 0 + WHERE + del_flag = 1 + AND id IN + + #{routeId} + + + + + + DELETE FROM sys_gateway_route + WHERE + del_flag = 1 + AND id IN + + #{routeId} + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysLogMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysLogMapper.xml new file mode 100644 index 0000000..7703d54 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysLogMapper.xml @@ -0,0 +1,69 @@ + + + + + + + DELETE FROM sys_log + + + + + + + + + + + + + + + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPackPermissionMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPackPermissionMapper.xml new file mode 100644 index 0000000..a1e985f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPackPermissionMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + delete from sys_tenant_pack_perms + where pack_id in( + select id from sys_tenant_pack where tenant_id in + + #{tenantId} + + ) + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPermissionDataRuleMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPermissionDataRuleMapper.xml new file mode 100644 index 0000000..14aacf0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPermissionDataRuleMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPermissionMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPermissionMapper.xml new file mode 100644 index 0000000..0c0387c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPermissionMapper.xml @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPositionMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPositionMapper.xml new file mode 100644 index 0000000..ce17129 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysPositionMapper.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysRoleIndexMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysRoleIndexMapper.xml new file mode 100644 index 0000000..8186457 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysRoleIndexMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysRoleMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysRoleMapper.xml new file mode 100644 index 0000000..f5afd59 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysRoleMapper.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTableWhiteListMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTableWhiteListMapper.xml new file mode 100644 index 0000000..8aab9b2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTableWhiteListMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantMapper.xml new file mode 100644 index 0000000..4ab0263 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantMapper.xml @@ -0,0 +1,142 @@ + + + + + + + + + + DELETE FROM sys_tenant + WHERE + del_flag = 1 + AND id in + + #{id} + + + + + + UPDATE sys_tenant set del_flag = 0 + WHERE + del_flag = 1 + AND id in + + #{id} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantPackMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantPackMapper.xml new file mode 100644 index 0000000..216dadd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantPackMapper.xml @@ -0,0 +1,13 @@ + + + + + + + delete from sys_tenant_pack + where tenant_id in + + #{tenantId} + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantPackUserMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantPackUserMapper.xml new file mode 100644 index 0000000..4498bf5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysTenantPackUserMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + delete from sys_tenant_pack_user where tenant_id = #{tenantId} + and user_id in + + #{userId} + + + + + + delete from sys_tenant_pack_user + where tenant_id in + + #{tenantId} + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysThirdAccountMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysThirdAccountMapper.xml new file mode 100644 index 0000000..94972c0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysThirdAccountMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysThirdAppConfigMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysThirdAppConfigMapper.xml new file mode 100644 index 0000000..0776aff --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysThirdAppConfigMapper.xml @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUgroupMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUgroupMapper.xml new file mode 100644 index 0000000..e14c935 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUgroupMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUgroupUserMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUgroupUserMapper.xml new file mode 100644 index 0000000..2f50ac7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUgroupUserMapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserDepartMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserDepartMapper.xml new file mode 100644 index 0000000..45aab4d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserDepartMapper.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + delete from sys_user_depart + where + user_id = #{userId} + and dep_id in( + select id from sys_depart where tenant_id = #{tenantId} + ) + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserMapper.xml new file mode 100644 index 0000000..e16b10c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserMapper.xml @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE sys_user SET + + org_code = #{orgCode, jdbcType=VARCHAR} + ,login_tenant_id = #{loginTenantId, jdbcType=VARCHAR} + + + login_tenant_id = #{loginTenantId, jdbcType=VARCHAR} + + + org_code = #{orgCode, jdbcType=VARCHAR} + + + org_code = #{orgCode, jdbcType=VARCHAR} + + where username = #{username} + + + + + + + + + + + FROM + sys_depart + INNER JOIN sys_user_depart ON sys_user_depart.dep_id = sys_depart.id + INNER JOIN sys_user ON sys_user.id = sys_user_depart.user_id + WHERE + + + + + + + sys_user.del_flag = 0 AND sys_depart.org_code LIKE #{bindOrgCode} + + + + AND sys_user.realname LIKE concat(concat('%',#{userParams.realname}),'%') + + + AND sys_user.work_no LIKE concat(concat('%',#{userParams.workNo}),'%') + + + + + + + + + + + + + delete from sys_user_role + where role_id in + + #{id} + + + + + delete from sys_role_permission + where role_id in + + #{id} + + + + + + + + + UPDATE + sys_user + SET + del_flag = 0, + update_by = #{entity.updateBy}, + update_time = #{entity.updateTime} + WHERE + del_flag = 1 + AND id IN + + #{userId} + + + + + + DELETE FROM sys_user WHERE del_flag = 1 AND id IN + + #{userId} + + + + + + UPDATE sys_user + + SET email = NULL WHERE email = '' + + + SET phone = NULL WHERE phone = '' + + + + + + + + + + + + + + + + + + UPDATE + sys_user + SET + del_flag = 0, + update_by = #{sysUser.updateBy}, + update_time = #{sysUser.updateTime}, + status = 1 + WHERE + del_flag = 1 + AND id IN + + #{userId} + + + + + + + + + + WHERE + su.status = 1 + and su.del_flag = 0 + and username '_reserve_user_external' + + and su.id = #{userId} + + + + and su.username like #{bindUserName} + + + + #{idItem} + + + + + #{usernameItem} + + + + + + + + + + WHERE + su.status = 1 + and su.del_flag = 0 + and username '_reserve_user_external' + + + and su.id like #{bindRealname} + + + + and su.username like #{bindUserName} + + + + and sd.org_code like #{bindOrgCode} + + + + + + + + + WHERE + + + + + + + su.del_flag = 0 AND sd.org_code LIKE #{bindOrgCode} + + + + AND su.realname LIKE #{bindRealname} + + + + AND su.work_no LIKE #{bindWorkNo} + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserPositionMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserPositionMapper.xml new file mode 100644 index 0000000..b7814a1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserPositionMapper.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + DELETE FROM sys_user_position + WHERE + position_id = #{positionId} + AND user_id IN + + #{userId} + + + + + + + + + DELETE FROM sys_user_position + WHERE user_id = #{userId} + AND position_id in ( + SELECT id FROM sys_position where name in + + #{name} + + AND tenant_id = #{tenantId} + ) + + diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserTenantMapper.xml b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserTenantMapper.xml new file mode 100644 index 0000000..549aaa5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/mapper/xml/SysUserTenantMapper.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_user_tenant set status='1' + where + tenant_id = #{tenantId} + AND user_id in + + #{userId} + + + + + + + + + + DELETE FROM sys_user_tenant + WHERE + tenant_id in + + #{tenantId} + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/AnnouncementSendModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/AnnouncementSendModel.java new file mode 100644 index 0000000..87e0d94 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/AnnouncementSendModel.java @@ -0,0 +1,107 @@ +package com.ghb.base.modules.system.model; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.List; + +/** + * @Description: 用户通告阅读标记表 + * @Author: Ghb-boot + * @Date: 2019-02-21 + * @Version: V1.0 + */ +@Data +public class AnnouncementSendModel implements Serializable { + private static final long serialVersionUID = 1L; + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + private java.lang.String id; + /**通告id*/ + private java.lang.String anntId; + /**用户id*/ + private java.lang.String userId; + /**标题*/ + private java.lang.String titile; + /**内容*/ + private java.lang.String msgContent; + /**发布人*/ + private java.lang.String sender; + /**优先级(L低,M中,H高)*/ + private java.lang.String priority; + /**阅读状态*/ + private java.lang.Integer readFlag; + /**发布时间*/ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private java.util.Date sendTime; + /**页数*/ + private java.lang.Integer pageNo; + /**大小*/ + private java.lang.Integer pageSize; + /** + * 消息类型1:通知公告2:系统消息 + */ + private java.lang.String msgCategory; + /** + * 业务id + */ + private java.lang.String busId; + /** + * 业务类型 + */ + private java.lang.String busType; + /** + * 打开方式 组件:component 路由:url + */ + private java.lang.String openType; + /** + * 组件/路由 地址 + */ + private java.lang.String openPage; + + /** + * 业务类型查询(0.非bpm业务) + */ + private java.lang.String bizSource; + + /** + * 摘要 + */ + private java.lang.String msgAbstract; + + /** + * 发布开始日期 + */ + private java.lang.String sendTimeBegin; + + /** + * 发布结束日期 + */ + private java.lang.String sendTimeEnd; + /** + * 附件 + */ + private java.lang.String files; + /** + * 访问量 + */ + private java.lang.Integer visitsNum; + /** + * 是否置顶(0否 1是) + */ + private java.lang.Integer izTop; + /** + * 通知类型(plan:日程计划 | flow:流程消息 | meeting:会议 | file:知识库 | collab:协同通知 | supe:督办通知 | attendance:考勤) + */ + private java.lang.String noticeType; + /** + * 通告类型数组 + */ + private List noticeTypeList; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/DepartIdModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/DepartIdModel.java new file mode 100644 index 0000000..bf8dac5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/DepartIdModel.java @@ -0,0 +1,111 @@ +package com.ghb.base.modules.system.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.ghb.base.modules.system.entity.SysDepart; + +/** + *

+ * 部门表 封装树结构的部门的名称的实体类 + *

+ * + * @Author Steve + * @Since 2019-01-22 + * + */ +public class DepartIdModel implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + private String key; + + /** + * 主键ID + */ + private String value; + /** + * 部门编码 + */ + private String code; + + /** + * 部门名称 + */ + private String title; + + List children = new ArrayList<>(); + + /** + * 将SysDepartTreeModel的部分数据放在该对象当中 + * @param treeModel + * @return + */ + public DepartIdModel convert(SysDepartTreeModel treeModel) { + this.key = treeModel.getId(); + this.value = treeModel.getId(); + this.title = treeModel.getDepartName(); + return this; + } + + /** + * 该方法为用户部门的实现类所使用 + * @param sysDepart + * @return + */ + public DepartIdModel convertByUserDepart(SysDepart sysDepart) { + this.key = sysDepart.getId(); + this.value = sysDepart.getId(); + this.code = sysDepart.getOrgCode(); + this.title = sysDepart.getDepartName(); + return this; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public static long getSerialVersionUID() { + return serialVersionUID; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/DuplicateCheckVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/DuplicateCheckVo.java new file mode 100644 index 0000000..b189a8b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/DuplicateCheckVo.java @@ -0,0 +1,44 @@ +package com.ghb.base.modules.system.model; + +import java.io.Serializable; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Title: DuplicateCheckVo + * @Description: 重复校验VO + * @Author 张代浩 + * @Date 2019-03-25 + * @Version V1.0 + */ +@Data +@Schema(description="重复校验数据模型") +public class DuplicateCheckVo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 表名 + */ + @Schema(description="表名",name="tableName",example="sys_log") + private String tableName; + + /** + * 字段名 + */ + @Schema(description="字段名",name="fieldName",example="id") + private String fieldName; + + /** + * 字段值 + */ + @Schema(description="字段值",name="fieldVal",example="1000") + private String fieldVal; + + /** + * 数据ID + */ + @Schema(description="数据ID",name="dataId",example="2000") + private String dataId; + +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysDepartTreeModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysDepartTreeModel.java new file mode 100644 index 0000000..6f2dbb2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysDepartTreeModel.java @@ -0,0 +1,429 @@ +package com.ghb.base.modules.system.model; + +import com.ghb.base.modules.system.entity.SysDepart; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + *

+ * 部门表 存储树结构数据的实体类 + *

+ * + * @Author Steve + * @Since 2019-01-22 + */ +public class SysDepartTreeModel implements Serializable{ + + private static final long serialVersionUID = 1L; + + /** 对应SysDepart中的id字段,前端数据树中的key*/ + private String key; + + /** 对应SysDepart中的id字段,前端数据树中的value*/ + private String value; + + /** 对应depart_name字段,前端数据树中的title*/ + private String title; + + + private boolean isLeaf; + // 以下所有字段均与SysDepart相同 + + private String id; + + private String parentId; + + private String departName; + + private String departNameEn; + + private String departNameAbbr; + + private Integer departOrder; + + private String description; + + private String orgCategory; + + private String orgType; + + private String orgCode; + + private String mobile; + + private String fax; + + private String address; + + private String memo; + + private String status; + + private String delFlag; + + private String qywxIdentifier; + + private String createBy; + + private Date createTime; + + private String updateBy; + + private Date updateTime; + + /**部门负责人ids + * [JTC-119]在部门管理菜单下设置部门负责人,新增字段部门负责人ids + * */ + private String directorUserIds; + + /**职务*/ + private String positionId; + + /**上级岗位id*/ + private String depPostParentId; + + private List children = new ArrayList<>(); + + + /** + * 将SysDepart对象转换成SysDepartTreeModel对象 + * @param sysDepart + */ + public SysDepartTreeModel(SysDepart sysDepart) { + this.key = sysDepart.getId(); + this.value = sysDepart.getId(); + this.title = sysDepart.getDepartName(); + this.id = sysDepart.getId(); + this.parentId = sysDepart.getParentId(); + this.departName = sysDepart.getDepartName(); + this.departNameEn = sysDepart.getDepartNameEn(); + this.departNameAbbr = sysDepart.getDepartNameAbbr(); + this.departOrder = sysDepart.getDepartOrder(); + this.description = sysDepart.getDescription(); + this.orgCategory = sysDepart.getOrgCategory(); + this.orgType = sysDepart.getOrgType(); + this.orgCode = sysDepart.getOrgCode(); + this.mobile = sysDepart.getMobile(); + this.fax = sysDepart.getFax(); + this.address = sysDepart.getAddress(); + this.memo = sysDepart.getMemo(); + this.status = sysDepart.getStatus(); + this.delFlag = sysDepart.getDelFlag(); + this.qywxIdentifier = sysDepart.getQywxIdentifier(); + this.createBy = sysDepart.getCreateBy(); + this.createTime = sysDepart.getCreateTime(); + this.updateBy = sysDepart.getUpdateBy(); + this.updateTime = sysDepart.getUpdateTime(); + this.directorUserIds = sysDepart.getDirectorUserIds(); + this.positionId = sysDepart.getPositionId(); + this.depPostParentId = sysDepart.getDepPostParentId(); + if(0 == sysDepart.getIzLeaf()){ + this.isLeaf = false; + }else{ + this.isLeaf = true; + } + } + + public boolean getIsLeaf() { + return isLeaf; + } + + public void setIsLeaf(boolean isleaf) { + this.isLeaf = isleaf; + } + + public String getKey() { + return key; + } + + + public void setKey(String key) { + this.key = key; + } + + + public String getValue() { + return value; + } + + + public void setValue(String value) { + this.value = value; + } + + + public String getTitle() { + return title; + } + + + public void setTitle(String title) { + this.title = title; + } + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + if (children==null){ + this.isLeaf=true; + } + this.children = children; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public static long getSerialVersionUID() { + return serialVersionUID; + } + + public String getDepartName() { + return departName; + } + + public void setDepartName(String departName) { + this.departName = departName; + } + + public String getOrgCategory() { + return orgCategory; + } + + public void setOrgCategory(String orgCategory) { + this.orgCategory = orgCategory; + } + + public String getOrgType() { + return orgType; + } + + public void setOrgType(String orgType) { + this.orgType = orgType; + } + + public String getOrgCode() { + return orgCode; + } + + public void setOrgCode(String orgCode) { + this.orgCode = orgCode; + } + + public String getMobile() { + return mobile; + } + + public void setMobile(String mobile) { + this.mobile = mobile; + } + + public String getFax() { + return fax; + } + + public void setFax(String fax) { + this.fax = fax; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getMemo() { + return memo; + } + + public void setMemo(String memo) { + this.memo = memo; + } + + public String getDepartNameEn() { + return departNameEn; + } + + public void setDepartNameEn(String departNameEn) { + this.departNameEn = departNameEn; + } + + public String getDepartNameAbbr() { + return departNameAbbr; + } + + public void setDepartNameAbbr(String departNameAbbr) { + this.departNameAbbr = departNameAbbr; + } + + public Integer getDepartOrder() { + return departOrder; + } + + public void setDepartOrder(Integer departOrder) { + this.departOrder = departOrder; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getDelFlag() { + return delFlag; + } + + public void setDelFlag(String delFlag) { + this.delFlag = delFlag; + } + + public String getQywxIdentifier() { + return qywxIdentifier; + } + + public void setQywxIdentifier(String qywxIdentifier) { + this.qywxIdentifier = qywxIdentifier; + } + + public String getCreateBy() { + return createBy; + } + + public void setCreateBy(String createBy) { + this.createBy = createBy; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getUpdateBy() { + return updateBy; + } + + public void setUpdateBy(String updateBy) { + this.updateBy = updateBy; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public SysDepartTreeModel() { } + + public String getDirectorUserIds() { + return directorUserIds; + } + + public void setDirectorUserIds(String directorUserIds) { + this.directorUserIds = directorUserIds; + } + + public String getPositionId() { + return positionId; + } + + public void setPositionId(String positionId) { + this.positionId = positionId; + } + + public String getDepPostParentId() { + return depPostParentId; + } + + public void setDepPostParentId(String depPostParentId) { + this.depPostParentId = depPostParentId; + } + + /** + * 重写equals方法 + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SysDepartTreeModel model = (SysDepartTreeModel) o; + return Objects.equals(id, model.id) && + Objects.equals(parentId, model.parentId) && + Objects.equals(departName, model.departName) && + Objects.equals(departNameEn, model.departNameEn) && + Objects.equals(departNameAbbr, model.departNameAbbr) && + Objects.equals(departOrder, model.departOrder) && + Objects.equals(description, model.description) && + Objects.equals(orgCategory, model.orgCategory) && + Objects.equals(orgType, model.orgType) && + Objects.equals(orgCode, model.orgCode) && + Objects.equals(mobile, model.mobile) && + Objects.equals(fax, model.fax) && + Objects.equals(address, model.address) && + Objects.equals(memo, model.memo) && + Objects.equals(status, model.status) && + Objects.equals(delFlag, model.delFlag) && + Objects.equals(qywxIdentifier, model.qywxIdentifier) && + Objects.equals(createBy, model.createBy) && + Objects.equals(createTime, model.createTime) && + Objects.equals(updateBy, model.updateBy) && + Objects.equals(updateTime, model.updateTime) && + Objects.equals(directorUserIds, model.directorUserIds) && + Objects.equals(positionId, model.positionId) && + Objects.equals(depPostParentId, model.depPostParentId) && + Objects.equals(children, model.children); + } + + /** + * 重写hashCode方法 + */ + @Override + public int hashCode() { + + return Objects.hash(id, parentId, departName, departNameEn, departNameAbbr, + departOrder, description, orgCategory, orgType, orgCode, mobile, fax, address, + memo, status, delFlag, qywxIdentifier, createBy, createTime, updateBy, updateTime, + children,directorUserIds, positionId, depPostParentId); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysDictTree.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysDictTree.java new file mode 100644 index 0000000..07f9c4f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysDictTree.java @@ -0,0 +1,96 @@ +package com.ghb.base.modules.system.model; + +import java.io.Serializable; +import java.util.Date; + +import com.ghb.base.modules.system.entity.SysDict; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 字典表 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +public class SysDictTree implements Serializable { + + private static final long serialVersionUID = 1L; + + private String key; + + private String title; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + /** + * 字典类型,0 string,1 number类型,2 boolean + * 前端js对stirng类型和number类型 boolean 类型敏感,需要区分。在select 标签匹配的时候会用到 + * 默认为string类型 + */ + private Integer type; + + /** + * 字典名称 + */ + private String dictName; + + /** + * 字典编码 + */ + private String dictCode; + + /** + * 描述 + */ + private String description; + + /** + * 删除状态 + */ + private Integer delFlag; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + public SysDictTree(SysDict node) { + this.id = node.getId(); + this.key = node.getId(); + this.title = node.getDictName(); + this.dictCode = node.getDictCode(); + this.description = node.getDescription(); + this.delFlag = node.getDelFlag(); + this.type = node.getType(); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysLoginModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysLoginModel.java new file mode 100644 index 0000000..1d808b1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysLoginModel.java @@ -0,0 +1,64 @@ +package com.ghb.base.modules.system.model; + + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 登录表单 + * + * @Author scott + * @since 2019-01-18 + */ +@Schema(description="登录对象") +public class SysLoginModel { + @Schema(description = "账号") + private String username; + @Schema(description = "密码") + private String password; + @Schema(description = "登录部门") + private String loginOrgCode; + @Schema(description = "验证码") + private String captcha; + @Schema(description = "验证码key") + private String checkKey; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getCaptcha() { + return captcha; + } + + public void setCaptcha(String captcha) { + this.captcha = captcha; + } + + public String getCheckKey() { + return checkKey; + } + + public void setCheckKey(String checkKey) { + this.checkKey = checkKey; + } + + public String getLoginOrgCode() { + return loginOrgCode; + } + + public void setLoginOrgCode(String loginOrgCode) { + this.loginOrgCode = loginOrgCode; + } +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysPermissionTree.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysPermissionTree.java new file mode 100644 index 0000000..d920626 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysPermissionTree.java @@ -0,0 +1,429 @@ +package com.ghb.base.modules.system.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import com.ghb.base.modules.system.entity.SysPermission; + +/** + * @Description: 菜单树,封装树结构 + * @author: Ghb-boot + */ +public class SysPermissionTree implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + private String id; + + private String key; + private String title; + + /** + * 父id + */ + private String parentId; + + /** + * 菜单名称 + */ + private String name; + + /** + * 菜单权限编码 + */ + private String perms; + /** + * 权限策略1显示2禁用 + */ + private String permsType; + + /** + * 菜单图标 + */ + private String icon; + + /** + * 组件 + */ + private String component; + + /** + * 组件名字 + */ + private String componentName; + + /** + * 跳转网页链接 + */ + private String url; + + /** + * 一级菜单跳转地址 + */ + private String redirect; + + /** + * 菜单排序 + */ + private Double sortNo; + + /** + * 类型(0:一级菜单;1:子菜单 ;2:按钮权限) + */ + private Integer menuType; + + /** + * 是否叶子节点: 1:是 0:不是 + */ + private boolean isLeaf; + + /** + * 是否路由菜单: 0:不是 1:是(默认值1) + */ + private boolean route; + + + /** + * 是否路缓存页面: 0:不是 1:是(默认值1) + */ + private boolean keepAlive; + + + /** + * 描述 + */ + private String description; + + /** + * 删除状态 0正常 1已删除 + */ + private Integer delFlag; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /**alwaysShow*/ + private boolean alwaysShow; + /**是否隐藏路由菜单: 0否,1是(默认值0)*/ + private boolean hidden; + + /**按钮权限状态(0无效1有效)*/ + private java.lang.String status; + + /*update_begin author:wuxianquan date:20190908 for:model增加字段 */ + /** 外链菜单打开方式 0/内部打开 1/外部打开 */ + private boolean internalOrExternal; + /*update_end author:wuxianquan date:20190908 for:model增加字段 */ + + /*update_begin author:liusq date:20230601 for:【issues/4986】model增加hideTab字段 */ + /** + * 是否隐藏Tab: 0否,1是(默认值0) + */ + private boolean hideTab; + /*update_end author:liusq date:20230601 for:【issues/4986】model增加hideTab字段 */ + + public SysPermissionTree() { + } + + public SysPermissionTree(SysPermission permission) { + this.key = permission.getId(); + this.id = permission.getId(); + this.perms = permission.getPerms(); + this.permsType = permission.getPermsType(); + this.component = permission.getComponent(); + this.componentName = permission.getComponentName(); + this.createBy = permission.getCreateBy(); + this.createTime = permission.getCreateTime(); + this.delFlag = permission.getDelFlag(); + this.description = permission.getDescription(); + this.icon = permission.getIcon(); + this.isLeaf = permission.isLeaf(); + this.menuType = permission.getMenuType(); + this.name = permission.getName(); + this.parentId = permission.getParentId(); + this.sortNo = permission.getSortNo(); + this.updateBy = permission.getUpdateBy(); + this.updateTime = permission.getUpdateTime(); + this.redirect = permission.getRedirect(); + this.url = permission.getUrl(); + this.hidden = permission.isHidden(); + this.route = permission.isRoute(); + this.keepAlive = permission.isKeepAlive(); + this.alwaysShow= permission.isAlwaysShow(); + /*update_begin author:wuxianquan date:20190908 for:赋值 */ + this.internalOrExternal = permission.isInternalOrExternal(); + /*update_end author:wuxianquan date:20190908 for:赋值 */ + this.title=permission.getName(); + /*update_end author:liusq date:20230601 for:【issues/4986】model增加hideTab字段 */ + this.hideTab = permission.isHideTab(); + /*update_end author:liusq date:20230601 for:【issues/4986】model增加hideTab字段 */ + if (!permission.isLeaf()) { + this.children = new ArrayList(); + } + this.status = permission.getStatus(); + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + private List children; + + public boolean isLeaf() { + return isLeaf; + } + + public void setLeaf(boolean leaf) { + isLeaf = leaf; + } + + public boolean isKeepAlive() { + return keepAlive; + } + + public void setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + } + + public boolean isAlwaysShow() { + return alwaysShow; + } + + public void setAlwaysShow(boolean alwaysShow) { + this.alwaysShow = alwaysShow; + } + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public String getRedirect() { + return redirect; + } + + public void setRedirect(String redirect) { + this.redirect = redirect; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public boolean isHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public String getComponentName() { + return componentName; + } + + public void setComponentName(String componentName) { + this.componentName = componentName; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Double getSortNo() { + return sortNo; + } + + public void setSortNo(Double sortNo) { + this.sortNo = sortNo; + } + + public Integer getMenuType() { + return menuType; + } + + public void setMenuType(Integer menuType) { + this.menuType = menuType; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isRoute() { + return route; + } + + public void setRoute(boolean route) { + this.route = route; + } + + public Integer getDelFlag() { + return delFlag; + } + + public void setDelFlag(Integer delFlag) { + this.delFlag = delFlag; + } + + public String getCreateBy() { + return createBy; + } + + public void setCreateBy(String createBy) { + this.createBy = createBy; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + public String getUpdateBy() { + return updateBy; + } + + public void setUpdateBy(String updateBy) { + this.updateBy = updateBy; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getPerms() { + return perms; + } + + public void setPerms(String perms) { + this.perms = perms; + } + + public boolean getIsLeaf() { + return isLeaf; + } + + public void setIsLeaf(boolean isLeaf) { + this.isLeaf = isLeaf; + } + + public String getPermsType() { + return permsType; + } + + public void setPermsType(String permsType) { + this.permsType = permsType; + } + + public java.lang.String getStatus() { + return status; + } + + public void setStatus(java.lang.String status) { + this.status = status; + } + + /*update_begin author:wuxianquan date:20190908 for:get set方法 */ + public boolean isInternalOrExternal() { + return internalOrExternal; + } + + public void setInternalOrExternal(boolean internalOrExternal) { + this.internalOrExternal = internalOrExternal; + } + /*update_end author:wuxianquan date:20190908 for:get set 方法 */ + + public boolean isHideTab() { + return hideTab; + } + + public void setHideTab(boolean hideTab) { + this.hideTab = hideTab; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysUserSysDepPostModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysUserSysDepPostModel.java new file mode 100644 index 0000000..5c17a40 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysUserSysDepPostModel.java @@ -0,0 +1,170 @@ +package com.ghb.base.modules.system.model; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @Description: 部门用户和部门岗位用户的Model + * @author: wangshuai + * @date: 2025/9/5 16:43 + */ +@Data +public class SysUserSysDepPostModel { + /** + * 用户ID + */ + private String id; + + /** + * 用户名 + */ + private String username; + + /* 真实姓名 */ + private String realname; + + /** + * 头像 + */ + @Excel(name = "头像", width = 15, type = 2) + private String avatar; + /** + * 生日 + */ + @Excel(name = "生日", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date birthday; + + /** + * 性别(1:男 2:女) + */ + @Excel(name = "性别", width = 15, dicCode = "sex") + @Dict(dicCode = "sex") + private Integer sex; + + /** + * 电子邮件 + */ + @Excel(name = "电子邮件", width = 15) + private String email; + + /** + * 电话 + */ + @Excel(name = "电话", width = 15) + private String phone; + + /** + * 状态(1:正常 2:冻结 ) + */ + @Excel(name = "状态", width = 15, dicCode = "user_status") + @Dict(dicCode = "user_status") + private Integer status; + + /** + * 删除状态(0,正常,1已删除) + */ + @Excel(name = "删除状态", width = 15, dicCode = "del_flag") + @TableLogic + private Integer delFlag; + + /** + * 座机号 + */ + @Excel(name = "座机号", width = 15) + private String telephone; + + /** + * 身份(0 普通成员 1 上级) + */ + @Excel(name = "(1普通成员 2上级)", width = 15) + private Integer userIdentity; + + /** + * 负责部门 + */ + @Excel(name = "负责部门", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id") + @Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id") + private String departIds; + + /** + * 多租户ids临时用,不持久化数据库(数据库字段不存在) + */ + private String relTenantIds; + + /** + * 同步工作流引擎(1-同步 0-不同步) + */ + private String activitiSync; + /** + * 主岗位 + */ + @Excel(name = "主岗位", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id") + @Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id") + private String mainDepPostId; + + /** + * 兼职岗位 + */ + @Excel(name = "兼职岗位", width = 15, dictTable = "sys_depart", dicText = "depart_name", dicCode = "id") + @Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id") + @TableField(exist = false) + private String otherDepPostId; + + /** + * 部门名称 + */ + private String departName; + /** + * 主岗位 + */ + private String postName; + + /** + * 兼职岗位 + */ + private String otherPostName; + + /** + * 部门text + */ + private String orgCodeTxt; + + /** + * 职务 + */ + private String post; + + /** + * 部门编码 + */ + private String orgCode; + + /** + * 职务(字典) + */ + private String positionType; + + /** + * 排序 + */ + private Integer sort; + + /** + * 是否隐藏联系方式 0否1是 + */ + private String izHideContact; + + /** + * 工号 + */ + private String workNo; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysUserSysDepartModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysUserSysDepartModel.java new file mode 100644 index 0000000..2e2814d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/SysUserSysDepartModel.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.system.model; + +import lombok.Data; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysUser; + +/** + * 包含 SysUser 和 SysDepart 的 Model + * + * @author sunjianlei + */ +@Data +public class SysUserSysDepartModel { + + private String id; + private String realname; + private String workNo; + private String post; + private String telephone; + private String email; + private String phone; + private String departId; + private String departName; + private String avatar; + private String sex; + private String birthday; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/ThirdLoginModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/ThirdLoginModel.java new file mode 100644 index 0000000..372668f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/ThirdLoginModel.java @@ -0,0 +1,73 @@ +package com.ghb.base.modules.system.model; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 第三方登录 信息存储 + * @author: Ghb-boot + */ +@Data +public class ThirdLoginModel implements Serializable { + private static final long serialVersionUID = 4098628709290780891L; + + /** + * 第三方登录 来源 + */ + private String source; + + /** + * 第三方登录 uuid + */ + private String uuid; + + /** + * 第三方登录 username + */ + private String username; + + /** + * 第三方登录 头像 + */ + private String avatar; + + /** + * 账号 后缀第三方登录 防止账号重复 + */ + private String suffix; + + /** + * 操作码 防止被攻击 + */ + private String operateCode; + + public ThirdLoginModel(){ + + } + + /** + * 构造器 + * @param source + * @param uuid + * @param username + * @param avatar + */ + public ThirdLoginModel(String source,String uuid,String username,String avatar){ + this.source = source; + this.uuid = uuid; + this.username = username; + this.avatar = avatar; + } + + /** + * 获取登录账号名 + * @return + */ + public String getUserLoginAccount(){ + if(suffix==null){ + return this.uuid; + } + return this.uuid + this.suffix; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/TreeModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/TreeModel.java new file mode 100644 index 0000000..d586985 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/TreeModel.java @@ -0,0 +1,175 @@ +package com.ghb.base.modules.system.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.ghb.base.modules.system.entity.SysPermission; + +/** + * 树形列表用到 + * @author: Ghb-boot + */ +public class TreeModel implements Serializable { + + private static final long serialVersionUID = 4013193970046502756L; + + private String key; + + private String title; + + private String slotTitle; + + private Boolean isLeaf; + + private String icon; + + private Integer ruleFlag; + + private Map scopedSlots; + + public Map getScopedSlots() { + return scopedSlots; + } + + public void setScopedSlots(Map scopedSlots) { + this.scopedSlots = scopedSlots; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Boolean getIsLeaf() { + return isLeaf; + } + + public void setIsLeaf(Boolean isLeaf) { + this.isLeaf = isLeaf; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + private List children; + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public TreeModel() { + + } + + public TreeModel(SysPermission permission) { + this.key = permission.getId(); + this.icon = permission.getIcon(); + this.parentId = permission.getParentId(); + this.title = permission.getName(); + this.slotTitle = permission.getName(); + this.value = permission.getId(); + this.isLeaf = permission.isLeaf(); + this.label = permission.getName(); + if(!permission.isLeaf()) { + this.children = new ArrayList(); + } + } + + public TreeModel(String key,String parentId,String slotTitle,Integer ruleFlag,boolean isLeaf) { + this.key = key; + this.parentId = parentId; + this.ruleFlag=ruleFlag; + this.slotTitle = slotTitle; + Map map = new HashMap(5); + map.put("title", "hasDatarule"); + this.scopedSlots = map; + this.isLeaf = isLeaf; + this.value = key; + if(!isLeaf) { + this.children = new ArrayList(); + } + } + + private String parentId; + + private String label; + + private String value; + + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + /** + * @return the label + */ + public String getLabel() { + return label; + } + + /** + * @param label the label to set + */ + public void setLabel(String label) { + this.label = label; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value the value to set + */ + public void setValue(String value) { + this.value = value; + } + + public String getSlotTitle() { + return slotTitle; + } + + public void setSlotTitle(String slotTitle) { + this.slotTitle = slotTitle; + } + + public Integer getRuleFlag() { + return ruleFlag; + } + + public void setRuleFlag(Integer ruleFlag) { + this.ruleFlag = ruleFlag; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/TreeSelectModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/TreeSelectModel.java new file mode 100644 index 0000000..63542d9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/model/TreeSelectModel.java @@ -0,0 +1,96 @@ +package com.ghb.base.modules.system.model; + +import java.io.Serializable; +import java.util.List; + +/** + * 树形下拉框 + * @author: Ghb-boot + */ +public class TreeSelectModel implements Serializable { + + private static final long serialVersionUID = 9016390975325574747L; + + private String key; + + private String title; + /** + * 是否叶子节点 + */ + private boolean isLeaf; + + private String icon; + + private String parentId; + + private String value; + + private String code; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public boolean isLeaf() { + return isLeaf; + } + + public void setLeaf(boolean isLeaf) { + this.isLeaf = isLeaf; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + private List children; + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/CategoryCodeRule.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/CategoryCodeRule.java new file mode 100644 index 0000000..3f30e93 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/CategoryCodeRule.java @@ -0,0 +1,73 @@ +package com.ghb.base.modules.system.rule; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.handler.IFillRuleHandler; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.YouBianCodeUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysCategory; +import com.ghb.base.modules.system.mapper.SysCategoryMapper; + +import java.util.List; + +/** + * @Author scott + * @Date 2019/12/9 11:32 + * @Description: 分类字典编码生成规则 + */ +@Slf4j +public class CategoryCodeRule implements IFillRuleHandler { + + public static final String ROOT_PID_VALUE = "0"; + + @Override + public Object execute(JSONObject params, JSONObject formData) { + log.info("系统自定义编码规则[category_code_rule],params:{} ,formData: {}", params, formData); + + String categoryPid = ROOT_PID_VALUE; + String categoryCode = null; + + if (formData != null && formData.size() > 0) { + Object obj = formData.get("pid"); + if (oConvertUtils.isNotEmpty(obj)) { + categoryPid = obj.toString(); + } + } else { + if (params != null) { + Object obj = params.get("pid"); + if (oConvertUtils.isNotEmpty(obj)) { + categoryPid = obj.toString(); + } + } + } + + /* + * 分成三种情况 + * 1.数据库无数据 调用YouBianCodeUtil.getNextYouBianCode(null); + * 2.添加子节点,无兄弟元素 YouBianCodeUtil.getSubYouBianCode(parentCode,null); + * 3.添加子节点有兄弟元素 YouBianCodeUtil.getNextYouBianCode(lastCode); + * */ + //找同类 确定上一个最大的code值 + SysCategoryMapper baseMapper = (SysCategoryMapper) SpringContextUtils.getBean("sysCategoryMapper"); + // 代码逻辑说明: 【issues/4846】开启saas多租户功能后,租户管理员在添加分类字典时,报错------------ + Page page = new Page<>(1,1); + List list = baseMapper.getMaxCategoryCodeByPage(page,categoryPid); + if (list == null || list.size() == 0) { + if (ROOT_PID_VALUE.equals(categoryPid)) { + //情况1 + categoryCode = YouBianCodeUtil.getNextYouBianCode(null); + } else { + //情况2 + SysCategory parent = (SysCategory) baseMapper.selectSysCategoryById(categoryPid); + categoryCode = YouBianCodeUtil.getSubYouBianCode(parent.getCode(), null); + } + } else { + //情况3 + categoryCode = YouBianCodeUtil.getNextYouBianCode(list.get(0).getCode()); + } + return categoryCode; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/OrderNumberRule.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/OrderNumberRule.java new file mode 100644 index 0000000..71977a1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/OrderNumberRule.java @@ -0,0 +1,36 @@ +package com.ghb.base.modules.system.rule; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.math.RandomUtils; +import com.ghb.base.common.handler.IFillRuleHandler; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 填值规则Demo:生成订单号 + * 【测试示例】 + */ +public class OrderNumberRule implements IFillRuleHandler { + + @Override + public Object execute(JSONObject params, JSONObject formData) { + String prefix = "CN"; + //订单前缀默认为CN 如果规则参数不为空,则取自定义前缀 + if (params != null) { + Object obj = params.get("prefix"); + if (obj != null) prefix = obj.toString(); + } + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss"); + int random = RandomUtils.nextInt(90) + 10; + String value = prefix + format.format(new Date()) + random; + // 根据formData的值的不同,生成不同的订单号 + String name = formData.getString("name"); + if (!StringUtils.isEmpty(name)) { + value += name; + } + return value; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/OrgCodeRule.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/OrgCodeRule.java new file mode 100644 index 0000000..d2b3663 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/rule/OrgCodeRule.java @@ -0,0 +1,101 @@ +package com.ghb.base.modules.system.rule; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.netty.util.internal.StringUtil; +import com.ghb.base.common.handler.IFillRuleHandler; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.YouBianCodeUtil; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.service.ISysDepartService; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author scott + * @Date 2019/12/9 11:33 + * @Description: 机构编码生成规则 + */ +public class OrgCodeRule implements IFillRuleHandler { + + @Override + public Object execute(JSONObject params, JSONObject formData) { + ISysDepartService sysDepartService = (ISysDepartService) SpringContextUtils.getBean("sysDepartServiceImpl"); + + LambdaQueryWrapper query = new LambdaQueryWrapper(); + LambdaQueryWrapper query1 = new LambdaQueryWrapper(); + // 创建一个List集合,存储查询返回的所有SysDepart对象 + List departList = new ArrayList<>(); + String[] strArray = new String[2]; + //定义部门类型 + String orgType = ""; + // 定义新编码字符串 + String newOrgCode = ""; + // 定义旧编码字符串 + String oldOrgCode = ""; + + String parentId = null; + if (formData != null && formData.size() > 0) { + Object obj = formData.get("parentId"); + if (obj != null) { + parentId = obj.toString(); + } + } else { + if (params != null) { + Object obj = params.get("parentId"); + if (obj != null) { + parentId = obj.toString(); + } + } + } + + //如果是最高级,则查询出同级的org_code, 调用工具类生成编码并返回 + if (StringUtil.isNullOrEmpty(parentId)) { + // 线判断数据库中的表是否为空,空则直接返回初始编码 + //获取最大值code的部门信息 + // 代码逻辑说明: [QQYUN-4209]租户隔离下部门新建不了------------ + Page page = new Page<>(1,1); + IPage pageList = sysDepartService.getMaxCodeDepart(page,""); + List records = pageList.getRecords(); + if (null==records || records.size()==0) { + strArray[0] = YouBianCodeUtil.getNextYouBianCode(null); + strArray[1] = "1"; + return strArray; + } else { + SysDepart depart = records.get(0); + oldOrgCode = depart.getOrgCode(); + orgType = depart.getOrgType(); + newOrgCode = YouBianCodeUtil.getNextYouBianCode(oldOrgCode); + } + } else {//反之则查询出所有同级的部门,获取结果后有两种情况,有同级和没有同级 + //获取自己部门最大值orgCode部门信息 + // 代码逻辑说明: [QQYUN-4209]租户隔离下部门新建不了------------ + Page page = new Page<>(1,1); + IPage pageList = sysDepartService.getMaxCodeDepart(page,parentId); + List records = pageList.getRecords(); + // 查询出父级部门 + SysDepart depart = sysDepartService.getDepartById(parentId); + // 获取父级部门的Code + String parentCode = depart.getOrgCode(); + // 根据父级部门类型算出当前部门的类型 + orgType = String.valueOf(Integer.valueOf(depart.getOrgType()) + 1); + // 处理同级部门为null的情况 + if (null == records || records.size()==0) { + // 直接生成当前的部门编码并返回 + newOrgCode = YouBianCodeUtil.getSubYouBianCode(parentCode, null); + } else { //处理有同级部门的情况 + // 获取同级部门的编码,利用工具类 + String subCode = records.get(0).getOrgCode(); + // 返回生成的当前部门编码 + newOrgCode = YouBianCodeUtil.getSubYouBianCode(parentCode, subCode); + } + } + // 返回最终封装了部门编码和部门类型的数组 + strArray[0] = newOrgCode; + strArray[1] = orgType; + return strArray; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/security/DictQueryBlackListHandler.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/security/DictQueryBlackListHandler.java new file mode 100644 index 0000000..d98cb45 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/security/DictQueryBlackListHandler.java @@ -0,0 +1,84 @@ +package com.ghb.base.modules.system.security; + +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.common.util.security.AbstractQueryBlackListHandler; +import org.springframework.stereotype.Component; + +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; + +/** + * 字典组件 执行sql前校验 只校验表字典 + * dictCodeString格式如: + * table,text,code + * table where xxx,text,code + * table,text,code, where xxx + * + * @Author taoYan + * @Date 2022/3/23 21:10 + **/ +@Component("dictQueryBlackListHandler") +public class DictQueryBlackListHandler extends AbstractQueryBlackListHandler { + + @Override + protected List getQueryTableInfo(String dictCodeString) { + //针对转义字符进行解码 + try { + if (dictCodeString.contains("%")) { + dictCodeString = URLDecoder.decode(dictCodeString, "UTF-8"); + } + } catch (Exception e) { + //e.printStackTrace(); + } + dictCodeString = dictCodeString.trim(); + + // 无论什么场景 第二、三个元素一定是表的字段,直接add + if (dictCodeString != null && dictCodeString.indexOf(SymbolConstant.COMMA) > 0) { + String[] arr = dictCodeString.split(SymbolConstant.COMMA); + if (arr.length != 3 && arr.length != 4) { + return null; + } + + //获取表名 + String tableName = getTableName(arr[0]); + QueryTable table = new QueryTable(tableName, ""); + // 无论什么场景 第二、三个元素一定是表的字段,直接add + //参数字段1 + table.addField(arr[1].trim()); + //参数字段2 + String filed = arr[2].trim(); + if (oConvertUtils.isNotEmpty(filed)) { + table.addField(filed); + } + List list = new ArrayList<>(); + list.add(table); + return list; + } + return null; + } + + /** + * 取where前面的为:table name + * + * @param str + * @return + */ + private String getTableName(String str) { + String[] arr = str.split("\\s+(?i)where\\s+"); + String tableName = arr[0].trim(); + //【20230814】解决使用参数tableName=sys_user t&复测,漏洞仍然存在 + if (tableName.contains(".")) { + tableName = tableName.substring(tableName.indexOf(".")+1, tableName.length()).trim(); + } + if (tableName.contains(" ")) { + tableName = tableName.substring(0, tableName.indexOf(" ")).trim(); + } + + //【issues/4393】 sys_user , (sys_user), sys_user%20, %60sys_user%60 + String reg = "\\s+|\\(|\\)|`"; + return tableName.replaceAll(reg, ""); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysAnnouncementSendService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysAnnouncementSendService.java new file mode 100644 index 0000000..4fc9d03 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysAnnouncementSendService.java @@ -0,0 +1,55 @@ +package com.ghb.base.modules.system.service; + +import java.util.List; + +import com.ghb.base.modules.system.entity.SysAnnouncementSend; +import com.ghb.base.modules.system.model.AnnouncementSendModel; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 用户通告阅读标记表 + * @Author: Ghb-boot + * @Date: 2019-02-21 + * @Version: V1.0 + */ +public interface ISysAnnouncementSendService extends IService { + + /** + * 获取我的消息 + * @param announcementSendModel + * @param page 当前页数 + * @return + */ + public Page getMyAnnouncementSendPage(Page page,AnnouncementSendModel announcementSendModel); + + /** + * 根据消息发送记录ID获取消息内容 + * @return + */ + AnnouncementSendModel getOne(String sendId); + + + /** + * 获取当前用户已阅读的内容 + * + * @param id + * @return + */ + long getReadCountByUserId(String id); + + /** + * 根据多个id批量删除已阅读的数量 + * + * @param ids + */ + void deleteBatchByIds(String ids); + + /** + * 根据id更新阅读状态 + * @param busId + * @param busType + */ + boolean updateReadFlagByBusId(String busId, String busType); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysAnnouncementService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysAnnouncementService.java new file mode 100644 index 0000000..d7087ea --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysAnnouncementService.java @@ -0,0 +1,111 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysAnnouncement; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.List; + +/** + * @Description: 系统通告表 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version: V1.0 + */ +public interface ISysAnnouncementService extends IService { + + /** + * 保存系统通告 + * @param sysAnnouncement + */ + public void saveAnnouncement(SysAnnouncement sysAnnouncement); + + /** + * 修改系统通告 + * @param sysAnnouncement + * @return + */ + public boolean upDateAnnouncement(SysAnnouncement sysAnnouncement); + + /** + * 保存系统通告 + * @param title 标题 + * @param msgContent 信息内容 + */ + public void saveSysAnnouncement(String title, String msgContent); + + /** + * 分页查询系统通告 + * @param page 当前页数 + * @param userId 用户id + * @param msgCategory 消息类型 + * @return Page + */ + public Page querySysCementPageByUserId(Page page, String userId, String msgCategory, Integer tenantId, Date beginDate); + + /** + * 获取用户未读消息数量 + * + * @param userId 用户id + * @param noticeType 通知类型 + * @return + */ + public Integer getUnreadMessageCountByUserId(String userId, Date beginDate, String noticeType); + + + /** + * 补全当前登录用户的消息阅读记录 + * @作废无用 2023-09-19 + */ + @Deprecated + void completeAnnouncementSendInfo(); + + /** + * 补全所有用户的推送公告关系数据 + * + * @param commentId + * @param tenantId + */ + void batchInsertSysAnnouncementSend(String commentId, Integer tenantId); + + /** + * 分页查询当前登录用户的消息, 并且标记哪些是未读消息 + */ + List querySysMessageList(int pageSize, int pageNo, String fromUser, String starFlag, String busType, String msgCategory, Date beginDate, Date endDate, String noticeType); + + /** + * 修改为已读消息 + */ + void updateReaded(List annoceIdList); + + + /** + * 清除所有未读消息 + */ + void clearAllUnReadMessage(); + + /** + * 查询用户未阅读的通知公告 + * @param userId + * @return + */ + public List getNotSendedAnnouncementlist(String userId); + + /** + * 添加访问次数 + * @param id + * @param count + */ + void updateVisitsNum(String id, int count); + + /** + * 批量下载文件 + * @param id + * @param request + * @param response + */ + void downLoadFiles(String id, HttpServletRequest request, HttpServletResponse response); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCategoryService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCategoryService.java new file mode 100644 index 0000000..de56753 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCategoryService.java @@ -0,0 +1,101 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.modules.system.entity.SysCategory; +import com.ghb.base.modules.system.model.TreeSelectModel; + +import java.util.List; +import java.util.Map; + +/** + * @Description: 分类字典 + * @Author: Ghb-boot + * @Date: 2019-05-29 + * @Version: V1.0 + */ +public interface ISysCategoryService extends IService { + + /**根节点父ID的值*/ + public static final String ROOT_PID_VALUE = "0"; + + /** + * 存在子节点 + */ + public static final String HAS_CHILD = "1"; + + /** + * 添加分类字典 + * @param sysCategory + */ + void addSysCategory(SysCategory sysCategory); + + /** + * 修改分类字典 + * @param sysCategory + */ + void updateSysCategory(SysCategory sysCategory); + + /** + * 根据父级编码加载分类字典的数据 + * @param pcode + * @return + * @throws GhbBootException + */ + public List queryListByCode(String pcode) throws GhbBootException; + + /** + * 根据pid查询子节点集合 + * @param pid + * @return + */ + public List queryListByPid(String pid); + + /** + * 根据pid查询子节点集合,支持查询条件 + * @param pid + * @param condition + * @return + */ + public List queryListByPid(String pid, Map condition); + + /** + * 根据code查询id + * @param code + * @return + */ + public String queryIdByCode(String code); + + /** + * 删除节点时同时删除子节点及修改父级节点 + * @param ids + */ + void deleteSysCategory(String ids); + + /** + * 分类字典控件数据回显[表单页面] + * + * @param ids + * @return + */ + List loadDictItem(String ids); + + /** + * 分类字典控件数据回显[表单页面] + * + * @param ids + * @param delNotExist 是否移除不存在的项,设为false如果某个key不存在数据库中,则直接返回key本身 + * @return + */ + List loadDictItem(String ids, boolean delNotExist); + + /** + * 【仅导入使用】分类字典控件反向翻译 + * + * @param names + * @param delNotExist 是否移除不存在的项,设为false如果某个key不存在数据库中,则直接返回key本身 + * @return + */ + List loadDictItemByNames(String names, boolean delNotExist); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCheckRuleService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCheckRuleService.java new file mode 100644 index 0000000..11ce28a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCheckRuleService.java @@ -0,0 +1,33 @@ +package com.ghb.base.modules.system.service; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysCheckRule; + +/** + * @Description: 编码校验规则 + * @Author: Ghb-boot + * @Date: 2020-02-04 + * @Version: V1.0 + */ +public interface ISysCheckRuleService extends IService { + + /** + * 通过 code 获取规则 + * + * @param ruleCode + * @return + */ + SysCheckRule getByCode(String ruleCode); + + + /** + * 通过用户设定的自定义校验规则校验传入的值 + * + * @param checkRule + * @param value + * @return 返回 null代表通过校验,否则就是返回的错误提示文本 + */ + JSONObject checkValue(SysCheckRule checkRule, String value); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCommentService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCommentService.java new file mode 100644 index 0000000..a46c535 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysCommentService.java @@ -0,0 +1,65 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysComment; +import com.ghb.base.modules.system.vo.SysCommentFileVo; +import com.ghb.base.modules.system.vo.SysCommentVO; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * @Description: 系统评论回复表 + * @Author: Ghb-boot + * @Date: 2022-07-19 + * @Version: V1.0 + */ +public interface ISysCommentService extends IService { + + + /** + * 保存评论 返回评论ID + * + * @param sysComment + */ + String saveOne(SysComment sysComment); + + /** + * 删除 + * + * @param id + */ + void deleteOne(String id); + + /** + * 根据表名和数据id查询表单评论及文件信息 + * + * @param sysComment + * @return + */ + List queryFormCommentInfo(SysComment sysComment); + + + /** + * 保存文件+评论 + * + * @param req + */ + void saveOneFileComment(HttpServletRequest req); + + + /** + * 查询当前表单的文件列表 + * + * @param tableName + * @param formDataId + * @return + */ + List queryFormFileList(String tableName, String formDataId); + /** + * app端 保存文件+评论 + * + * @param request + */ + void appSaveOneFileComment(HttpServletRequest request); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDataLogService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDataLogService.java new file mode 100644 index 0000000..a94f8d0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDataLogService.java @@ -0,0 +1,21 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysDataLog; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 数据日志service接口 + * @author: Ghb-boot + */ +public interface ISysDataLogService extends IService { + + /** + * 添加数据日志 + * @param tableName + * @param dataId + * @param dataContent + */ + public void addDataLog(String tableName, String dataId, String dataContent); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDataSourceService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDataSourceService.java new file mode 100644 index 0000000..5f1d846 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDataSourceService.java @@ -0,0 +1,36 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.system.entity.SysDataSource; + +/** + * @Description: 多数据源管理 + * @Author: Ghb-boot + * @Date: 2019-12-25 + * @Version: V1.0 + */ +public interface ISysDataSourceService extends IService { + + /** + * 添加数据源 + * @param sysDataSource + * @return + */ + Result saveDataSource(SysDataSource sysDataSource); + + /** + * 修改数据源 + * @param sysDataSource + * @return + */ + Result editDataSource(SysDataSource sysDataSource); + + + /** + * 删除数据源 + * @param id + * @return + */ + Result deleteDataSource(String id); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartPermissionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartPermissionService.java new file mode 100644 index 0000000..36046c6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartPermissionService.java @@ -0,0 +1,31 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysDepartPermission; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysPermissionDataRule; + +import java.util.List; + +/** + * @Description: 部门权限表 + * @Author: Ghb-boot + * @Date: 2020-02-11 + * @Version: V1.0 + */ +public interface ISysDepartPermissionService extends IService { + /** + * 保存授权 将上次的权限和这次作比较 差异处理提高效率 + * @param departId + * @param permissionIds + * @param lastPermissionIds + */ + public void saveDepartPermission(String departId,String permissionIds,String lastPermissionIds); + + /** + * 根据部门id,菜单id获取数据规则 + * @param permissionId 菜单id + * @param departId 部门id + * @return + */ + List getPermRuleListByDeptIdAndPermId(String departId,String permissionId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRolePermissionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRolePermissionService.java new file mode 100644 index 0000000..1ce8189 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRolePermissionService.java @@ -0,0 +1,20 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysDepartRolePermission; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 部门角色权限 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +public interface ISysDepartRolePermissionService extends IService { + /** + * 保存授权 将上次的权限和这次作比较 差异处理提高效率 + * @param roleId + * @param permissionIds + * @param lastPermissionIds + */ + public void saveDeptRolePermission(String roleId,String permissionIds,String lastPermissionIds); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRoleService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRoleService.java new file mode 100644 index 0000000..2c85dcf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRoleService.java @@ -0,0 +1,29 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysDepartRole; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** + * @Description: 部门角色 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +public interface ISysDepartRoleService extends IService { + + /** + * 根据用户id,部门id查询可授权所有部门角色 + * @param orgCode + * @param userId + * @return + */ + List queryDeptRoleByDeptAndUser(String orgCode, String userId); + + /** + * 删除部门角色和对应关联表信息 + * @param ids + */ + void deleteDepartRole(List ids); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRoleUserService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRoleUserService.java new file mode 100644 index 0000000..34f2b07 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartRoleUserService.java @@ -0,0 +1,30 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysDepartRoleUser; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** + * @Description: 部门角色人员信息 + * @Author: Ghb-boot + * @Date: 2020-02-13 + * @Version: V1.0 + */ +public interface ISysDepartRoleUserService extends IService { + + /** + * 添加用户与部门关联 + * @param userId 用户id + * @param newRoleId 新的角色id + * @param oldRoleId 旧的角色id + */ + void deptRoleUserAdd(String userId,String newRoleId,String oldRoleId); + + /** + * 取消用户与部门关联,删除关联关系 + * @param userIds + * @param depId + */ + void removeDeptRoleUser(List userIds,String depId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartService.java new file mode 100644 index 0000000..91d010f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDepartService.java @@ -0,0 +1,326 @@ +package com.ghb.base.modules.system.service; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.model.DepartIdModel; +import com.ghb.base.modules.system.model.SysDepartTreeModel; +import com.ghb.base.modules.system.vo.SysChangeDepartVo; +import com.ghb.base.modules.system.vo.SysDepartExportVo; +import com.ghb.base.modules.system.vo.SysPositionSelectTreeVo; +import com.ghb.base.modules.system.vo.lowapp.ExportDepartVo; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + *

+ * 部门表 服务实现类 + *

+ * + * @Author:Steve + * @Since: 2019-01-22 + */ +public interface ISysDepartService extends IService{ + + /** + * 查询我的部门信息,并分节点进行显示 + * @param departIds 部门id + * @return + */ + List queryMyDeptTreeList(String departIds); + + /** + * 查询所有部门信息,并分节点进行显示 + * @return + */ + List queryTreeList(); + + + /** + * 查询所有部门信息,并分节点进行显示 + * @param ids 多个部门id + * @return + */ + List queryTreeList(String ids); + + /** + * 查询所有部门DepartId信息,并分节点进行显示 + * @return + */ + public List queryDepartIdTreeList(); + + /** + * 保存部门数据 + * @param sysDepart + * @param username 用户名 + */ + void saveDepartData(SysDepart sysDepart,String username); + + /** + * 更新depart数据 + * @param sysDepart + * @param username 用户名 + * @return + */ + Boolean updateDepartDataById(SysDepart sysDepart,String username); + + /** + * 删除depart数据 + * @param id + * @return + */ + /* boolean removeDepartDataById(String id); */ + + /** + * 根据关键字搜索相关的部门数据 + * + * @param keyWord + * @param myDeptSearch + * @param departIds 多个部门id + * @param orgCategory + * @param depIds + * @return + */ + List searchByKeyWord(String keyWord, String myDeptSearch, String departIds, String orgCategory, String depIds); + + /** + * 根据部门id删除并删除其可能存在的子级部门 + * @param id + * @return + */ + boolean delete(String id); + + /** + * 查询SysDepart集合 + * @param userId + * @return + */ + public List queryUserDeparts(String userId); + + /** + * 根据用户名查询部门 + * + * @param username + * @return + */ + List queryDepartsByUsername(String username); + + /** + * 根据用户ID查询部门 + * + * @param userId + * @return + */ + List queryDepartsByUserId(String userId); + + /** + * 根据 用户ID 查询部门ID列表 + * + * @param userIds + * @return key = 用户ID, value = 部门ID列表 + */ + Map> queryDepartIdsByUserIds(Collection userIds); + + /** + * 根据部门id批量删除并删除其可能存在的子级部门 + * @param ids 多个部门id + * @return + */ + void deleteBatchWithChildren(List ids); + + /** + * 根据部门Id查询,当前和下级所有部门IDS + * @param departId + * @return + */ + List getSubDepIdsByDepId(String departId); + + /** + * 获取我的部门下级所有部门IDS + * @param departIds 多个部门id + * @return + */ + List getMySubDepIdsByDepId(String departIds); + /** + * 根据关键字获取部门信息(通讯录) + * @param keyWord 搜索词 + * @return + */ + List queryTreeByKeyWord(String keyWord); + /** + * 获取我的部门下级所有部门 + * @param parentId 父id + * @param ids 多个部门id + * @param primaryKey 主键字段(id或者orgCode) + * @param orgCategory 逗号分隔的 orgCategory 值,如 "1,2";为空时退化为默认行为(排除岗位) + * @return + */ + List queryTreeListByPid(String parentId,String ids, String primaryKey, String orgCategory); + + /** + * 获取某个部门的所有父级部门的ID + * + * @param departId 根据departId查 + * @return JSONObject + */ + JSONObject queryAllParentIdByDepartId(String departId); + + /** + * 获取某个部门的所有父级部门的ID + * + * @param orgCode 根据orgCode查 + * @return JSONObject + */ + JSONObject queryAllParentIdByOrgCode(String orgCode); + /** + * 获取公司信息 + * @param orgCode 部门编码 + * @return + */ + SysDepart queryCompByOrgCode(String orgCode); + /** + * 获取下级部门 + * @param pid + * @return + */ + List queryDeptByPid(String pid); + + /** + * 获取我的部门已加入的公司 + * @return + */ + List getMyDepartList(); + + /** + * 删除部门 + * @param id + */ + void deleteDepart(String id); + + /** + * 通讯录通过租户id查询部门数据 + * @param parentId + * @param tenantId + * @param departName + * @return + */ + List queryBookDepTreeSync(String parentId, Integer tenantId, String departName); + + /** + * 根据id查询部门信息 + * @param parentId + * @return + */ + SysDepart getDepartById(String parentId); + + /** + * 根据id查询部门信息 + * @param parentId + * @return + */ + IPage getMaxCodeDepart(Page page, String parentId); + + /** + * 更新叶子节点 + * @param id + * @param izLeaf + */ + void updateIzLeaf(String id, Integer izLeaf); + + /** + * 获取导出部门的数据 + * @param tenantId + * @return + */ + List getExcelDepart(int tenantId); + + void importExcel(List listSysDeparts, List errorMessageList); + + /** + * 根据租户id导出部门 + * @param tenantId + * @param idList + * @return + */ + List getExportDepart(Integer tenantId, List idList); + + /** + * 导出系统部门excel + * @param listSysDeparts + * @param errorMessageList + */ + void importSysDepart(List listSysDeparts, List errorMessageList); + + /** + * 根据部门id和职级id获取岗位信息 + * + * @param parentId + * @param departId + * @param positionId + */ + List getPositionByDepartId(String parentId, String departId, String positionId); + + /** + * 获取职级关系 + * @param departId + * @return + */ + List getRankRelation(String departId); + + /** + * 异步查询部门和岗位接口 + * + * @param parentId + * @param ids + * @param primaryKey + * @param departIds + * @return + */ + List queryDepartAndPostTreeSync(String parentId, String ids, String primaryKey, String departIds,String orgName); + + /** + * 根据部门code获取当前和上级部门名称 + * + * @param orgCode + * @param depId + * @return + */ + String getDepartPathNameByOrgCode(String orgCode, String depId); + + /** + * 根据部门id获取部门下的岗位id + * + * @param depIds 当前选择的公司、子公司、部门id + * @return + */ + String getDepPostIdByDepId(String depIds); + + /** + * 调整部门位置 + * + * @param changeDepartVo + * @return + */ + void updateChangeDepart(SysChangeDepartVo changeDepartVo); + + /** + * 获取部门负责人 + * + * @param departId + * @param page + * @return + */ + IPage getDepartmentHead(String departId, Page page); + + /** + * 获取所有职级关系 + * @param departId + * @return + */ + List getALLRankRelation(String departId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDictItemService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDictItemService.java new file mode 100644 index 0000000..efd5aef --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDictItemService.java @@ -0,0 +1,24 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysDictItem; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** + *

+ * 服务类 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +public interface ISysDictItemService extends IService { + + /** + * 通过字典id查询字典项 + * @param mainId 字典id + * @return + */ + public List selectItemsByMainId(String mainId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDictService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDictService.java new file mode 100644 index 0000000..cee516c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysDictService.java @@ -0,0 +1,301 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.common.system.vo.DictModel; +import com.ghb.base.common.system.vo.DictQuery; +import com.ghb.base.modules.system.entity.SysDict; +import com.ghb.base.modules.system.entity.SysDictItem; +import com.ghb.base.modules.system.model.DuplicateCheckVo; +import com.ghb.base.modules.system.model.TreeSelectModel; +import com.ghb.base.modules.system.vo.lowapp.SysDictVo; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 字典表 服务类 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +public interface ISysDictService extends IService { + + /** + * 校验数据是否可用,不存在重复数据 + * + * @param duplicateCheckVo + * @return + */ + @Deprecated + public boolean duplicateCheckData(DuplicateCheckVo duplicateCheckVo); + + /** + * 通过字典code获取字典数据 + * @param code + * @return + */ + public List queryDictItemsByCode(String code); + + /** + * 查询有效的数据字典项 + * @param code + * @return + */ + List queryEnableDictItemsByCode(String code); + + /** + * 通过多个字典code获取字典数据 + * + * @param dictCodeList + * @return key = 字典code,value=对应的字典选项 + */ + Map> queryDictItemsByCodeList(List dictCodeList); + + /** + * 登录加载系统字典 + * @return + */ + public Map> queryAllDictItems(); + + /** + * 查通过查询指定table的 text code 获取字典 + * @param tableFilterSql + * @param text + * @param code + * @return + */ + @Deprecated + List queryTableDictItemsByCode(String tableFilterSql, String text, String code); + + /** + * 通过查询指定table的 text code 获取字典(指定查询条件) + * @param table + * @param text + * @param code + * @param filterSql + * @return + */ + @Deprecated + public List queryTableDictItemsByCodeAndFilter(String table, String text, String code, String filterSql); + + /** + * 通过字典code及字典项的value获取字典文本 + * @param code + * @param key + * @return + */ + public String queryDictTextByKey(String code, String key); + + /** + * 可通过多个字典code查询翻译文本 + * @param dictCodeList 多个字典code + * @param keys 数据列表 + * @return + */ + Map> queryManyDictByKeys(List dictCodeList, List keys); + + /** + * 通过查询指定table的 text code key 获取字典值 + * @param table + * @param text + * @param code + * @param key + * @return + */ + @Deprecated + String queryTableDictTextByKey(String table, String text, String code, String key); + + /** + * 通过查询指定table的 text code key 获取字典值,可批量查询 + * + * @param table + * @param text + * @param code + * @param keys + * @param dataSource 数据源 + * @return + */ + List queryTableDictTextByKeys(String table, String text, String code, List keys, String dataSource); + + /** + * 通过查询指定table的 text code key 获取字典值,包含value + * @param table 表名 + * @param text + * @param code + * @param keys + * @return + */ + @Deprecated + List queryTableDictByKeys(String table, String text, String code, String keys); + + /** + * 通过查询指定table的 text code key 获取字典值,包含value + * @param table + * @param text + * @param code + * @param keys + * @param delNotExist + * @return + */ + @Deprecated + List queryTableDictByKeys(String table, String text, String code, String keys,boolean delNotExist); + + /** + * 根据字典类型删除关联表中其对应的数据 + * + * @param sysDict + * @return + */ + boolean deleteByDictId(SysDict sysDict); + + /** + * 添加一对多 + * @param sysDict + * @param sysDictItemList + * @return Integer + */ + public Integer saveMain(SysDict sysDict, List sysDictItemList); + + /** + * 查询所有部门 作为字典信息 id -->value,departName -->text + * @return + */ + public List queryAllDepartBackDictModel(); + + /** + * 查询所有用户 作为字典信息 username -->value,realname -->text + * @return + */ + public List queryAllUserBackDictModel(); + +// /** +// * 通过关键字查询字典表 +// * @param table +// * @param text +// * @param code +// * @param keyword +// * @return +// */ +// @Deprecated +// public List queryTableDictItems(String table, String text, String code,String keyword); + + /** + * 查询字典表数据 只查询前10条 + * @param table + * @param text + * @param code + * @param keyword + * @param condition + * @param pageSize 每页条数 + * @return + */ + @Deprecated + public List queryLittleTableDictItems(String table, String text, String code, String condition, String keyword, int pageNo, int pageSize); + + /** + * 查询字典表所有数据 + * @param table + * @param text + * @param code + * @param condition + * @param keyword + * @return + */ + @Deprecated + public List queryAllTableDictItems(String table, String text, String code, String condition, String keyword); + /** + * 根据表名、显示字段名、存储字段名 查询树 + * @param table + * @param text + * @param code + * @param pidField + * @param pid + * @param hasChildField + * @param query + * @return + */ + @Deprecated + List queryTreeList(Map query,String table, String text, String code, String pidField,String pid,String hasChildField,int converIsLeafVal); + + /** + * 真实删除 + * @param id + */ + public void deleteOneDictPhysically(String id); + + /** + * 修改delFlag + * @param delFlag + * @param id + */ + public void updateDictDelFlag(int delFlag,String id); + + /** + * 查询被逻辑删除的数据 + * @return + */ + public List queryDeleteList(String tenantId); + + /** + * 分页查询 + * @param query + * @param pageSize + * @param pageNo + * @return + */ + @Deprecated + public List queryDictTablePageList(DictQuery query,int pageSize, int pageNo); + + /** + * 获取字典数据 + * @param dictCode 字典code + * @param dictCode 表名,文本字段,code字段 | 举例:sys_user,realname,id + * @return + */ + List getDictItems(String dictCode); + + /** + * 【JSearchSelectTag下拉搜索组件专用接口】 + * 大数据量的字典表 走异步加载 即前端输入内容过滤数据 + * + * @param dictCode 字典code格式:table,text,code + * @param keyword + * @param pageNo + * @param pageSize 每页条数 + * @return + */ + List loadDict(String dictCode, String keyword, Integer pageNo, Integer pageSize); + + /** + * 根据应用id获取字典列表和详情 + * @param lowAppId + * @return + */ + List getDictListByLowAppId(String lowAppId); + + /** + * 创建字典 + * @param sysDictVo + */ + String addDictByLowAppId(SysDictVo sysDictVo); + + /** + * 编辑字典 + * @param sysDictVo + */ + void editDictByLowAppId(SysDictVo sysDictVo); + + /** + * 还原逻辑删除 + * @param ids + */ + boolean revertLogicDeleted(List ids); + + /** + * 彻底删除数据 + * @param ids + */ + boolean removeLogicDeleted(List ids); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysFillRuleService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysFillRuleService.java new file mode 100644 index 0000000..5812aa8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysFillRuleService.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysFillRule; + +/** + * @Description: 填值规则 + * @Author: Ghb-boot + * @Date: 2019-11-07 + * @Version: V1.0 + */ +public interface ISysFillRuleService extends IService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysFormFileService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysFormFileService.java new file mode 100644 index 0000000..40e26a1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysFormFileService.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysFormFile; + +/** + * @Description: 表单评论文件 + * @Author: Ghb-boot + * @Date: 2022-07-21 + * @Version: V1.0 + */ +public interface ISysFormFileService extends IService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysGatewayRouteService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysGatewayRouteService.java new file mode 100644 index 0000000..b99ab88 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysGatewayRouteService.java @@ -0,0 +1,64 @@ +package com.ghb.base.modules.system.service; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysGatewayRoute; + +import java.util.List; + +/** + * @Description: gateway路由管理 + * @Author: Ghb-boot + * @Date: 2020-05-26 + * @Version: V1.0 + */ +public interface ISysGatewayRouteService extends IService { + + /** + * 添加所有的路由信息到redis + * @param key + */ + void addRoute2Redis(String key); + + /** + * 删除路由 + * @param id + */ + void deleteById(String id); + + /** + * 保存路由配置 + * @param array + */ + void updateAll(JSONObject array); + + /** + * 清空redis中的route信息 + */ + void clearRedis(); + + /** + * 还原逻辑删除 + * @param ids + */ + void revertLogicDeleted(List ids); + + /** + * 彻底删除 + * @param ids + */ + void deleteLogicDeleted(List ids); + + /** + * 复制路由 + * @param id + * @return + */ + SysGatewayRoute copyRoute(String id); + + /** + * 获取删除列表 + * @return + */ + List getDeletelist(); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysLogService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysLogService.java new file mode 100644 index 0000000..041fc9a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysLogService.java @@ -0,0 +1,56 @@ +package com.ghb.base.modules.system.service; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import com.ghb.base.modules.system.entity.SysLog; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 系统日志表 服务类 + *

+ * + * @Author zhangweijian + * @since 2018-12-26 + */ +public interface ISysLogService extends IService { + + /** + * 清空所有日志记录 + */ + public void removeAll(); + + /** + * 获取系统总访问次数 + * + * @return Long + */ + Long findTotalVisitCount(); + + /** + * 获取系统今日访问次数 + * @param dayStart + * @param dayEnd + * @return Long + */ + Long findTodayVisitCount(Date dayStart, Date dayEnd); + + /** + * 获取系统今日访问 IP数 + * @param dayStart 开始时间 + * @param dayEnd 结束时间 + * @return Long + */ + Long findTodayIp(Date dayStart, Date dayEnd); + + /** + * 首页:根据时间统计访问数量/ip数量 + * @param dayStart + * @param dayEnd + * @return + */ + List> findVisitCount(Date dayStart, Date dayEnd); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPackPermissionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPackPermissionService.java new file mode 100644 index 0000000..86bf0f7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPackPermissionService.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysPackPermission; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 产品包菜单关系表 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +public interface ISysPackPermissionService extends IService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPermissionDataRuleService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPermissionDataRuleService.java new file mode 100644 index 0000000..4f685cc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPermissionDataRuleService.java @@ -0,0 +1,56 @@ +package com.ghb.base.modules.system.service; + +import java.util.List; + +import com.ghb.base.modules.system.entity.SysPermissionDataRule; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 菜单权限规则 服务类 + *

+ * + * @Author huangzhilin + * @since 2019-04-01 + */ +public interface ISysPermissionDataRuleService extends IService { + + /** + * 根据菜单id查询其对应的权限数据 + * + * @param permissionId + * @return List + */ + List getPermRuleListByPermId(String permissionId); + + /** + * 根据页面传递的参数查询菜单权限数据 + * @param permRule + * @return + */ + List queryPermissionRule(SysPermissionDataRule permRule); + + + /** + * 根据菜单ID和用户名查找数据权限配置信息 + * @param permissionId + * @param username + * @return + */ + List queryPermissionDataRules(String username,String permissionId); + + /** + * 新增菜单权限配置 修改菜单rule_flag + * @param sysPermissionDataRule + */ + public void savePermissionDataRule(SysPermissionDataRule sysPermissionDataRule); + + /** + * 删除菜单权限配置 判断菜单还有无权限 + * @param dataRuleId + */ + public void deletePermissionDataRule(String dataRuleId); + + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPermissionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPermissionService.java new file mode 100644 index 0000000..f4e5064 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPermissionService.java @@ -0,0 +1,112 @@ +package com.ghb.base.modules.system.service; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.modules.system.entity.SysPermission; +import com.ghb.base.modules.system.model.TreeModel; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 菜单权限表 服务类 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +public interface ISysPermissionService extends IService { + /** + * 切换vue3菜单 + */ + public void switchVue3Menu(); + + /** + * 通过父id查询菜单 + * @param parentId 父id + * @return + */ + public List queryListByParentId(String parentId); + + /** + * 真实删除 + * @param id 菜单id + * @throws GhbBootException + */ + public void deletePermission(String id) throws GhbBootException; + /** + * 逻辑删除 + * @param id 菜单id + * @throws GhbBootException + */ + public void deletePermissionLogical(String id) throws GhbBootException; + + /** + * 添加菜单 + * @param sysPermission SysPermission对象 + * @throws GhbBootException + */ + public void addPermission(SysPermission sysPermission) throws GhbBootException; + + /** + * 编辑菜单 + * @param sysPermission SysPermission对象 + * @throws GhbBootException + */ + public void editPermission(SysPermission sysPermission) throws GhbBootException; + + /** + * 获取登录用户拥有的权限 + * @param username 用户名 + * @return + */ + public List queryByUser(String username); + + /** + * 根据permissionId删除其关联的SysPermissionDataRule表中的数据 + * + * @param id + * @return + */ + public void deletePermRuleByPermId(String id); + + /** + * 查询出带有特殊符号的菜单地址的集合 + * @return + */ + public List queryPermissionUrlWithStar(); + + /** + * 判断用户否拥有权限 + * @param username + * @param sysPermission + * @return + */ + public boolean hasPermission(String username, SysPermission sysPermission); + + /** + * 根据用户和请求地址判断是否有此权限 + * @param username + * @param url + * @return + */ + public boolean hasPermission(String username, String url); + + /** + * 查询部门权限数据 + * @param departId + * @return + */ + List queryDepartPermissionList(String departId); + + /** + * 检测地址是否存在(聚合路由的情况下允许使用子菜单路径作为父菜单的路由地址) + * @param id + * @param url + * @param alwaysShow 是否是聚合路由 + * @return + */ + boolean checkPermDuplication(String id, String url,Boolean alwaysShow); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPositionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPositionService.java new file mode 100644 index 0000000..133a9e7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysPositionService.java @@ -0,0 +1,46 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysPosition; +import com.ghb.base.modules.system.vo.SysPositionVO; + +import java.util.List; + +/** + * @Description: 职务表 + * @Author: Ghb-boot + * @Date: 2019-09-19 + * @Version: V1.0 + */ +public interface ISysPositionService extends IService { + + /** + * 通过code查询 + * @param code 职务编码 + * @return SysPosition + */ + SysPosition getByCode(String code); + + /** + * 通过用户id获取职位名称列表 + * @param userId + * @return + */ + List getPositionList(String userId); + + /** + * 获取职位名称 + * @param postList + * @return + */ + String getPositionName(List postList); + + /** + * 批量通过用户id列表查询职位VO(含userId字段,用于批量同步场景消除N+1查询) + * + * @param userIds 用户id列表 + * @return 职位VO列表(每条记录含userId字段,供调用方分组) + */ + List getPositionListByUserIds(List userIds); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRoleIndexService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRoleIndexService.java new file mode 100644 index 0000000..db2e91a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRoleIndexService.java @@ -0,0 +1,57 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysRoleIndex; + +/** + * @Description: 角色首页配置 + * @Author: Ghb-boot + * @Date: 2022-03-25 + * @Version: V1.0 + */ +public interface ISysRoleIndexService extends IService { + + /** + * 查询默认首页 + * + * @return + */ + SysRoleIndex queryDefaultIndex(); + + /** + * 更新默认首页 + * + * @param url + * @param component + * @param isRoute 是否是路由页面 + * @return + */ + boolean updateDefaultIndex(String url, String component, boolean isRoute); + + /** + * 创建最原始的默认首页配置 + * + * @return + */ + SysRoleIndex initDefaultIndex(); + + /** + * 清理默认首页的redis缓存 + */ + void cleanDefaultIndexCache(); + + /** + * 切换默认门户 + * @param sysRoleIndex + */ + void changeDefHome(SysRoleIndex sysRoleIndex); + + /** + * 更新其他全局默认的状态值 + * + * @param roleCode + * @param status + * @param id + */ + void updateOtherDefaultStatus(String roleCode, String status, String id); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRolePermissionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRolePermissionService.java new file mode 100644 index 0000000..43c84c4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRolePermissionService.java @@ -0,0 +1,31 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysRolePermission; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 角色权限表 服务类 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +public interface ISysRolePermissionService extends IService { + + /** + * 保存授权/先删后增 + * @param roleId + * @param permissionIds + */ + public void saveRolePermission(String roleId,String permissionIds); + + /** + * 保存授权 将上次的权限和这次作比较 差异处理提高效率 + * @param roleId + * @param permissionIds + * @param lastPermissionIds + */ + public void saveRolePermission(String roleId,String permissionIds,String lastPermissionIds); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRoleService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRoleService.java new file mode 100644 index 0000000..9e83174 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysRoleService.java @@ -0,0 +1,77 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import org.apache.ibatis.annotations.Param; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.system.entity.SysRole; +import com.ghb.base.modules.system.entity.SysUser; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + *

+ * 角色表 服务类 + *

+ * + * @Author scott + * @since 2018-12-19 + */ +public interface ISysRoleService extends IService { + /** + * 查询全部的角色(不做租户隔离) + * @param page + * @param role + * @return + */ + Page listAllSysRole(@Param("page") Page page, SysRole role); + + /** + * 查询角色是否存在不做租户隔离 + * + * @param roleCode + * @return + */ + SysRole getRoleNoTenant(@Param("roleCode") String roleCode); + + /** + * 导入 excel ,检查 roleCode 的唯一性 + * + * @param file + * @param params + * @return + * @throws Exception + */ + Result importExcelCheckRoleCode(MultipartFile file, ImportParams params) throws Exception; + + /** + * 删除角色 + * @param roleid + * @return + */ + public boolean deleteRole(String roleid); + + /** + * 批量删除角色 + * @param roleids + * @return + */ + public boolean deleteBatchRole(String[] roleids); + + /** + * 根据角色id和当前租户判断当前角色是否存在这个租户中 + * @param id + * @return + */ + Long getRoleCountByTenantId(String id, Integer tenantId); + + /** + * 验证是否为admin角色 + * + * @param ids + */ + void checkAdminRoleRejectDel(String ids); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTableWhiteListService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTableWhiteListService.java new file mode 100644 index 0000000..b694835 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTableWhiteListService.java @@ -0,0 +1,57 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysTableWhiteList; + +import java.util.Map; + +/** + * @Description: 系统表白名单 + * @Author: Ghb-boot + * @Date: 2023-09-12 + * @Version: V1.0 + */ +public interface ISysTableWhiteListService extends IService { + + /** + * 新增 + * + * @param sysTableWhiteList + * @return + */ + boolean add(SysTableWhiteList sysTableWhiteList); + + /** + * 编辑 + * + * @param sysTableWhiteList + * @return + */ + boolean edit(SysTableWhiteList sysTableWhiteList); + + /** + * 通过id删除,可批量删除 + * + * @param ids 多个使用逗号分割 + * @return + */ + boolean deleteByIds(String ids); + + /** + * 自动添加到数据库中 + * + * @param tableName + * @param fieldName + * @return + */ + SysTableWhiteList autoAdd(String tableName, String fieldName); + + /** + * 以map的方式获取所有数据 + * key=tableName,value=fieldName + * + * @return + */ + Map getAllConfigMap(); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTenantPackService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTenantPackService.java new file mode 100644 index 0000000..b847837 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTenantPackService.java @@ -0,0 +1,114 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysTenantPack; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysTenantPackUser; + +import java.util.List; + +/** + * @Description: 租户产品包 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +public interface ISysTenantPackService extends IService { + + /** + * 新增产品包并将菜单插入关系表 + * @param sysTenantPack + */ + void addPackPermission(SysTenantPack sysTenantPack); + + /** + * 设置菜单id + * @param records + * @return + */ + List setPermissions(List records); + + /** + * 编辑产品包并将菜单插入关系表 + * @param sysTenantPack + */ + void editPackPermission(SysTenantPack sysTenantPack); + + /** + * 删除租户产品包 + * @param ids + */ + void deleteTenantPack(String ids); + + /** + * 退出租户 + * @param tenantId + * @param s + */ + void exitTenant(String tenantId, String s); + + /** + * 创建租户的时候默认创建3个 产品包 + * @param tenantId + */ + void addDefaultTenantPack(Integer tenantId); + + /** + * 保存产品包 + * @param sysTenantPack + */ + String saveOne(SysTenantPack sysTenantPack); + + + /** + * 保存产品包和人员的关系 + * @param sysTenantPackUser + */ + void savePackUser(SysTenantPackUser sysTenantPackUser); + + /** + * 根据租户ID和编码查询 + * @param tenantId + * @param packCode + * @return + */ + SysTenantPack getSysTenantPack(Integer tenantId ,String packCode); + + /** + * 添加由管理员创建的默认产品包 + * @param id + */ + void addTenantDefaultPack(Integer id); + + /** + * 同步默认的套餐 + * for [QQYUN-11032]【Ghb】租户套餐管理增加初始化套餐包按钮 + * @param tenantId + * @author chenrui + * @date 2025/2/5 19:08 + */ + void syncDefaultPack(Integer tenantId); + + /** + * 根据用户id和当前的租户id获取产品包的id + * + * @param userId + * @param tenantId + * @return + */ + List getPackIdByUserIdAndTenantId(String userId, Integer tenantId); + + /** + * 根据租户id获取用户的产品包列表 + * + * @param tenantId + * @return + */ + List getPackListByTenantId(String tenantId); + + /** + * 是否为拥有管理用户权限【accountAdmin,superAdmin】 + * + * @param tenantId + */ + void izHaveManageUserAuth(String tenantId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTenantService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTenantService.java new file mode 100644 index 0000000..2faa9c8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysTenantService.java @@ -0,0 +1,250 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.system.entity.SysTenant; +import com.ghb.base.modules.system.entity.SysTenantPackUser; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.vo.tenant.TenantDepartAuthInfo; +import com.ghb.base.modules.system.vo.tenant.TenantPackModel; +import com.ghb.base.modules.system.vo.tenant.TenantPackUser; +import com.ghb.base.modules.system.vo.tenant.TenantPackUserCount; + +import java.util.Collection; +import java.util.List; + +/** + * @Description: 租户service接口 + * @author: Ghb-boot + */ +public interface ISysTenantService extends IService { + + /** + * 查询有效的租户 + * + * @param idList + * @return + */ + List queryEffectiveTenant(Collection idList); + + /** + * 返回某个租户被多少个用户引用了 + * + * @param id + * @return + */ + Long countUserLinkTenant(String id); + + /** + * 根据ID删除租户,会判断是否已被引用 + * + * @param id + * @return + */ + boolean removeTenantById(String id); + + /** + * 邀请用户加入租户,通过手机号 + * @param ids + * @param phone + * @param username + */ + void invitationUserJoin(String ids, String phone,String username); + + /** + * 请离用户(租户) + * @param userIds + * @param tenantId + */ + void leaveTenant(String userIds, String tenantId); + + /** + * 添加租户,并将创建的用户加入关系表 + * @param sysTenant + * @param userId + */ + Integer saveTenantJoinUser(SysTenant sysTenant, String userId); + + /** + * 保存租户 + * @param sysTenant + */ + void saveTenant(SysTenant sysTenant); + + /** + * 通过门牌号加入租户 + * @param sysTenant + * @param userId + */ + Integer joinTenantByHouseNumber(SysTenant sysTenant, String userId); + + /** + * 统计一个人创建了多少个租户 + * + * @param userId + * @return + */ + Integer countCreateTenantNum(String userId); + + /** + * 获取租户回收站的数据 + * @param page + * @param sysTenant + * @return + */ + IPage getRecycleBinPageList(Page page, SysTenant sysTenant); + + /** + * 彻底删除租户 + * @param ids + */ + void deleteTenantLogic(String ids); + + /** + * 还原租户 + * @param ids + */ + void revertTenantLogic(String ids); + + /** + * 退出租户 + * @param userId + * @param userId + * @param username + */ + void exitUserTenant(String userId, String username, String tenantId); + + /** + * 变更租户拥有者 + * @param userId + * @param tenantId + */ + void changeOwenUserTenant(String userId, String tenantId); + + /** + * 邀请用户到租户。通过手机号匹配 + * @param phone + * @param departId + * @return + */ + Result invitationUser(String phone, String departId); + + /** + * 进入应用组织页面 查询用户是否有 超级管理员的权限 + * @param tenantId + * @return + */ + TenantDepartAuthInfo getTenantDepartAuthInfo(Integer tenantId); + + + /** + * 获取 租户产品包-3个默认admin的人员数量 + * @param tenantId + * @return + */ + List queryTenantPackUserCount(Integer tenantId); + + /** + * 查询租户产品包信息 + * @param model + * @return + */ + TenantPackModel queryTenantPack(TenantPackModel model); + + /** + * 添加多个用户和产品包的关系数据 + * @param sysTenantPackUser + */ + void addBatchTenantPackUser(SysTenantPackUser sysTenantPackUser); + + /** + * 添加用户和产品包的关系数据 带日志记录的 + * @param sysTenantPackUser + */ + void addTenantPackUser(SysTenantPackUser sysTenantPackUser); + + /** + * 移除用户和产品包的关系数据 带日志记录的 + * @param sysTenantPackUser + */ + void deleteTenantPackUser(SysTenantPackUser sysTenantPackUser); + + + /** + * 查询申请的用户列表 + */ + List getTenantPackApplyUsers(Integer tenantId); + + /** + * 个人 申请成为管理员 + * @param sysTenantPackUser + */ + void doApplyTenantPackUser(SysTenantPackUser sysTenantPackUser); + + /** + * 申请通过 成为管理员 + * @param sysTenantPackUser + */ + void passApply(SysTenantPackUser sysTenantPackUser); + + /** + * 拒绝申请 成为管理员 + * @param sysTenantPackUser + */ + void deleteApply(SysTenantPackUser sysTenantPackUser); + + /** + * 产品包用户列表 + * @param tenantId + * @param packId + * @param status + * @param page + * @return + */ + IPage queryTenantPackUserList(String tenantId, String packId, Integer status, Page page); + + /** + * 查看是否已经申请过了管理员 + * @return + */ + Long getApplySuperAdminCount(); + + /** + * 发送同意或者拒绝消息 + * + * @param user + * @param content + */ + void sendMsgForAgreeAndRefuseJoin(SysUser user, String content); + + /** + * 根据密码删除当前用户 + * + * @param sysUser + * @param tenantId + */ + void deleteUserByPassword(SysUser sysUser, Integer tenantId); + + /** + * 根据用户id获取租户信息 + * @param userId + * @return + */ + List getTenantListByUserId(String userId); + + /** + * 删除用户 + * @param sysUser + * @param tenantId + */ + void deleteUser(SysUser sysUser, Integer tenantId); + + /** + * 为用户添加租户下所有套餐 + * @param userId + * @param tenantId + */ + void addPackUser(String userId, String tenantId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysThirdAccountService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysThirdAccountService.java new file mode 100644 index 0000000..0c10db0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysThirdAccountService.java @@ -0,0 +1,92 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysThirdAccount; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.model.ThirdLoginModel; + +import java.util.List; + +/** + * @Description: 第三方登录账号表 + * @Author: Ghb-boot + * @Date: 2020-11-17 + * @Version: V1.0 + */ +public interface ISysThirdAccountService extends IService { + /** + * 更新第三方账户信息 + * @param sysUser SysUser对象 + * @param thirdUserUuid 第三方id + */ + void updateThirdUserId(SysUser sysUser,String thirdUserUuid); + + /** + * 创建第三方用户 + * @param phone 手机号 + * @param thirdUserUuid 第三方id + * @return SysUser + */ + SysUser createUser(String phone, String thirdUserUuid, Integer tenantId); + + /** + * 根据本地userId查询数据 + * @param sysUserId 用户id + * @param thirdType 第三方登录类型 + * @return SysThirdAccount + */ + SysThirdAccount getOneBySysUserId(String sysUserId, String thirdType); + + /** + * 根据第三方userId查询数据 + * @param thirdUserId 第三方id + * @param thirdType 第三方登录类型 + * @return SysThirdAccount + */ + SysThirdAccount getOneByThirdUserId(String thirdUserId, String thirdType); + + /** + * 通过 sysUsername 集合批量查询 + * + * @param sysUsernameArr username集合 + * @param thirdType 第三方类型 + * @return + */ + List listThirdUserIdByUsername(String[] sysUsernameArr, String thirdType, Integer tenantId); + + /** + * 创建新用户 + * + * @param tlm 第三方登录信息 + * @return SysThirdAccount + * @return tenantId 租户id + */ + SysThirdAccount saveThirdUser(ThirdLoginModel tlm, Integer tenantId); + + /** + * 绑定第三方账号(登录后根据用户id绑定第三方账号) + * @param sysThirdAccount + */ + SysThirdAccount bindThirdAppAccountByUserId(SysThirdAccount sysThirdAccount); + + + /** + * 根据第三方 UUID和第三方类别获取第三方用户数据 + * @param unionid + * @param thirdType + * @param tenantId + * @param thirdUserId + * @return + */ + SysThirdAccount getOneByUuidAndThirdType(String unionid, String thirdType,Integer tenantId,String thirdUserId); + + /** + * 批量通过本地用户id列表查询第三方账号(用于全量同步批量预加载) + * + * @param sysUserIds 本地用户id列表 + * @param thirdType 第三方类型 + * @return 第三方账号列表 + */ + List listBySysUserIds(List sysUserIds, String thirdType); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysThirdAppConfigService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysThirdAppConfigService.java new file mode 100644 index 0000000..eea48d7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysThirdAppConfigService.java @@ -0,0 +1,37 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; + +import com.ghb.base.modules.system.entity.SysThirdAppConfig; + +import java.util.List; + +/** + * @Description: 第三方配置表 + * @Author: Ghb-boot + * @Date: 2023-02-03 + * @Version: V1.0 + */ +public interface ISysThirdAppConfigService extends IService{ + + /** + * 根据租户id获取钉钉/企业微信配置 + * @param tenantId + * @return + */ + List getThirdConfigListByThirdType(int tenantId); + + /** + * 根据租户id和第三方类别获取第三方配置 + * @param tenantId + * @param thirdType + * @return + */ + SysThirdAppConfig getThirdConfigByThirdType(Integer tenantId, String thirdType); + + /** + * 根据应用key获取第三方表配置 + * @param clientId + */ + List getThirdAppConfigByClientId(String clientId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUgroupService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUgroupService.java new file mode 100644 index 0000000..2ff8a41 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUgroupService.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysUgroup; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +/** + * @Description: 用户组表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +public interface ISysUgroupService extends IService { + + void deleteById(String id); + + void deleteByIds(List list); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUgroupUserService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUgroupUserService.java new file mode 100644 index 0000000..fa9dc01 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUgroupUserService.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.modules.system.entity.SysUgroupUser; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 用户组关系表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +public interface ISysUgroupUserService extends IService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserDepPostService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserDepPostService.java new file mode 100644 index 0000000..d136b5c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserDepPostService.java @@ -0,0 +1,13 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.entity.SysUserDepPost; + +/** + * @Description: 部门岗位用户表 + * @author: wangshuai + * @date: 2025/9/5 11:45 + */ +public interface ISysUserDepPostService extends IService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserDepartService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserDepartService.java new file mode 100644 index 0000000..ae80525 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserDepartService.java @@ -0,0 +1,103 @@ +package com.ghb.base.modules.system.service; + + +import java.util.List; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserDepart; +import com.ghb.base.modules.system.model.DepartIdModel; + + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * SysUserDpeart用户组织机构service + *

+ * @Author ZhiLin + * + */ +public interface ISysUserDepartService extends IService { + + + /** + * 根据指定用户id查询部门信息 + * @param userId + * @return + */ + List queryDepartIdsOfUser(String userId); + + + /** + * 根据部门id查询用户信息 + * @param depId + * @return + */ + List queryUserByDepId(String depId); + /** + * 根据部门code,查询当前部门和下级部门的用户信息 + * @param depCode 部门code + * @param realname 真实姓名 + * @return List + */ + List queryUserByDepCode(String depCode,String realname); + + /** + * 用户组件数据查询 + * @param departId + * @param username + * @param pageSize + * @param pageNo + * @param realname + * @param id + * @param isMultiTranslate 是否多字段翻译 + * @return + */ + IPage queryDepartUserPageList(String departId, String username, String realname, int pageSize, int pageNo,String id,String isMultiTranslate); + + /** + * 获取用户信息 + * @param tenantId + * @param departId + * @param keyword + * @param pageSize + * @param pageNo + * @return + */ + IPage getUserInformation(Integer tenantId, String departId, String keyword, Integer pageSize, Integer pageNo); + + /** + * 获取用户信息 + * @param tenantId + * @param departId + * @param roleId + * @param keyword + * @param pageSize + * @param pageNo + * @return + */ + IPage getUserInformation(Integer tenantId,String departId,String roleId, String keyword, Integer pageSize, Integer pageNo, String excludeUserIdList, String includeUsernameList); + + /** + * 通过部门id和租户id获取多个用户 + * @param departId + * @param tenantId + * @return + */ + List getUsersByDepartTenantId(String departId,Integer tenantId); + + /** + * 查询部门岗位下的用户 + * + * @param departId + * @param username + * @param realname + * @param pageSize + * @param pageNo + * @param id + * @param isMultiTranslate + * @return + */ + IPage queryDepartPostUserPageList(String departId, String username, String realname, Integer pageSize, Integer pageNo, String id, String isMultiTranslate); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserPositionService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserPositionService.java new file mode 100644 index 0000000..40dc261 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserPositionService.java @@ -0,0 +1,43 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserPosition; +import com.baomidou.mybatisplus.extension.service.IService; +/** + * @Description: 用户职位关系表 + * @Author: Ghb-boot + * @Date: 2023-02-14 + * @Version: V1.0 + */ +public interface ISysUserPositionService extends IService { + + /** + * 获取职位用户列表 + * @param page + * @param positionId + * @return + */ + IPage getPositionUserList(Page page, String positionId); + + /** + * 添加成员到用户职位关系表 + * @param userIds + * @param positionId + */ + void saveUserPosition(String userIds, String positionId); + + /** + * 通过职位id删除用户职位关系表 + * @param positionId + */ + void removeByPositionId(String positionId); + + /** + * 移除成员 + * @param userIds + * @param positionId + */ + void removePositionUser(String userIds, String positionId); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserRoleService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserRoleService.java new file mode 100644 index 0000000..3885a29 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserRoleService.java @@ -0,0 +1,18 @@ +package com.ghb.base.modules.system.service; + +import java.util.Map; + +import com.ghb.base.modules.system.entity.SysUserRole; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 用户角色表 服务类 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +public interface ISysUserRoleService extends IService { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserService.java new file mode 100644 index 0000000..216cb93 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserService.java @@ -0,0 +1,551 @@ +package com.ghb.base.modules.system.service; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.system.vo.SysUserCacheInfo; +import com.ghb.base.modules.system.entity.SysRoleIndex; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.model.SysUserSysDepPostModel; +import com.ghb.base.modules.system.model.SysUserSysDepartModel; +import com.ghb.base.modules.system.vo.SysUserExportVo; +import com.ghb.base.modules.system.vo.lowapp.DepartAndUserInfo; +import com.ghb.base.modules.system.vo.lowapp.UpdateDepartInfo; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + *

+ * 用户表 服务类 + *

+ * + * @Author scott + * @since 2018-12-20 + */ +public interface ISysUserService extends IService { + + /** + * 查询用户数据列表 + * + * @param req + * @param queryWrapper + * @param pageSize + * @param pageNo + * @return + */ + Result> queryPageList(HttpServletRequest req, QueryWrapper queryWrapper, Integer pageSize, Integer pageNo); + + /** + * 重置密码 + * + * @param username + * @param oldpassword + * @param newpassword + * @param confirmpassword + * @return + */ + public Result resetPassword(String username, String oldpassword, String newpassword, String confirmpassword); + + /** + * 修改密码 + * + * @param sysUser + * @return + */ + public Result changePassword(SysUser sysUser); + + /** + * 删除用户 + * @param userId + * @return + */ + public boolean deleteUser(String userId); + + /** + * 批量删除用户 + * @param userIds + * @return + */ + public boolean deleteBatchUsers(String userIds); + + /** + * 根据用户名查询 + * @param username 用户名 + * @return SysUser + */ + public SysUser getUserByName(String username); + + /** + * 添加用户和用户角色关系 + * @param user + * @param roles + */ + public void addUserWithRole(SysUser user,String roles); + + + /** + * 修改用户和用户角色关系 + * @param user + * @param roles + */ + public void editUserWithRole(SysUser user,String roles); + + /** + * 获取用户的授权角色 + * @param username + * @return + */ + public List getRole(String username); + + /** + * 获取根据登录用户的角色获取动态首页 + * + * @param username + * @param version 前端UI版本 + * @return + */ + public SysRoleIndex getDynamicIndexByUserRole(String username,String version); + + /** + * 查询用户信息包括 部门信息 + * @param username + * @return + */ + @Deprecated + public SysUserCacheInfo getCacheUser(String username); + + /** + * 根据部门Id查询 + * @param page + * @param departId 部门id + * @param username 用户账户名称 + * @return + */ + public IPage getUserByDepId(Page page, String departId, String username); + + /** + * 根据部门Ids查询 + * @param page + * @param departIds 部门id集合 + * @param username 用户账户名称 + * @return + */ + public IPage getUserByDepIds(Page page, List departIds, String username); + + /** + * 根据 userIds查询,查询用户所属部门的名称(多个部门名逗号隔开) + * @param userIds + * @return + */ + public Map getDepNamesByUserIds(List userIds); + + /** + * 根据部门 Id 和 QueryWrapper 查询 + * + * @param page + * @param departId + * @param queryWrapper + * @return + */ + // public IPage getUserByDepartIdAndQueryWrapper(Page page, String departId, QueryWrapper queryWrapper); + + /** + * 根据 orgCode 查询用户,包括子部门下的用户 + * + * @param orgCode + * @param userParams 用户查询条件,可为空 + * @param page 分页参数 + * @return + */ + IPage queryUserByOrgCode(String orgCode, SysUser userParams, IPage page); + + /** + * 根据角色Id查询 + * @param page + * @param roleId 角色id + * @param username 用户账户 + * @param realname 用户姓名 + * @return + */ + public IPage getUserByRoleId(Page page,String roleId, String username, String realname); + + /** + * 通过用户名获取用户角色集合 + * + * @param username 用户名 + * @return 角色集合 + */ + Set getUserRolesSet(String username); + + /** + * 通过用户名获取用户角色集合 + * + * @param userId 用户id + * @return 角色集合 + */ + Set getUserRoleSetById(String userId); + + /** + * 通过用户名获取用户权限集合 + * + * @param userId 用户id + * @return 权限集合 + */ + Set getUserPermissionsSet(String userId); + + /** + * 根据用户名设置部门ID + * @param username + * @param orgCode + */ + void updateUserDepart(String username,String orgCode,Integer loginTenantId); + + /** + * 根据手机号获取用户名和密码 + * @param phone 手机号 + * @return SysUser + */ + public SysUser getUserByPhone(String phone); + + + /** + * 根据邮箱获取用户 + * @param email 邮箱 + * @return SysUser + */ + public SysUser getUserByEmail(String email); + + + /** + * 添加用户和用户部门关系 + * @param user + * @param selectedParts + */ + void addUserWithDepart(SysUser user, String selectedParts); + + /** + * 编辑用户和用户部门关系 + * @param user + * @param departs + */ + void editUserWithDepart(SysUser user, String departs); + + /** + * 校验用户是否有效 + * @param sysUser + * @return + */ + Result checkUserIsEffective(SysUser sysUser); + + /** + * 查询被逻辑删除的用户 + * @return List + */ + List queryLogicDeleted(); + + /** + * 查询被逻辑删除的用户(可拼装查询条件) + * @param wrapper + * @return List + */ + List queryLogicDeleted(LambdaQueryWrapper wrapper); + + /** + * 还原被逻辑删除的用户 + * @param userIds 存放用户id集合 + * @param updateEntity + * @return boolean + */ + boolean revertLogicDeleted(List userIds, SysUser updateEntity); + + /** + * 彻底删除被逻辑删除的用户 + * @param userIds 存放用户id集合 + * @return boolean + */ + boolean removeLogicDeleted(List userIds); + + /** + * 更新手机号、邮箱空字符串为 null + * @return boolean + */ + @Transactional(rollbackFor = Exception.class) + boolean updateNullPhoneEmail(); + + /** + * 保存第三方用户信息 + * @param sysUser + */ + void saveThirdUser(SysUser sysUser); + + /** + * 根据部门Ids查询 + * @param departIds 部门id集合 + * @param username 用户账户名称 + * @return + */ + List queryByDepIds(List departIds, String username); + + /** + * 保存用户 + * + * @param user 用户 + * @param selectedRoles 选择的角色id,多个以逗号隔开 + * @param selectedDeparts 选择的部门id,多个以逗号隔开 + * @param relTenantIds 多个租户id + * @param izSyncPack 是否需要同步租户套餐包 + */ + void saveUser(SysUser user, String selectedRoles, String selectedDeparts, String relTenantIds, boolean izSyncPack); + + /** + * 编辑用户 + * @param user 用户 + * @param roles 选择的角色id,多个以逗号隔开 + * @param departs 选择的部门id,多个以逗号隔开 + * @param relTenantIds 多个租户id + * @param updateFromPage 更新来自的页面 [TV360X-1686] + */ + void editUser(SysUser user, String roles, String departs, String relTenantIds, String updateFromPage); + + /** + * userId转为username + * @param userIdList + * @return List + */ + List userIdToUsername(Collection userIdList); + + + /** + * 获取用户信息 字段信息是加密后的 【加密用户信息】 + * @param username + * @return + */ + LoginUser getEncodeUserInfo(String username); + + /** + * 用户离职 + * @param username + */ + void userQuit(String username); + + /** + * 获取离职人员列表 + * @param tenantId 租户id + * @return + */ + List getQuitList(Integer tenantId); + + /** + * 更新刪除状态和离职状态 + * @param userIds 存放用户id集合 + * @param sysUser + * @return boolean + */ + void updateStatusAndFlag(List userIds, SysUser sysUser); + + /** + * 设置登录租户 + * @param sysUser + * @return + */ + Result setLoginTenant(SysUser sysUser, JSONObject obj, String username, Result result); + + //--- author:taoyan date:20221231 for: QQYUN-3515【应用】应用下的组织机构管理功能,细节实现 --- + /** + * 批量编辑用户信息 + * @param json + */ + void batchEditUsers(JSONObject json); + + /** + * 根据关键词查询用户和部门 + * @param keyword + * @return + */ + DepartAndUserInfo searchByKeyword(String keyword); + + /** + * 查询 部门修改的信息 + * @param departId + * @return + */ + UpdateDepartInfo getUpdateDepartInfo(String departId); + + /** + * 修改部门相关信息 + * @param updateDepartInfo + */ + void doUpdateDepartInfo(UpdateDepartInfo updateDepartInfo); + + /** + * 设置负责人 取消负责人 + * @param json + */ + void changeDepartChargePerson(JSONObject json); + //--- author:taoyan date:20221231 for: QQYUN-3515【应用】应用下的组织机构管理功能,细节实现 --- + + /** + * 编辑租户用户 + * @param sysUser + * @param tenantId + * @param departs + */ + void editTenantUser(SysUser sysUser, String tenantId, String departs, String roles); + +/** + * 修改用户账号状态 + * @param id 账号id + * @param status 账号状态 + */ + void updateStatus(String id, String status); + + /** + * 导出应用下的用户Excel + * @param request + * @return + */ + ModelAndView exportAppUser(HttpServletRequest request); + + /** + * 导入应用下的用户 + * @param request + * @return + */ + Result importAppUser(HttpServletRequest request); + + /** + * 验证用户是否为管理员 + * @param ids + */ + void checkUserAdminRejectDel(String ids); + + /** + * 修改手机号 + * + * @param json + * @param username + */ + void changePhone(JSONObject json, String username); + + /** + * 发送短信验证码 + * + * @param jsonObject + * @param username 用户名 + * @param ipAddress ip地址 + */ + void sendChangePhoneSms(JSONObject jsonObject, String username, String ipAddress); + + /** + * 发送注销用户手机号验证密码[敲敲云专用] + * @param jsonObject + * @param username + * @param ipAddress + */ + void sendLogOffPhoneSms(JSONObject jsonObject, String username, String ipAddress); + + /** + * 用户注销[敲敲云专用] + * @param jsonObject + * @param username + */ + void userLogOff(JSONObject jsonObject, String username); + + /** + * 获取部门和用户关系的导出信息 + * @param pageList + */ + List getDepartAndRoleExportMsg(List pageList); + + /** + * 导入用户 + * + * @param request + */ + Result importSysUser(HttpServletRequest request); + + /** + * 没有绑定手机号 直接修改密码 + * + * @param oldPassword + * @param password + * @param username + */ + void updatePasswordNotBindPhone(String oldPassword, String password, String username); + + /** + * 根据用户名称查询用户和部门信息 + * @param userName + * @return + */ + Map queryUserAndDeptByName(String userName); + + /** + * 查询部门、岗位下的用户 包括子部门下的用户 + * + * @param orgCode + * @param userParams + * @param page + * @return + */ + IPage queryDepartPostUserByOrgCode(String orgCode, SysUser userParams, IPage page); + + /** + * 根据 orgCode 查询用户信息(部门全路径,主岗位和兼职岗位的信息),包括公司、子公司、部门 + * + * @param orgCode + * @param userParams + * @param page + * @return + */ + IPage queryDepartUserByOrgCode(String orgCode, SysUser userParams, IPage page); + + /** + * 通讯录点击用户获取用户详情(包含用户基本信息、部门全路径、主岗位兼职岗位全路径) + * + * @param userId + * @return + */ + SysUserSysDepPostModel getUserDetailByUserId(String userId); + + /** + * 登录获取用户部门信息 + * @param jsonObject + * @return + */ + Result loginGetUserDeparts(JSONObject jsonObject); + + /** + * 根据用户名查询重置成系统密码 + * @param usernames + */ + void resetToSysPassword(String usernames); + + /** + * 更新用户设备ID + * @param clientId + * @param userId + */ + void updateClientId(String clientId,String userId); + + /** + * 根据用户组查询用户列表 + * @param page + * @param groupId + * @param username + * @param realname + * @return + */ + IPage getUserByUgroupId(Page page, String groupId, String username, String realname); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserTenantService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserTenantService.java new file mode 100644 index 0000000..14cb735 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/ISysUserTenantService.java @@ -0,0 +1,131 @@ +package com.ghb.base.modules.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ghb.base.modules.system.entity.SysTenant; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserTenant; +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.system.vo.SysUserTenantVo; + +import java.util.List; + +/** + * @Description: sys_user_tenant_relation + * @Author: Ghb-boot + * @Date: 2022-12-23 + * @Version: V1.0 + */ +public interface ISysUserTenantService extends IService { + + /** + * 通过租户id获取数据 + * @param page + * @param userTenantId + * @param user + * @return + */ + Page getPageUserList(Page page, Integer userTenantId, SysUser user); + + /** + * 设置租户id + * @param records + * @return + */ + List setUserTenantIds(List records); + + /** + * 获取租户id获取用户ids + * @param tenantId + * @return + */ + List getUserIdsByTenantId(Integer tenantId); + + /** + * 通过用户id获取租户ids + * @param userId + * @return + */ + List getTenantIdsByUserId(String userId); + + /** + * 通过用户id获取租户列表 + * @param userId + * @param userTenantStatus + * @return + */ + List getTenantListByUserId(String userId, List userTenantStatus); + + /** + * 更新用户租户状态 + * @param id + * @param tenantId + * @param userTenantStatus + */ + void updateUserTenantStatus(String id, String tenantId, String userTenantStatus); + + /** + * 联查用户和租户审核状态 + * @param page + * @param status 租户用户状态,默认为1正常 + * @param user + * @return + */ + IPage getUserTenantPageList(Page page, List status, SysUser user, Integer tenantId); + + /** + * 取消离职 + * @param userIds + * @param tenantId + */ + void putCancelQuit(List userIds, Integer tenantId); + + /** + * 验证用户是否已存在 + * @param userId + * @param tenantId + * @return + */ + Integer userTenantIzExist(String userId, Integer tenantId); + + /** + * 根据用户id获取我的租户 + * + * @param page + * @param userId + * @param userTenantStatus + * @param sysUserTenantVo + * @return + */ + IPage getTenantPageListByUserId(Page page, String userId, List userTenantStatus,SysUserTenantVo sysUserTenantVo); + + /** + * 同意加入租户 + * @param userId + * @param tenantId + */ + void agreeJoinTenant(String userId, Integer tenantId); + + /** + * 拒绝加入租户 + * @param userId + * @param tenantId + */ + void refuseJoinTenant(String userId, Integer tenantId); + + /** + * 根据用户id和租户id获取用户租户中间表信息 + * @param userId + * @param tenantId + * @return + */ + SysUserTenant getUserTenantByTenantId(String userId, Integer tenantId); + + /** + * 获取租户下的成员数量 + * @param tenantId + * @param tenantStatus + * @return + */ + Long getUserCount(Integer tenantId, String tenantStatus); +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/IThirdAppService.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/IThirdAppService.java new file mode 100644 index 0000000..5fbc8c0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/IThirdAppService.java @@ -0,0 +1,89 @@ +package com.ghb.base.modules.system.service; + +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.modules.system.vo.thirdapp.SyncInfoVo; + +import java.util.List; + +/** + * 第三方App对接 + * @author: Ghb-boot + */ +public interface IThirdAppService { + + /** + * 获取AccessToken + * @return String + */ + String getAccessToken(); + + /** + * 将本地部门同步到第三方App
+ * 同步方向:本地 --> 第三方APP + * 同步逻辑:
+ * 1. 先判断是否同步过,有则修改,无则创建;
+ * 2. 本地没有但第三方App里有则删除第三方App里的。 + * @param ids + * @return 成功返回true + */ + SyncInfoVo syncLocalDepartmentToThirdApp(String ids); + +// /** +// * 将第三方App部门同步到本地
+// * 同步方向:第三方APP --> 本地 +// * 同步逻辑:
+// * 1. 先判断是否同步过,有则修改,无则创建;
+// * 2. 本地没有但第三方App里有则删除第三方App里的。 +// * @param ids +// * @return 成功返回true +// */ +// SyncInfoVo syncThirdAppDepartmentToLocal(String ids); + + /** + * 将本地用户同步到第三方App
+ * 同步方向:本地 --> 第三方APP
+ * 同步逻辑:先判断是否同步过,有则修改、无则创建
+ * 注意:同步人员的状态,比如离职、禁用、逻辑删除等。 + * (特殊点:1、目前逻辑特意做的不删除用户,防止企业微信提前上线,用户已经存在,但是平台无此用户。 + * 企业微信支持禁用账号;钉钉不支持 + * 2、企业微信里面是手机号激活,只能用户自己改,不允许通过接口改) + * @param ids + * @return 成功返回空数组,失败返回错误信息 + */ + SyncInfoVo syncLocalUserToThirdApp(String ids); + +// /** +// * 将第三方App用户同步到本地
+// * 同步方向:第三方APP --> 本地
+// * 同步逻辑:先判断是否同步过,有则修改、无则创建
+// * 注意:同步人员的状态,比如离职、禁用、逻辑删除等。 +// * +// * @return 成功返回空数组,失败返回错误信息 +// */ +// SyncInfoVo syncThirdAppUserToLocal(); + + /** + * 根据本地用户ID,删除第三方APP的用户 + * + * @param userIdList 本地用户ID列表 + * @return 0表示成功,其他值表示失败 + */ + int removeThirdAppUser(List userIdList); + + /** + * 发送消息 + * + * @param message + * @param verifyConfig 是否验证配置(未启用的APP会拒绝发送) + * @return + */ + boolean sendMessage(MessageDTO message, boolean verifyConfig); + + /** + * 发送消息 + * @param message + * @return boolean + */ + boolean sendMessage(MessageDTO message); + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ImportFileServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ImportFileServiceImpl.java new file mode 100644 index 0000000..1f3af41 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ImportFileServiceImpl.java @@ -0,0 +1,37 @@ +package com.ghb.base.modules.system.service.impl; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.util.CommonUtils; +import org.jeecgframework.poi.excel.imports.base.ImportFileServiceI; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * excel导入 实现类 + * @author: Ghb-boot + */ +@Slf4j +@Service +public class ImportFileServiceImpl implements ImportFileServiceI { + + @Value("${ghb.path.upload}") + private String upLoadPath; + + @Value(value="${ghb.uploadType}") + private String uploadType; + + @Override + public String doUpload(byte[] data) { + return CommonUtils.uploadOnlineImage(data, upLoadPath, "import", uploadType); + } + + @Override + public String doUpload(byte[] data, String saveUrl) { + // 代码逻辑说明: [QQYUN-10902]AutoPoi Excel表格导入有问题,还会报个错。 #7703------------ + String bizPath = "import"; + if(null != saveUrl && !saveUrl.isEmpty()){ + bizPath = saveUrl; + } + return CommonUtils.uploadOnlineImage(data, upLoadPath, bizPath, uploadType); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysAnnouncementSendServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysAnnouncementSendServiceImpl.java new file mode 100644 index 0000000..fc17843 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysAnnouncementSendServiceImpl.java @@ -0,0 +1,100 @@ +package com.ghb.base.modules.system.service.impl; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import jakarta.annotation.Resource; + +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysAnnouncement; +import com.ghb.base.modules.system.entity.SysAnnouncementSend; +import com.ghb.base.modules.system.mapper.SysAnnouncementMapper; +import com.ghb.base.modules.system.mapper.SysAnnouncementSendMapper; +import com.ghb.base.modules.system.model.AnnouncementSendModel; +import com.ghb.base.modules.system.service.ISysAnnouncementSendService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: 用户通告阅读标记表 + * @Author: Ghb-boot + * @Date: 2019-02-21 + * @Version: V1.0 + */ +@Service +public class SysAnnouncementSendServiceImpl extends ServiceImpl implements ISysAnnouncementSendService { + + @Resource + private SysAnnouncementSendMapper sysAnnouncementSendMapper; + + @Autowired + private SysAnnouncementMapper sysAnnouncementMapper; + + @Override + public Page getMyAnnouncementSendPage(Page page, + AnnouncementSendModel announcementSendModel) { + return page.setRecords(sysAnnouncementSendMapper.getMyAnnouncementSendList(page, announcementSendModel)); + } + + @Override + public AnnouncementSendModel getOne(String sendId) { + return sysAnnouncementSendMapper.getOne(sendId); + } + + /** + * 获取当前用户已阅读数量 + * + * @param id + * @return + */ + @Override + public long getReadCountByUserId(String id) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + return sysAnnouncementSendMapper.getReadCountByUserId(id, sysUser.getId()); + } + + /** + * 根据多个id批量删除已阅读的数量 + * + * @param ids + */ + @Override + public void deleteBatchByIds(String ids) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //根据用户id和阅读表的id获取所有阅读的数据 + List sendIds = sysAnnouncementSendMapper.getReadAnnSendByUserId(Arrays.asList(ids.split(SymbolConstant.COMMA)),sysUser.getId()); + if(CollectionUtil.isNotEmpty(sendIds)){ + this.removeByIds(sendIds); + } + } + + /** + * 根据busId更新阅读状态 + * @param busId + * @param busType + */ + @Override + public boolean updateReadFlagByBusId(String busId, String busType) { + boolean updateFlag = false; + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + List unReadAnnouncementsIds = sysAnnouncementSendMapper.getUnReadAnnByBusAndUserId(busId,busType,userId); + if(CollectionUtil.isNotEmpty(unReadAnnouncementsIds)){ + sysAnnouncementSendMapper.updateReaded(userId, unReadAnnouncementsIds); + updateFlag = true; + } + return updateFlag; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysAnnouncementServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysAnnouncementServiceImpl.java new file mode 100644 index 0000000..05c7071 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysAnnouncementServiceImpl.java @@ -0,0 +1,327 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.io.IoUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.archivers.zip.Zip64Mode; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.FileDownloadUtils; +import com.ghb.base.common.util.filter.SsrfFileTypeFilter; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysAnnouncement; +import com.ghb.base.modules.system.entity.SysAnnouncementSend; +import com.ghb.base.modules.system.mapper.SysAnnouncementMapper; +import com.ghb.base.modules.system.mapper.SysAnnouncementSendMapper; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import com.ghb.base.modules.system.service.ISysAnnouncementSendService; +import com.ghb.base.modules.system.service.ISysAnnouncementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @Description: 系统通告表 + * @Author: Ghb-boot + * @Date: 2019-01-02 + * @Version: V1.0 + */ +@Service +@Slf4j +public class SysAnnouncementServiceImpl extends ServiceImpl implements ISysAnnouncementService { + /** + * 补数据改成后台模式 + */ + public static ExecutorService completeNoteThreadPool = new ThreadPoolExecutor(0, 1024, 60L, TimeUnit.SECONDS, new SynchronousQueue()); + + @Resource + private SysAnnouncementMapper sysAnnouncementMapper; + @Resource + private SysUserMapper sysUserMapper; + @Resource + private SysAnnouncementSendMapper sysAnnouncementSendMapper; + @Autowired + private ISysAnnouncementSendService sysAnnouncementSendService; + @Autowired + private GhbBaseConfig GhbBaseConfig; + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveAnnouncement(SysAnnouncement sysAnnouncement) { + if(sysAnnouncement.getMsgType().equals(CommonConstant.MSG_TYPE_ALL)) { + sysAnnouncementMapper.insert(sysAnnouncement); + }else { + // 1.插入通告表记录 + sysAnnouncementMapper.insert(sysAnnouncement); + // 2.插入用户通告阅读标记表记录 + String userId = sysAnnouncement.getUserIds(); + // 代码逻辑说明: [issues/5503]【公告】通知无法接收 + if(StringUtils.isNotBlank(userId) && userId.endsWith(",")){ + userId = userId.substring(0, (userId.length()-1)); + } + String[] userIds = userId.split(","); + String anntId = sysAnnouncement.getId(); + Date refDate = new Date(); + for(int i=0;i queryWrapper = new LambdaQueryWrapper(); + queryWrapper.eq(SysAnnouncementSend::getAnntId, anntId); + queryWrapper.eq(SysAnnouncementSend::getUserId, userIds[i]); + List announcementSends=sysAnnouncementSendMapper.selectList(queryWrapper); + if(announcementSends.size()<=0) { + SysAnnouncementSend announcementSend = new SysAnnouncementSend(); + announcementSend.setAnntId(anntId); + announcementSend.setUserId(userIds[i]); + announcementSend.setReadFlag(CommonConstant.NO_READ_FLAG); + announcementSend.setReadTime(refDate); + sysAnnouncementSendMapper.insert(announcementSend); + } + } + // 3. 删除多余通知用户数据 + Collection delUserIds = Arrays.asList(userIds); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); + queryWrapper.notIn(SysAnnouncementSend::getUserId, delUserIds); + queryWrapper.eq(SysAnnouncementSend::getAnntId, anntId); + sysAnnouncementSendMapper.delete(queryWrapper); + } + return true; + } + + /** + * 流程执行完成保存消息通知 + * @param title 标题 + * @param msgContent 信息内容 + */ + @Override + public void saveSysAnnouncement(String title, String msgContent) { + SysAnnouncement announcement = new SysAnnouncement(); + announcement.setTitile(title); + announcement.setMsgContent(msgContent); + announcement.setSender("Ghb BOOT"); + announcement.setPriority(CommonConstant.PRIORITY_L); + announcement.setMsgType(CommonConstant.MSG_TYPE_ALL); + announcement.setSendStatus(CommonConstant.HAS_SEND); + announcement.setSendTime(new Date()); + announcement.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); + sysAnnouncementMapper.insert(announcement); + } + + @Override + public Page querySysCementPageByUserId(Page page, String userId, String msgCategory, Integer tenantId, Date beginDate) { + if (page.getSize() == -1) { + return page.setRecords(sysAnnouncementMapper.querySysCementListByUserId(null, userId, msgCategory,tenantId,beginDate)); + } else { + return page.setRecords(sysAnnouncementMapper.querySysCementListByUserId(page, userId, msgCategory,tenantId,beginDate)); + } + } + + @Override + public Integer getUnreadMessageCountByUserId(String userId, Date beginDate, String noticeType) { + return sysAnnouncementMapper.getUnreadMessageCountByUserId(userId, beginDate, noticeType); + } + + @Override + public void completeAnnouncementSendInfo() { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + List announcementIds = this.getNotSendedAnnouncementlist(userId); + List sysAnnouncementSendList = new ArrayList<>(); + if (!CollectionUtils.isEmpty(announcementIds)) { + for (String commentId : announcementIds) { + SysAnnouncementSend announcementSend = new SysAnnouncementSend(); + announcementSend.setAnntId(commentId); + announcementSend.setUserId(userId); + announcementSend.setReadFlag(CommonConstant.NO_READ_FLAG); + sysAnnouncementSendList.add(announcementSend); + } + } + if (!CollectionUtils.isEmpty(sysAnnouncementSendList)) { + sysAnnouncementSendService.saveBatch(sysAnnouncementSendList); + } + } + + @Override + public void batchInsertSysAnnouncementSend(String commentId, Integer tenantId) { + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(tenantId)) { + log.info("补全公告与用户的关系数据,租户ID = {}", tenantId); + } else { + tenantId = null; + } + + List userIdList = sysUserMapper.getTenantUserIdList(tenantId); + List sysAnnouncementSendList = new ArrayList<>(); + if (!CollectionUtils.isEmpty(userIdList)) { + for (String userId : userIdList) { + SysAnnouncementSend announcementSend = new SysAnnouncementSend(); + announcementSend.setAnntId(commentId); + announcementSend.setUserId(userId); + announcementSend.setReadFlag(CommonConstant.NO_READ_FLAG); + sysAnnouncementSendList.add(announcementSend); + } + } + if (!CollectionUtils.isEmpty(sysAnnouncementSendList)) { + log.info("补全公告与用户的关系数据,sysAnnouncementSendList size = {}", sysAnnouncementSendList.size()); + sysAnnouncementSendService.saveBatch(sysAnnouncementSendList); + } + } + + @Override + public List querySysMessageList(int pageSize, int pageNo, String fromUser, String starFlag, String busType, String msgCategory, Date beginDate, Date endDate, String noticeType) { +// //1. 补全send表的数据 +// completeNoteThreadPool.execute(()->{ +// completeAnnouncementSendInfo(); +// }); + + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + log.debug(" 获取登录人 LoginUser id: {}", sysUser.getId()); + Page page = new Page(pageNo,pageSize); + List list = baseMapper.queryAllMessageList(page, sysUser.getId(), fromUser, starFlag, busType, msgCategory,beginDate, endDate, noticeType); + return list; + } + + @Override + public void updateReaded(List annoceIdList) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + sysAnnouncementSendMapper.updateReaded(sysUser.getId(), annoceIdList); + } + + @Override + public void clearAllUnReadMessage() { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + sysAnnouncementSendMapper.clearAllUnReadMessage(sysUser.getId()); + } + + /** + * 查询用户未读的通知公告,防止SQL注入写法调整 + * @param userId + * @return + */ + + @Override + public List getNotSendedAnnouncementlist(String userId) { + return sysAnnouncementMapper.getNotSendedAnnouncementlist(new Date(), userId); + } + + /** + * 更新访问量 + * @param id + * @param increaseCount + */ + @Override + public void updateVisitsNum(String id, int increaseCount) { + SysAnnouncement sysAnnouncement = sysAnnouncementMapper.selectById(id); + if (oConvertUtils.isNotEmpty(sysAnnouncement)) { + int visits = oConvertUtils.getInt(sysAnnouncement.getVisitsNum(), 0); + int totalValue = increaseCount + visits; + sysAnnouncement.setVisitsNum(totalValue); + sysAnnouncementMapper.updateById(sysAnnouncement); + log.info("通知公告:{} 访问次数+1,总访问数量:{}", sysAnnouncement.getTitile(), sysAnnouncement.getVisitsNum()); + } + } + + /** + * 批量下载文件 + * @param id + * @param request + * @param response + */ + @Override + public void downLoadFiles(String id, HttpServletRequest request, HttpServletResponse response) { + // 参数校验 + if (oConvertUtils.isEmpty(id)) { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + // 获取文章信息 + SysAnnouncement sysAnnouncement = this.baseMapper.selectById(id); + if (oConvertUtils.isEmpty(sysAnnouncement)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + //设置HTTP响应头:准备文件下载 + response.reset(); + response.setCharacterEncoding("utf-8"); + response.setContentType("application/force-download"); + ZipArchiveOutputStream zous = null; + try { + // 生成ZIP文件名:使用文章标题+时间戳避免重名 + String title = sysAnnouncement.getTitile() + new Date().getTime(); + String zipName = URLEncoder.encode( title + ".zip", "UTF-8").replaceAll("\\+", "%20"); + response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + zipName); + // 创建ZIP输出流:直接输出到HTTP响应流 + zous = new ZipArchiveOutputStream(response.getOutputStream()); + zous.setUseZip64(Zip64Mode.AsNeeded);// 支持大文件 + + // 批量下载文件 + String[] fileUrls = sysAnnouncement.getFiles().split(","); + // 遍历所有文件URL + for (int i = 0; i < fileUrls.length; i++) { + String fileUrl = fileUrls[i].trim(); + if (oConvertUtils.isEmpty(fileUrl)) { + continue; + } + // 【安全校验】防止路径遍历攻击 + SsrfFileTypeFilter.checkPathTraversal(fileUrl); + // 生成ZIP内文件名:避免重名,添加序号 + String fileName = FileDownloadUtils.generateFileName(fileUrl, i, fileUrls.length); + String uploadUrl = GhbBaseConfig.getPath().getUpload(); + // 下载单个文件并添加到ZIP + FileDownloadUtils.downLoadSingleFile(fileUrl,fileName,uploadUrl, zous); + } + // 完成ZIP写入 + zous.finish(); + // 刷新缓冲区确保数据发送 + response.flushBuffer(); + } catch (IOException e) { + log.error("文件下载失败"+e.getMessage(), e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } finally { + // 确保流关闭,防止资源泄漏 + IoUtil.close(zous); + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysBaseApiImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysBaseApiImpl.java new file mode 100644 index 0000000..b0854fb --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysBaseApiImpl.java @@ -0,0 +1,2270 @@ +package com.ghb.base.modules.system.service.impl; +import org.jeecg.common.constant.CacheConstant; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.aliyuncs.exceptions.ClientException; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.base.Joiner; +import com.jeecg.dingtalk.api.core.response.Response; +import freemarker.core.TemplateClassResolver; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.api.dto.DataLogDTO; +import com.ghb.base.common.api.dto.OnlineAuthDTO; +import com.ghb.base.common.api.dto.PushMessageDTO; +import com.ghb.base.common.api.dto.message.*; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.UrlMatchEnum; +import com.ghb.base.common.constant.*; +import com.ghb.base.common.constant.enums.*; +import com.ghb.base.common.desensitization.util.SensitiveInfoUtil; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.query.QueryCondition; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.query.QueryRuleEnum; +import com.ghb.base.common.system.vo.*; +import com.ghb.base.common.util.*; +import com.ghb.base.common.util.dynamic.db.FreemarkerParseFactory; +import com.ghb.base.config.firewall.SqlInjection.IDictTableWhiteListHandler; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.message.entity.SysMessageTemplate; +import com.ghb.base.modules.message.handle.impl.DdSendMsgHandle; +import com.ghb.base.modules.message.handle.impl.EmailSendMsgHandle; +import com.ghb.base.modules.message.handle.impl.QywxSendMsgHandle; +import com.ghb.base.modules.message.handle.impl.SystemSendMsgHandle; +import com.ghb.base.modules.message.service.ISysMessageTemplateService; +import com.ghb.base.modules.message.websocket.WebSocket; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.mapper.*; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.util.SecurityUtil; +import com.ghb.base.modules.system.vo.lowapp.SysDictVo; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.util.PathMatcher; + +import jakarta.annotation.Resource; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import javax.sql.DataSource; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Description: 底层共通业务API,提供其他独立模块调用 + * @Author: scott + * @Date:2019-4-20 + * @Version:V1.0 + */ +@Slf4j +@Service +public class SysBaseApiImpl implements ISysBaseAPI { + /** 当前系统数据库类型 */ + private static String DB_TYPE = ""; + + // uniapp 推送调用api地址 + @Value("${ghb.unicloud.pushUrl:}") + private String GhbPushUrl; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ISysMessageTemplateService sysMessageTemplateService; + @Resource + private SysUserMapper userMapper; + @Resource + private SysUserRoleMapper sysUserRoleMapper; + @Autowired + private ISysDepartService sysDepartService; + @Autowired + private ISysDictService sysDictService; + @Resource + private SysAnnouncementMapper sysAnnouncementMapper; + @Resource + private SysAnnouncementSendMapper sysAnnouncementSendMapper; + @Resource + private WebSocket webSocket; + @Resource + private SysRoleMapper roleMapper; + @Resource + private SysDepartMapper departMapper; + @Resource + private SysCategoryMapper categoryMapper; + @Autowired + private ISysDataSourceService dataSourceService; + @Autowired + private ISysUserDepartService sysUserDepartService; + @Autowired + private ISysUserDepPostService sysUserDepPostService; + @Resource + private SysPermissionMapper sysPermissionMapper; + @Autowired + private ISysPermissionDataRuleService sysPermissionDataRuleService; + @Autowired + private ThirdAppWechatEnterpriseServiceImpl wechatEnterpriseService; + @Autowired + private ThirdAppDingtalkServiceImpl dingtalkService; + @Autowired + ISysCategoryService sysCategoryService; + @Autowired + private ISysUserService sysUserService; + @Autowired + private ISysDataLogService sysDataLogService; + @Autowired + private ISysRoleService sysRoleService; + @Autowired + private ISysUserTenantService sysUserTenantService; + + @Autowired + private ISysUserRoleService sysUserRoleService; + + @Autowired + private ISysUserPositionService sysUserPositionService; + + @Autowired + private IDictTableWhiteListHandler dictTableWhiteListHandler; + + @Autowired + private ISysAnnouncementService sysAnnouncementService; + + @Override + //@SensitiveDecode + public LoginUser getUserByName(String username) { + // 代码逻辑说明: VUEN-1276 【v3流程图】测试bug 1、通过我发起的流程或者流程实例,查看历史,流程图预览问题 + if (oConvertUtils.isEmpty(username)) { + return null; + } + LoginUser user = sysUserService.getEncodeUserInfo(username); + + //相同类中方法间调用时脱敏解密 Aop会失效,获取用户信息太重要,此处采用原生解密方法,不采用@SensitiveDecodeAble注解方式 + try { + SensitiveInfoUtil.handlerObject(user, false); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + return user; + } + + + @Override + @Cacheable(cacheNames=CommonConstant.SYS_USER_ID_MAPPING_CACHE, key="#username") + public String getUserIdByName(String username) { + if (oConvertUtils.isEmpty(username)) { + return null; + } + String userId = userMapper.getUserIdByName(username); + return userId; + } + + + @Override + public String translateDictFromTable(String table, String text, String code, String key) { + return sysDictService.queryTableDictTextByKey(table, text, code, key); + } + + @Override + public String translateDict(String code, String key) { + return sysDictService.queryDictTextByKey(code, key); + } + + @Override + public List queryPermissionDataRule(String component, String requestPath, String username) { + List currentSyspermission = null; + if(oConvertUtils.isNotEmpty(component)) { + //1.通过注解属性pageComponent 获取菜单 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getDelFlag,0); + query.eq(SysPermission::getComponent, component); + currentSyspermission = sysPermissionMapper.selectList(query); + }else { + //1.直接通过前端请求地址查询菜单 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getMenuType,2); + query.eq(SysPermission::getDelFlag,0); + + // 代码逻辑说明: 解决参数顺序问题 + List allPossiblePaths = this.getOnlinePossiblePaths(requestPath); + log.debug("获取的菜单地址= {}", allPossiblePaths.toString()); + if(allPossiblePaths.size()==1){ + query.eq(SysPermission::getUrl, requestPath); + }else{ + query.in(SysPermission::getUrl, allPossiblePaths); + } + + currentSyspermission = sysPermissionMapper.selectList(query); + //2.未找到 再通过自定义匹配URL 获取菜单 + if(currentSyspermission==null || currentSyspermission.size()==0) { + //通过自定义URL匹配规则 获取菜单(实现通过菜单配置数据权限规则,实际上针对获取数据接口进行数据规则控制) + String userMatchUrl = UrlMatchEnum.getMatchResultByUrl(requestPath); + LambdaQueryWrapper queryQserMatch = new LambdaQueryWrapper(); + // 代码逻辑说明: online菜单如果配置成一级菜单 权限查询不到 取消menuType = 1 + //queryQserMatch.eq(SysPermission::getMenuType, 1); + queryQserMatch.eq(SysPermission::getDelFlag, 0); + queryQserMatch.eq(SysPermission::getUrl, userMatchUrl); + if(oConvertUtils.isNotEmpty(userMatchUrl)){ + currentSyspermission = sysPermissionMapper.selectList(queryQserMatch); + } + } + //3.未找到 再通过正则匹配获取菜单 + if(currentSyspermission==null || currentSyspermission.size()==0) { + //通过正则匹配权限配置 + String regUrl = getRegexpUrl(requestPath); + if(regUrl!=null) { + currentSyspermission = sysPermissionMapper.selectList(new LambdaQueryWrapper().eq(SysPermission::getMenuType,2).eq(SysPermission::getUrl, regUrl).eq(SysPermission::getDelFlag,0)); + } + } + } + if(currentSyspermission!=null && currentSyspermission.size()>0){ + List dataRules = new ArrayList(); + for (SysPermission sysPermission : currentSyspermission) { + // 代码逻辑说明: 数据权限规则编码不规范,项目存在相同包名和类名 #722 + List temp = sysPermissionDataRuleService.queryPermissionDataRules(username, sysPermission.getId()); + if(temp!=null && temp.size()>0) { + //dataRules.addAll(temp); + dataRules = oConvertUtils.entityListToModelList(temp,SysPermissionDataRuleModel.class); + } + } + return dataRules; + } + return null; + } + + /** + * 匹配前端传过来的地址 匹配成功返回正则地址 + * AntPathMatcher匹配地址 + *()* 匹配0个或多个字符 + *()**匹配0个或多个目录 + */ + private String getRegexpUrl(String url) { + List list = sysPermissionMapper.queryPermissionUrlWithStar(); + if(list!=null && list.size()>0) { + for (String p : list) { + PathMatcher matcher = new AntPathMatcher(); + if(matcher.match(p, url)) { + return p; + } + } + } + return null; + } + + @Override + public SysUserCacheInfo getCacheUser(String username) { + SysUserCacheInfo info = new SysUserCacheInfo(); + info.setOneDepart(true); + LoginUser user = this.getUserByName(username); + +// try { +// //相同类中方法间调用时脱敏@SensitiveDecodeAble解密 Aop失效处理 +// SensitiveInfoUtil.handlerObject(user, false); +// } catch (IllegalAccessException e) { +// e.printStackTrace(); +// } + + if(user!=null) { + info.setSysUserId(user.getId()); + info.setSysUserCode(user.getUsername()); + info.setSysUserName(user.getRealname()); + info.setSysOrgCode(user.getOrgCode()); + info.setSysOrgId(user.getOrgId()); + info.setSysRoleCode(user.getRoleCode()); + }else{ + return null; + } + //多部门支持in查询 + List list = departMapper.queryUserDeparts(user.getId()); + List sysMultiOrgCode = new ArrayList(); + if(list==null || list.size()==0) { + //当前用户无部门 + //sysMultiOrgCode.add("0"); + }else if(list.size()==1) { + sysMultiOrgCode.add(list.get(0).getOrgCode()); + }else { + info.setOneDepart(false); + for (SysDepart dpt : list) { + sysMultiOrgCode.add(dpt.getOrgCode()); + } + } + info.setSysMultiOrgCode(sysMultiOrgCode); + return info; + } + + @Override + public LoginUser getUserById(String id) { + if(oConvertUtils.isEmpty(id)) { + return null; + } + LoginUser loginUser = new LoginUser(); + SysUser sysUser = userMapper.selectById(id); + if(sysUser==null) { + return null; + } + BeanUtils.copyProperties(sysUser, loginUser); + //去掉用户敏感信息 + loginUser.setPassword(null); + loginUser.setRelTenantIds(null); + loginUser.setDepartIds(null); + return loginUser; + } + + @Override + public List getRolesByUsername(String username) { + return sysUserRoleMapper.getRoleByUserName(username); + } + + @Override + public List getRolesByUserId(String userId) { + return sysUserRoleMapper.getRoleCodeByUserId(userId); + } + + @Override + public List getDepartIdsByUsername(String username) { + List list = sysDepartService.queryDepartsByUsername(username); + List result = new ArrayList<>(list.size()); + for (SysDepart depart : list) { + result.add(depart.getId()); + } + return result; + } + + @Override + public List getDepartIdsByUserId(String userId) { + return sysDepartService.queryDepartsByUserId(userId); + } + + @Override + public Map> getDepartIdsByUserIds(Collection userIds) { + return sysDepartService.queryDepartIdsByUserIds(userIds); + } + + @Override + public Set getDepartParentIdsByUsername(String username) { + List list = sysDepartService.queryDepartsByUsername(username); + Set result = new HashSet<>(list.size()); + for (SysDepart depart : list) { + result.add(depart.getParentId()); + } + return result; + } + + @Override + public Set getDepartParentIdsByDepIds(Set depIds) { + LambdaQueryWrapper departQuery = new LambdaQueryWrapper().in(SysDepart::getId, depIds); + List departList = departMapper.selectList(departQuery); + + if(CollectionUtils.isEmpty(departList)){ + return null; + } + Set parentIds = departList.stream() + .map(SysDepart::getParentId) + .collect(Collectors.toSet()); + return parentIds; + } + + @Override + public List getDepartNamesByUsername(String username) { + List list = sysDepartService.queryDepartsByUsername(username); + List result = new ArrayList<>(list.size()); + for (SysDepart depart : list) { + result.add(depart.getDepartName()); + } + return result; + } + + @Override + @Cacheable(cacheNames = CacheConstant.SYS_USERS_CACHE, key = "#username + '::main_depart_info'", unless = "#result == null") + public SysDepartModel queryMainDepartByUsername(String username) { + if (oConvertUtils.isEmpty(username)) { + return null; + } + // 根据用户名查询主部门信息 + SysDepart mainDepart = userMapper.getMainDepartByUsername(username); + if (mainDepart == null) { + return null; + } + + // 复制部门信息到模型对象 + SysDepartModel model = new SysDepartModel(); + BeanUtils.copyProperties(mainDepart, model); + + // 设置部门路径名称 + String departPathName = sysDepartService.getDepartPathNameByOrgCode(model.getOrgCode(), null); + model.setDepartPathName(departPathName); + + return model; + } + + @Override + public DictModel getParentDepartId(String departId) { + SysDepart depart = departMapper.getParentDepartId(departId); + DictModel model = new DictModel(depart.getId(),depart.getParentId()); + return model; + } + + @Override + @Cacheable(value = CacheConstant.SYS_DICT_CACHE,key = "#code", unless = "#result == null ") + public List queryDictItemsByCode(String code) { + return sysDictService.queryDictItemsByCode(code); + } + + @Override + @Cacheable(value = CacheConstant.SYS_ENABLE_DICT_CACHE,key = "#code", unless = "#result == null ") + public List queryEnableDictItemsByCode(String code) { + return sysDictService.queryEnableDictItemsByCode(code); + } + + @Override + public List queryTableDictItemsByCode(String tableFilterSql, String text, String code) { + //【Online+系统】字典表加权限控制机制逻辑,想法不错 LOWCOD-799 + if(tableFilterSql.indexOf(SymbolConstant.SYS_VAR_PREFIX)>=0){ + tableFilterSql = QueryGenerator.getSqlRuleValue(tableFilterSql); + } + return sysDictService.queryTableDictItemsByCode(tableFilterSql, text, code); + } + + @Override + public List queryAllDepartBackDictModel() { + return sysDictService.queryAllDepartBackDictModel(); + } + + @Override + public void sendSysAnnouncement(MessageDTO message) { + this.sendSysAnnouncement(message.getFromUser(), + message.getToUser(), + message.getTitle(), + message.getContent(), + message.getCategory(), + message.getNoticeType()); + try { + // 同步发送第三方APP消息 + wechatEnterpriseService.sendMessage(message, true); + dingtalkService.sendMessage(message, true); + } catch (Exception e) { + log.error("同步发送第三方APP消息失败!", e); + } + } + + @Override + public void sendBusAnnouncement(BusMessageDTO message) { + sendBusAnnouncement(message.getFromUser(), + message.getToUser(), + message.getTitle(), + message.getContent(), + message.getCategory(), + message.getBusType(), + message.getBusId(), + message.getNoticeType()); + try { + // 同步发送第三方APP消息 + wechatEnterpriseService.sendMessage(message, true); + dingtalkService.sendMessage(message, true); + } catch (Exception e) { + log.error("同步发送第三方APP消息失败!", e); + } + } + + @Override + public void sendTemplateAnnouncement(TemplateMessageDTO message) { + String templateCode = message.getTemplateCode(); + String title = message.getTitle(); + Map tmplateParam = message.getTemplateParam(); + String fromUser = message.getFromUser(); + String toUser = message.getToUser(); + + List sysSmsTemplates = sysMessageTemplateService.selectByCode(templateCode); + if(sysSmsTemplates==null||sysSmsTemplates.size()==0){ + throw new GhbBootException("消息模板不存在,模板编码:"+templateCode); + } + SysMessageTemplate sysSmsTemplate = sysSmsTemplates.get(0); + //模板标题 + title = title==null?sysSmsTemplate.getTemplateName():title; + //模板内容 + String content = sysSmsTemplate.getTemplateContent(); + if(tmplateParam!=null) { + for (Map.Entry entry : tmplateParam.entrySet()) { + String str = "${" + entry.getKey() + "}"; + if(oConvertUtils.isNotEmpty(title)){ + title = title.replace(str, entry.getValue()); + } + content = content.replace(str, entry.getValue()); + } + } + String mobileOpenUrl = null; + if(tmplateParam!=null && oConvertUtils.isNotEmpty(tmplateParam.get(CommonConstant.MSG_HREF_URL))){ + mobileOpenUrl = tmplateParam.get(CommonConstant.MSG_HREF_URL); + } + SysAnnouncement announcement = new SysAnnouncement(); + announcement.setTitile(title); + announcement.setMsgContent(content); + announcement.setSender(fromUser); + announcement.setPriority(CommonConstant.PRIORITY_M); + announcement.setMsgType(CommonConstant.MSG_TYPE_UESR); + announcement.setSendStatus(CommonConstant.HAS_SEND); + announcement.setSendTime(new Date()); + announcement.setMsgCategory(CommonConstant.MSG_CATEGORY_2); + announcement.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0)); + // 代码逻辑说明: [QQYUN-12999]系统通知,系统通知时间更新,但是排到下面了 + announcement.setIzTop(CommonConstant.IZ_TOP_0); + sysAnnouncementMapper.insert(announcement); + // 2.插入用户通告阅读标记表记录 + String userId = toUser; + String[] userIds = userId.split(","); + String anntId = announcement.getId(); + for(int i=0;i tmplateParam = message.getTemplateParam(); + String fromUser = message.getFromUser(); + String toUser = message.getToUser(); + String busId = message.getBusId(); + String busType = message.getBusType(); + + List sysSmsTemplates = sysMessageTemplateService.selectByCode(templateCode); + if(sysSmsTemplates==null||sysSmsTemplates.size()==0){ + throw new GhbBootException("消息模板不存在,模板编码:"+templateCode); + } + SysMessageTemplate sysSmsTemplate = sysSmsTemplates.get(0); + //模板标题 + title = title==null?sysSmsTemplate.getTemplateName():title; + //模板内容 + String content = sysSmsTemplate.getTemplateContent(); + if(tmplateParam!=null) { + for (Map.Entry entry : tmplateParam.entrySet()) { + String str = "${" + entry.getKey() + "}"; + if (entry.getValue() != null) { + title = title.replace(str, entry.getValue()); + content = content.replace(str, entry.getValue()); + } + } + } + SysAnnouncement announcement = new SysAnnouncement(); + announcement.setTitile(title); + announcement.setMsgContent(content); + announcement.setSender(fromUser); + announcement.setPriority(CommonConstant.PRIORITY_M); + announcement.setMsgType(CommonConstant.MSG_TYPE_UESR); + announcement.setSendStatus(CommonConstant.HAS_SEND); + announcement.setSendTime(new Date()); + // 代码逻辑说明: [QQYUN-12999]系统通知,系统通知时间更新,但是排到下面了 + announcement.setIzTop(CommonConstant.IZ_TOP_0); + if(tmplateParam!=null && oConvertUtils.isNotEmpty(tmplateParam.get(CommonSendStatus.MSG_ABSTRACT_JSON))){ + announcement.setMsgAbstract(tmplateParam.get(CommonSendStatus.MSG_ABSTRACT_JSON)); + } + String mobileOpenUrl = null; + if(tmplateParam!=null && oConvertUtils.isNotEmpty(tmplateParam.get(CommonConstant.MSG_HREF_URL))){ + mobileOpenUrl = tmplateParam.get(CommonConstant.MSG_HREF_URL); + } + + // 如果传递扩展json,说明是个性化业务,有意见remark则设置为通知内容 + if(oConvertUtils.isJson(announcement.getMsgAbstract())) { + // 获取announcement.getMsgAbstract()的字段remark + JSONObject jsonObject = JSON.parseObject(announcement.getMsgAbstract()); + String remark = jsonObject.containsKey("remark")? jsonObject.getString("remark"): null; + if(oConvertUtils.isNotEmpty(remark)){ + announcement.setMsgContent(remark); + } + } + + announcement.setMsgCategory(CommonConstant.MSG_CATEGORY_2); + announcement.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0)); + announcement.setBusId(busId); + announcement.setBusType(busType); + announcement.setOpenType(SysAnnmentTypeEnum.getByType(busType).getOpenType()); + announcement.setOpenPage(SysAnnmentTypeEnum.getByType(busType).getOpenPage()); + sysAnnouncementMapper.insert(announcement); + // 2.插入用户通告阅读标记表记录 + String userId = toUser; + String[] userIds = userId.split(","); + String anntId = announcement.getId(); + for(int i=0;i(message.getTemplateParam())); + pushMessageDTO.setUsernames(Arrays.asList(toUser)); + this.uniPushMsgToUser(pushMessageDTO); + } catch (Exception e) { + log.error("同步发送第三方APP消息失败!", e); + } + + } + + @Override + public String parseTemplateByCode(TemplateDTO templateDTO) { + String templateCode = templateDTO.getTemplateCode(); + Map map = templateDTO.getTemplateParam(); + List sysSmsTemplates = sysMessageTemplateService.selectByCode(templateCode); + if(sysSmsTemplates==null||sysSmsTemplates.size()==0){ + throw new GhbBootException("消息模板不存在,模板编码:"+templateCode); + } + SysMessageTemplate sysSmsTemplate = sysSmsTemplates.get(0); + //模板内容 + String content = sysSmsTemplate.getTemplateContent(); + if(map!=null) { + for (Map.Entry entry : map.entrySet()) { + String str = "${" + entry.getKey() + "}"; + content = content.replace(str, entry.getValue()); + } + } + return content; + } + + @Override + public void updateSysAnnounReadFlag(String busType, String busId) { + SysAnnouncement announcement = sysAnnouncementMapper.selectOne(new QueryWrapper().eq("bus_type",busType).eq("bus_id",busId)); + if(announcement != null){ + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + LambdaUpdateWrapper updateWrapper = new UpdateWrapper().lambda(); + updateWrapper.set(SysAnnouncementSend::getReadFlag, CommonConstant.HAS_READ_FLAG); + updateWrapper.set(SysAnnouncementSend::getReadTime, new Date()); + updateWrapper.eq(SysAnnouncementSend::getAnntId,announcement.getId()); + updateWrapper.eq(SysAnnouncementSend::getUserId,userId); + //updateWrapper.last("where annt_id ='"+announcement.getId()+"' and user_id ='"+userId+"'"); + SysAnnouncementSend announcementSend = new SysAnnouncementSend(); + sysAnnouncementSendMapper.update(announcementSend, updateWrapper); + } + } + + /** + * 获取数据库类型 + * @param dataSource + * @return + * @throws SQLException + */ + private String getDatabaseTypeByDataSource(DataSource dataSource) throws SQLException{ + if("".equals(DB_TYPE)) { + Connection connection = dataSource.getConnection(); + try { + DatabaseMetaData md = connection.getMetaData(); + String dbType = md.getDatabaseProductName().toLowerCase(); + if(dbType.indexOf(DataBaseConstant.DB_TYPE_MYSQL.toLowerCase())>=0) { + DB_TYPE = DataBaseConstant.DB_TYPE_MYSQL; + }else if(dbType.indexOf(DataBaseConstant.DB_TYPE_ORACLE.toLowerCase())>=0) { + DB_TYPE = DataBaseConstant.DB_TYPE_ORACLE; + }else if(dbType.indexOf(DataBaseConstant.DB_TYPE_SQLSERVER.toLowerCase())>=0||dbType.indexOf(DataBaseConstant.DB_TYPE_SQL_SERVER_BLANK)>=0) { + DB_TYPE = DataBaseConstant.DB_TYPE_SQLSERVER; + }else if(dbType.indexOf(DataBaseConstant.DB_TYPE_POSTGRESQL.toLowerCase())>=0) { + DB_TYPE = DataBaseConstant.DB_TYPE_POSTGRESQL; + }else if(dbType.indexOf(DataBaseConstant.DB_TYPE_MARIADB.toLowerCase())>=0) { + DB_TYPE = DataBaseConstant.DB_TYPE_MARIADB; + }else { + log.error("数据库类型:[" + dbType + "]不识别!"); + //throw new GhbBootException("数据库类型:["+dbType+"]不识别!"); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + }finally { + connection.close(); + } + } + return DB_TYPE; + + } + + @Override + public List queryAllDict() { + // 查询并排序 + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.orderByAsc("create_time"); + List dicts = sysDictService.list(queryWrapper); + // 封装成 model + List list = new ArrayList(); + for (SysDict dict : dicts) { + list.add(new DictModel(dict.getDictCode(), dict.getDictName())); + } + + return list; + } + + @Override + public List queryAllSysCategory() { + List ls = categoryMapper.selectList(null); + List res = oConvertUtils.entityListToModelList(ls,SysCategoryModel.class); + return res; + } + + @Override + public List queryFilterTableDictInfo(String table, String text, String code, String filterSql) { + return sysDictService.queryTableDictItemsByCodeAndFilter(table,text,code,filterSql); + } + + @Override + public List queryTableDictByKeys(String table, String text, String code, String[] keyArray) { + return sysDictService.queryTableDictByKeys(table,text,code,Joiner.on(",").join(keyArray)); + } + + @Override + public List queryAllUserBackCombo() { + List list = new ArrayList(); + List userList = userMapper.selectList(new QueryWrapper().eq("status",1).eq("del_flag",0)); + for(SysUser user : userList){ + ComboModel model = new ComboModel(); + model.setTitle(user.getRealname()); + model.setId(user.getId()); + model.setUsername(user.getUsername()); + list.add(model); + } + return list; + } + + @Override + public JSONObject queryAllUser(String userIds, Integer pageNo, Integer pageSize) { + JSONObject json = new JSONObject(); + QueryWrapper queryWrapper = new QueryWrapper().eq("status",1).eq("del_flag",0); + List list = new ArrayList(); + Page page = new Page(pageNo, pageSize); + IPage pageList = userMapper.selectPage(page, queryWrapper); + for(SysUser user : pageList.getRecords()){ + ComboModel model = new ComboModel(); + model.setUsername(user.getUsername()); + model.setTitle(user.getRealname()); + model.setId(user.getId()); + model.setEmail(user.getEmail()); + if(oConvertUtils.isNotEmpty(userIds)){ + String[] temp = userIds.split(","); + for(int i = 0; i queryAllRole() { + List list = new ArrayList(); + List roleList = roleMapper.selectList(new QueryWrapper()); + for(SysRole role : roleList){ + ComboModel model = new ComboModel(); + model.setTitle(role.getRoleName()); + model.setId(role.getId()); + list.add(model); + } + return list; + } + + @Override + public List queryAllRole(String[] roleIds) { + List list = new ArrayList(); + List roleList = roleMapper.selectList(new QueryWrapper()); + for(SysRole role : roleList){ + ComboModel model = new ComboModel(); + model.setTitle(role.getRoleName()); + model.setId(role.getId()); + model.setRoleCode(role.getRoleCode()); + if(oConvertUtils.isNotEmpty(roleIds)) { + for (int i = 0; i < roleIds.length; i++) { + if (roleIds[i].equals(role.getId())) { + model.setChecked(true); + } + } + } + list.add(model); + } + return list; + } + + @Override + public List getRoleIdsByUsername(String username) { + return sysUserRoleMapper.getRoleIdByUserName(username); + } + + @Override + public String getDepartIdsByOrgCode(String orgCode) { + return departMapper.queryDepartIdByOrgCode(orgCode); + } + + @Override + public List getAllSysDepart() { + List departModelList = new ArrayList(); + List departList = departMapper.selectList(new QueryWrapper().eq("del_flag","0")); + for(SysDepart depart : departList){ + SysDepartModel model = new SysDepartModel(); + BeanUtils.copyProperties(depart,model); + departModelList.add(model); + } + return departModelList; + } + + @Override + public DynamicDataSourceModel getDynamicDbSourceById(String dbSourceId) { + SysDataSource dbSource = dataSourceService.getById(dbSourceId); + if(dbSource!=null && StringUtils.isNotBlank(dbSource.getDbPassword())){ + String dbPassword = dbSource.getDbPassword(); + String decodedStr = SecurityUtil.jiemi(dbPassword); + dbSource.setDbPassword(decodedStr); + } + return new DynamicDataSourceModel(dbSource); + } + + @Override + public DynamicDataSourceModel getDynamicDbSourceByCode(String dbSourceCode) { + SysDataSource dbSource = dataSourceService.getOne(new LambdaQueryWrapper().eq(SysDataSource::getCode, dbSourceCode)); + if(dbSource!=null && StringUtils.isNotBlank(dbSource.getDbPassword())){ + String dbPassword = dbSource.getDbPassword(); + String decodedStr = SecurityUtil.jiemi(dbPassword); + dbSource.setDbPassword(decodedStr); + } + return new DynamicDataSourceModel(dbSource); + } + + @Override + public List getDeptHeadByDepId(String deptId) { + log.debug(" getDeptHeadByDepId 根据部门ID获取负责人,deptId:{}", deptId); + if(oConvertUtils.isEmpty(deptId)){ + return null; + } + + QueryWrapper queryWrapper = new QueryWrapper().eq("status", 1).eq("del_flag", 0); + + // 支持逗号分割传递多个部门id + if (oConvertUtils.isNotEmpty(deptId) && deptId.contains(SymbolConstant.COMMA)) { + String[] vals = deptId.split(SymbolConstant.COMMA); + + // 先trim去除空格,再过滤空字符串,最后去重 + List validDeptIds = Arrays.stream(vals) + .map(String::trim) + .filter(oConvertUtils::isNotEmpty) + .distinct() // 去重处理 + .collect(Collectors.toList()); + + if (!validDeptIds.isEmpty()) { + queryWrapper.and(andWrapper -> { + for (int i = 0; i < validDeptIds.size(); i++) { + andWrapper.like("depart_ids", validDeptIds.get(i)); + if (i < validDeptIds.size() - 1) { + andWrapper.or(); + } + } + }); + } + } else if (oConvertUtils.isNotEmpty(deptId)) { + queryWrapper.like("depart_ids", deptId.trim()); // 单个值也要trim + } + + List userList = userMapper.selectList(queryWrapper); + + // 对结果也进行去重处理 + return userList.stream() + .map(SysUser::getUsername) + .distinct() + .collect(Collectors.toList()); + } + + @Override + public void sendWebSocketMsg(String[] userIds, String cmd) { + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, cmd); + webSocket.sendMessage(userIds, obj.toJSONString()); + } + + @Override + public List queryAllUserByIds(String[] userIds) { + QueryWrapper queryWrapper = new QueryWrapper().eq("status",1).eq("del_flag",0); + queryWrapper.in("id",userIds); + List loginUsers = new ArrayList<>(); + List sysUsers = userMapper.selectList(queryWrapper); + for (SysUser user:sysUsers) { + UserAccountInfo loginUser=new UserAccountInfo(); + BeanUtils.copyProperties(user, loginUser); + loginUsers.add(loginUser); + } + return loginUsers; + } + + /** + * 推送签到人员信息 + * @param userId + */ + @Override + public void meetingSignWebsocket(String userId) { + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_SIGN); + obj.put(WebsocketConst.MSG_USER_ID,userId); + //TODO 目前全部推送,后面修改 + webSocket.sendMessage(obj.toJSONString()); + } + + @Override + public List queryUserByNames(String[] userNames) { + QueryWrapper queryWrapper = new QueryWrapper().eq("status",1).eq("del_flag",0); + queryWrapper.in("username",userNames); + List loginUsers = new ArrayList<>(); + List sysUsers = userMapper.selectList(queryWrapper); + for (SysUser user:sysUsers) { + UserAccountInfo loginUser=new UserAccountInfo(); + BeanUtils.copyProperties(user, loginUser); + loginUsers.add(loginUser); + } + return loginUsers; + } + + @Override + public List queryUserBySuperQuery(String superQuery,String matchType) { + List result=new ArrayList<>(); + Map parameterMap=new HashMap<>(); + parameterMap.put("superQueryMatchType",new String[]{matchType}); + parameterMap.put("superQueryParams",new String[]{superQuery}); + SysUser sysUser=new SysUser(); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysUser, parameterMap); + List list= sysUserService.list(queryWrapper); + if(ObjectUtils.isNotEmpty(list)){ + + // 代码逻辑说明: QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选 + String departKey = "depart"; + QueryCondition departCondition = null; + try { + String temp = URLDecoder.decode(superQuery, "UTF-8"); + List conditions = JSON.parseArray(temp, QueryCondition.class); + for(QueryCondition condition: conditions){ + if(departKey.equals(condition.getField())){ + departCondition = condition; + break; + } + } + } catch (UnsupportedEncodingException e) { + log.error("查询用户信息,查询条件json转化失败", e); + } + + for (SysUser user : list) { + JSONObject userJson = JSONObject.parseObject(JSONObject.toJSONString(user)); + List departList = sysDepartService.queryDepartsByUsername(user.getUsername()); + List departIds = null; + if(departList!=null && departList.size()>0){ + departIds = departList.stream().map(i->i.getId()).collect(Collectors.toList()); + List departNames = departList.stream().map(i->i.getDepartName()).collect(Collectors.toList()); + userJson.put(departKey, oConvertUtils.list2JSONArray(departIds)); + userJson.put(departKey+"_dictText", String.join(",", departNames)); + } + boolean flag = getDepartConditionResult(departCondition, departIds); + if(flag){ + result.add(userJson); + } + + } + } + return result; + } + + /** + * 判断用户的部门是否满足条件 -等于/不等于/在--中/不在--中/为空/不为空 + * QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选 + * @param departCondition + * @param departIds + * @return + */ + private boolean getDepartConditionResult(QueryCondition departCondition, List departIds){ + if(departCondition == null){ + return true; + } + QueryRuleEnum rule = QueryRuleEnum.getByValue(departCondition.getRule()); + String conditionVal = departCondition.getVal(); + if(rule == QueryRuleEnum.EMPTY){ + if(departIds==null || departIds.size()==0){ + return true; + } + }else if (rule == QueryRuleEnum.NOT_EMPTY){ + if(departIds!=null && departIds.size()>0){ + return true; + } + }else{ + if(oConvertUtils.isEmpty(conditionVal)){ + return false; + } + if(departIds==null || departIds.size()==0){ + return false; + } + List conditionList; + if(conditionVal.startsWith("[") && conditionVal.endsWith("]")){ + conditionList = JSONArray.parseArray(conditionVal, String.class); + }else{ + conditionList = new ArrayList(Arrays.asList(conditionVal.split(","))); + } + if(rule == QueryRuleEnum.EQ){ + if(oConvertUtils.isEqList(conditionList, departIds)){ + return true; + } + }else if(rule == QueryRuleEnum.NE){ + if(!oConvertUtils.isEqList(conditionList, departIds)){ + return true; + } + }else if(rule == QueryRuleEnum.IN){ + if(oConvertUtils.isInList(departIds, conditionList)){ + return true; + } + }else if(rule == QueryRuleEnum.NOT_IN){ + if(!oConvertUtils.isInList(departIds, conditionList)){ + return true; + } + } + } + return false; + } + + @Override + public JSONObject queryUserById(String id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(true, SysUser::getId, id); + SysUser sysUser = sysUserService.getOne(queryWrapper); + if (ObjectUtils.isNotEmpty(sysUser)) { + return JSONObject.parseObject(JSONObject.toJSONString(sysUser)); + } + return null; + } + + @Override + public List queryDeptBySuperQuery(String superQuery,String matchType) { + List result=new ArrayList<>(); + Map parameterMap=new HashMap<>(); + parameterMap.put("superQueryMatchType",new String[]{matchType}); + parameterMap.put("superQueryParams",new String[]{superQuery}); + SysDepart sysDepart=new SysDepart(); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDepart, parameterMap); + List list= sysDepartService.list(queryWrapper); + if(ObjectUtils.isNotEmpty(list)){ + for (SysDepart depart: list) { + result.add(JSONObject.parseObject(JSONObject.toJSONString(depart))); + } + } + return result; + } + + @Override + public List queryRoleBySuperQuery(String superQuery,String matchType) { + List result=new ArrayList<>(); + Map parameterMap=new HashMap<>(); + parameterMap.put("superQueryMatchType",new String[]{matchType}); + parameterMap.put("superQueryParams",new String[]{superQuery}); + SysRole sysDepart=new SysRole(); + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(sysDepart, parameterMap); + List list= sysRoleService.list(queryWrapper); + if(ObjectUtils.isNotEmpty(list)){ + for (SysRole role: list) { + result.add(JSONObject.parseObject(JSONObject.toJSONString(role))); + } + } + return result; + } + + @Override + public List selectUserIdByTenantId(String tenantId) { + QueryWrapper queryWrapper=new QueryWrapper(); + queryWrapper.select("user_id"); + queryWrapper.eq("tenant_id",tenantId); + return sysUserTenantService.listObjs(queryWrapper,e->e.toString()); + } + + @Override + public SysDepartModel selectAllById(String id) { + SysDepart sysDepart = sysDepartService.getById(id); + SysDepartModel sysDepartModel = new SysDepartModel(); + BeanUtils.copyProperties(sysDepart,sysDepartModel); + return sysDepartModel; + } + + @Override + public List queryDeptUsersByUserId(String userId) { + List userIds = new ArrayList<>(); + List userDepartList = sysUserDepartService.list(new QueryWrapper().eq("user_id",userId)); + if(userDepartList != null){ + //查找所属公司 + String orgCodes = ""; + StringBuilder orgCodesBuilder = new StringBuilder(); + orgCodesBuilder.append(orgCodes); + for(SysUserDepart userDepart : userDepartList){ + //查询所属公司编码 + SysDepart depart = sysDepartService.getById(userDepart.getDepId()); + int length = YouBianCodeUtil.ZHANWEI_LENGTH; + String compyOrgCode = ""; + if(depart != null && depart.getOrgCode() != null){ + compyOrgCode = depart.getOrgCode().substring(0,length); + if(orgCodes.indexOf(compyOrgCode) == -1){ + orgCodesBuilder.append(SymbolConstant.COMMA).append(compyOrgCode); + } + } + } + orgCodes = orgCodesBuilder.toString(); + if(oConvertUtils.isNotEmpty(orgCodes)){ + orgCodes = orgCodes.substring(1); + List listIds = departMapper.getSubDepIdsByOrgCodes(orgCodes.split(",")); + List userList = sysUserDepartService.list(new QueryWrapper().in("dep_id",listIds)); + for(SysUserDepart userDepart : userList){ + if(!userIds.contains(userDepart.getUserId())){ + userIds.add(userDepart.getUserId()); + } + } + } + } + return userIds; + } + + /** + * 查询用户拥有的角色集合 + * @param username + * @return + */ + @Override + public Set getUserRoleSet(String username) { + // 查询用户拥有的角色集合 + List roles = sysUserRoleMapper.getRoleByUserName(username); + log.debug("-------通过数据库读取用户拥有的角色Rules------username: " + username + ",Roles size: " + (roles == null ? 0 : roles.size())); + return new HashSet<>(roles); + } + + + /** + * 查询用户拥有的角色集合 + * @param useId + * @return + */ + @Override + public Set getUserRoleSetById(String useId) { + // 查询用户拥有的角色集合 + List roles = sysUserRoleMapper.getRoleCodeByUserId(useId); + log.debug("-------通过数据库读取用户拥有的角色Rules------useId: " + useId + ",Roles size: " + (roles == null ? 0 : roles.size())); + return new HashSet<>(roles); + } + + /** + * 查询用户拥有的权限集合 + * @param userId + * @return + */ + @Override + public Set getUserPermissionSet(String userId) { + Set permissionSet = new HashSet<>(); + List permissionList = sysPermissionMapper.queryByUser(userId); + //================= begin 开启租户的时候 如果没有test角色,默认加入test角色================ + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + if (permissionList == null) { + permissionList = new ArrayList<>(); + } + List testRoleList = sysPermissionMapper.queryPermissionByTestRoleId(); + permissionList.addAll(testRoleList); + } + //================= end 开启租户的时候 如果没有test角色,默认加入test角色================ + for (SysPermission po : permissionList) { +// // TODO URL规则有问题? +// if (oConvertUtils.isNotEmpty(po.getUrl())) { +// permissionSet.add(po.getUrl()); +// } + if (oConvertUtils.isNotEmpty(po.getPerms())) { + permissionSet.add(po.getPerms()); + } + } + log.debug("-------通过数据库读取用户拥有的权限Perms------userId: "+ userId+",Perms size: "+ (permissionSet==null?0:permissionSet.size()) ); + return permissionSet; + } + + /** + * 判断online菜单是否有权限 + * @param onlineAuthDTO + * @return + */ + @Override + public boolean hasOnlineAuth(OnlineAuthDTO onlineAuthDTO) { + String username = onlineAuthDTO.getUsername(); + List possibleUrl = onlineAuthDTO.getPossibleUrl(); + String onlineFormUrl = onlineAuthDTO.getOnlineFormUrl(); + //查询菜单 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermission::getDelFlag, 0); + query.in(SysPermission::getUrl, possibleUrl); + List permissionList = sysPermissionMapper.selectList(query); + if (permissionList == null || permissionList.size() == 0) { + //没有配置菜单 找online表单菜单地址 + SysPermission sysPermission = new SysPermission(); + sysPermission.setUrl(onlineFormUrl); + int count = sysPermissionMapper.queryCountByUsername(username, sysPermission); + if(count<=0){ + // 代码逻辑说明: [QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------ + sysPermission.setUrl(onlineAuthDTO.getOnlineWorkOrderUrl()); + count = sysPermissionMapper.queryCountByUsername(username, sysPermission); + if(count<=0) { + return false; + } + } + } else { + //找到菜单了 + boolean has = false; + for (SysPermission p : permissionList) { + int count = sysPermissionMapper.queryCountByUsername(username, p); + has = has || (count>0); + } + if (!has) { + //没有配置菜单 找online表单菜单地址 + SysPermission sysPermission = new SysPermission(); + sysPermission.setUrl(onlineFormUrl); + int count = sysPermissionMapper.queryCountByUsername(username, sysPermission); + if (count <= 0) { + // 代码逻辑说明: [QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------ + sysPermission.setUrl(onlineAuthDTO.getOnlineWorkOrderUrl()); + count = sysPermissionMapper.queryCountByUsername(username, sysPermission); + if (count > 0) { + has = true; + } + } else { + has = true; + } + } + return has; + } + return true; + } + + /** + * 查询用户拥有的角色集合 common api 里面的接口实现 + * @param username + * @return + */ + @Override + public Set queryUserRoles(String username) { + return getUserRoleSet(username); + } + + @Override + public Set queryUserRolesById(String userId) { + return getUserRoleSetById(userId); + } + + /** + * 查询用户拥有的权限集合 common api 里面的接口实现 + * @param userId + * @return + */ + @Override + public Set queryUserAuths(String userId) { + return getUserPermissionSet(userId); + } + + /** + * 36根据多个用户账号(逗号分隔),查询返回多个用户信息 + * @param usernames + * @return + */ + @Override + public List queryUsersByUsernames(String usernames) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getUsername,usernames.split(",")); + return JSON.parseArray(JSON.toJSONString(userMapper.selectList(queryWrapper))).toJavaList(JSONObject.class); + } + + @Override + public List queryUsersByIds(String ids) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getId,ids.split(",")); + return JSON.parseArray(JSON.toJSONString(userMapper.selectList(queryWrapper))).toJavaList(JSONObject.class); + } + + /** + * 37根据多个部门编码(逗号分隔),查询返回多个部门信息 + * @param orgCodes + * @return + */ + @Override + public List queryDepartsByOrgcodes(String orgCodes) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysDepart::getOrgCode,orgCodes.split(",")); + return JSON.parseArray(JSON.toJSONString(sysDepartService.list(queryWrapper))).toJavaList(JSONObject.class); + } + + @Override + public List queryDepartsByIds(String ids) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysDepart::getId,ids.split(",")); + return JSON.parseArray(JSON.toJSONString(sysDepartService.list(queryWrapper))).toJavaList(JSONObject.class); + } + + /** + * 发消息 + * + * @param fromUser + * @param toUser + * @param title + * @param msgContent + * @param setMsgCategory + * @param noticeType + */ + private void sendSysAnnouncement(String fromUser, String toUser, String title, String msgContent, String setMsgCategory, String noticeType) { + SysAnnouncement announcement = new SysAnnouncement(); + announcement.setTitile(title); + announcement.setMsgContent(msgContent); + announcement.setSender(fromUser); + announcement.setPriority(CommonConstant.PRIORITY_M); + announcement.setMsgType(CommonConstant.MSG_TYPE_UESR); + announcement.setSendStatus(CommonConstant.HAS_SEND); + announcement.setSendTime(new Date()); + announcement.setMsgCategory(setMsgCategory); + announcement.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0)); + // 代码逻辑说明: [QQYUN-12999]系统通知,系统通知时间更新,但是排到下面了 + announcement.setIzTop(CommonConstant.IZ_TOP_0); + if(oConvertUtils.isEmpty(noticeType)){ + noticeType = NoticeTypeEnum.NOTICE_TYPE_SYSTEM.getValue(); + } + announcement.setNoticeType(noticeType); + sysAnnouncementMapper.insert(announcement); + // 2.插入用户通告阅读标记表记录 + String userId = toUser; + String[] userIds = userId.split(","); + String anntId = announcement.getId(); + for(int i=0;i getDeptUserByOrgCode(String orgCode) { + //1.获取公司信息 + SysDepart comp=sysDepartService.queryCompByOrgCode(orgCode); + if(comp!=null){ + //2.获取公司下级部门 + List departs=sysDepartService.queryDeptByPid(comp.getId()); + //3.获取部门下的人员信息 + List list=new ArrayList(); + //4.处理部门和下级用户数据 + for (SysDepart dept:departs) { + Map map=new HashMap(5); + //部门名称 + String departName = dept.getDepartName(); + //根据部门编码获取下级部门id + List listIds = departMapper.getSubDepIdsByDepId(dept.getId()); + //根据下级部门ids获取下级部门的所有用户 + List userList = sysUserDepartService.list(new QueryWrapper().in("dep_id",listIds)); + List userIds = new ArrayList<>(); + for(SysUserDepart userDepart : userList){ + if(!userIds.contains(userDepart.getUserId())){ + userIds.add(userDepart.getUserId()); + } + } + map.put("name",departName); + map.put("ids",userIds); + list.add(map); + } + return list; + } + return null; + } + + /** + * 查询分类字典翻译 + * + * @param ids 分类字典表id + * @return + */ + @Override + public List loadCategoryDictItem(String ids) { + return sysCategoryService.loadDictItem(ids, false); + } + + @Override + public List loadCategoryDictItemByNames(String names, boolean delNotExist) { + return sysCategoryService.loadDictItemByNames(names, delNotExist); + } + + /** + * 根据字典code加载字典text + * + * @param dictCode 顺序:tableName,text,code + * @param keys 要查询的key + * @return + */ + @Override + public List loadDictItem(String dictCode, String keys) { + String[] params = dictCode.split(","); + return sysDictService.queryTableDictByKeys(params[0], params[1], params[2], keys, false); + } + + @Override + public Map copyLowAppDict(String originalAppId, String appId, String tenantId) { + Map dictCodeMapping = new HashMap(); + List ls = sysDictService.getDictListByLowAppId(originalAppId); + for (SysDictVo vo : ls) { + vo.setId(null); + vo.setLowAppId(appId); + vo.setTenantId(oConvertUtils.getInt(tenantId, null)); + String newDictCode = sysDictService.addDictByLowAppId(vo); + dictCodeMapping.put(vo.getDictCode(), newDictCode); + } + + log.info(" --- 批量复制应用下的字典到新租户下 —— 原应用ID:{},新应用ID:{},新租户ID:{},字典个数:{} --- ", originalAppId, appId, tenantId, dictCodeMapping.size()); + return dictCodeMapping; + } + + /** + * 根据字典code查询字典项 + * + * @param dictCode 顺序:tableName,text,code + * @param dictCode 要查询的key + * @return + */ + @Override + public List getDictItems(String dictCode) { + List ls = sysDictService.getDictItems(dictCode); + if (ls == null) { + ls = new ArrayList<>(); + } + return ls; + } + + /** + * 根据多个字典code查询多个字典项 + * + * @param dictCodeList + * @return key = dictCode ; value=对应的字典项 + */ + @Override + public Map> getManyDictItems(List dictCodeList) { + return sysDictService.queryDictItemsByCodeList(dictCodeList); + } + + /** + * 【下拉搜索】 + * 大数据量的字典表 走异步加载,即前端输入内容过滤数据 + * + * @param dictCode 字典code格式:table,text,code + * @param keyword 过滤关键字 + * @return + */ + @Override + public List loadDictItemByKeyword(String dictCode, String keyword, Integer pageNo, Integer pageSize) { + return sysDictService.loadDict(dictCode, keyword,pageNo, pageSize); + } + + @Override + public Map> translateManyDict(String dictCodes, String keys) { + List dictCodeList = Arrays.asList(dictCodes.split(",")); + List values = Arrays.asList(keys.split(",")); + // 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------ + return sysDictService.queryManyDictByKeys(dictCodeList, values); + } + + // 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------ + @Override + public List translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource) { + return sysDictService.queryTableDictTextByKeys(table, text, code, Arrays.asList(keys.split(",")), dataSource); + } + + //-------------------------------------流程节点发送模板消息----------------------------------------------- + @Autowired + private QywxSendMsgHandle qywxSendMsgHandle; + + @Autowired + private SystemSendMsgHandle systemSendMsgHandle; + + @Autowired + private EmailSendMsgHandle emailSendMsgHandle; + + @Autowired + private DdSendMsgHandle ddSendMsgHandle; + + @Override + public void sendTemplateMessage(MessageDTO message) { + String messageType = message.getType(); + log.debug(" 【万能通用】推送消息 messageType = {}", messageType); + // 代码逻辑说明: 将模板解析代码移至消息发送, 而不是调用的地方 + String templateCode = message.getTemplateCode(); + if(oConvertUtils.isNotEmpty(templateCode)){ + SysMessageTemplate templateEntity = getTemplateEntity(templateCode); + boolean isMarkdown = CommonConstant.MSG_TEMPLATE_TYPE_MD.equals(templateEntity.getTemplateType()); + String content = templateEntity.getTemplateContent(); + if(oConvertUtils.isNotEmpty(content) && null!=message.getData()){ + content = FreemarkerParseFactory.parseTemplateContent(content, message.getData(), isMarkdown); + } + message.setIsMarkdown(isMarkdown); + message.setContent(content); + } + if(oConvertUtils.isEmpty(message.getContent())){ + log.error("发送消息失败,消息内容为空!"); + throw new GhbBootException("发送消息失败,消息内容为空!"); + } + + if(MessageTypeEnum.XT.getType().equals(messageType)){ + if (message.isMarkdown()) { + // 系统消息要解析Markdown + message.setContent(HTMLUtils.parseMarkdown(message.getContent())); + } + systemSendMsgHandle.sendMessage(message); + }else if(MessageTypeEnum.YJ.getType().equals(messageType)){ + if (message.isMarkdown()) { + // 邮件消息要解析Markdown + message.setContent(HTMLUtils.parseMarkdown(message.getContent())); + } + // 代码逻辑说明: 【QQYUN-8523】敲敲云发邮件通知,不稳定--- + if(message.getIsTimeJob() != null && message.getIsTimeJob()){ + emailSendMsgHandle.sendEmailMessage(message); + }else{ + emailSendMsgHandle.sendMessage(message); + } + }else if(MessageTypeEnum.DD.getType().equals(messageType)){ + ddSendMsgHandle.sendMessage(message); + }else if(MessageTypeEnum.QYWX.getType().equals(messageType)){ + qywxSendMsgHandle.sendMessage(message); + } + } + + @Override + public String getTemplateContent(String code) { + List list = sysMessageTemplateService.selectByCode(code); + if(list==null || list.size()==0){ + return null; + } + return list.get(0).getTemplateContent(); + } + + /** + * 获取模板内容,解析markdown + * + * @param code + * @return + */ + public SysMessageTemplate getTemplateEntity(String code) { + List list = sysMessageTemplateService.selectByCode(code); + if (list == null || list.size() == 0) { + return null; + } + return list.get(0); + } + + //-------------------------------------流程节点发送模板消息----------------------------------------------- + + @Override + public void saveDataLog(DataLogDTO dataLogDto) { + try { + SysDataLog entity = new SysDataLog(); + entity.setDataTable(dataLogDto.getTableName()); + entity.setDataId(dataLogDto.getDataId()); + entity.setDataContent(dataLogDto.getContent()); + entity.setType(dataLogDto.getType()); + entity.setDataVersion("1"); + if (oConvertUtils.isNotEmpty(dataLogDto.getCreateName())) { + entity.setCreateBy(dataLogDto.getCreateName()); + } else { + entity.autoSetCreateName(); + } + sysDataLogService.save(entity); + } catch (Exception e) { + log.warn(e.getMessage(), e); + //e.printStackTrace(); + } + } + + @Override + public void updateAvatar(LoginUser loginUser) { + SysUser sysUser = new SysUser(); + // 创建UpdateWrapper对象 + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.eq("id", loginUser.getId()); // 设置更新条件 + sysUser.setAvatar(loginUser.getAvatar()); // 设置要更新的字段 + sysUserService.update(sysUser, updateWrapper); + } + + @Override + public void sendAppChatSocket(String userId) { + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.MSG_CHAT); + obj.put(WebsocketConst.MSG_USER_ID, userId); + webSocket.sendMessage(userId, obj.toJSONString()); + } + + @Override + public String getRoleCodeById(String id) { + SysRole role = roleMapper.selectById(id); + if(role!=null){ + return role.getRoleCode(); + } + return null; + } + + @Override + public List queryRoleDictByCode(String roleCodes) { + if (oConvertUtils.isEmpty(roleCodes)) { + return new ArrayList<>(); + } + List codeList = Arrays.asList(roleCodes.split(",")); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysRole::getRoleCode, codeList); + List list = roleMapper.selectList(queryWrapper); + // 转换成SysRoleVo + return list.stream().map(sysRole -> { + DictModel model = new DictModel(); + model.setText(sysRole.getRoleName()); + model.setValue(sysRole.getRoleCode()); + return model; + }).collect(Collectors.toList()); + } + + @Override + public List queryUserIdsByDeptIds(List deptIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().select(SysUserDepart::getUserId).in(true,SysUserDepart::getDepId,deptIds); + return sysUserDepartService.listObjs(queryWrapper,e->e.toString()); + } + + @Override + public List queryUsernameByIds(List userIds) { + return userMapper.getUsernameByIds(userIds); + } + + @Override + public List queryUserIdsByCascadeDeptIds(List deptIds) { + Set userIds = new HashSet<>(); + List departs = sysDepartService.list(Wrappers.lambdaQuery(SysDepart.class) + .select(SysDepart::getOrgCode) + .in(SysDepart::getId, deptIds)); + departs.forEach(depart -> { + List sysUsers = sysUserDepartService.queryUserByDepCode(depart.getOrgCode(), null); + if(oConvertUtils.isObjectNotEmpty(sysUsers)){ + userIds.addAll(sysUsers.stream().map(SysUser::getId).collect(Collectors.toSet())); + } + }); + return new ArrayList<>(userIds); + } + + @Override + public List queryUserAccountsByDeptIds(List deptIds) { + return departMapper.queryUserAccountByDepartIds(deptIds); + } + + @Override + public List queryUserIdsByRoleds(List roleCodes) { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .in(SysRole::getRoleCode, roleCodes); + List roleList = sysRoleService.list(query); + if(roleList!=null && roleList.size()>0){ + List idList = roleList.stream().map(role->role.getId()).collect(Collectors.toList()); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().select(SysUserRole::getUserId).in(true,SysUserRole::getRoleId, idList); + return sysUserRoleService.listObjs(queryWrapper,e->e.toString()); + } + return null; + } + + @Override + public List queryUserIdsByDeptPostIds(List deptPostIds) { + // 1.查询兼职岗位对应的用户 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().select(SysUserDepPost::getUserId).in(true,SysUserDepPost::getDepId,deptPostIds); + List otherDepartPostUserIds = sysUserDepPostService.listObjs(queryWrapper,e->e.toString()); + log.info("兼职岗位对应的用户 otherDepartPostUserIds = "+ JSON.toJSONString(otherDepartPostUserIds)); + + // 2.查询主岗位对应的用户 + QueryWrapper mainQueryWrapper = new QueryWrapper<>(); + mainQueryWrapper.lambda().select(SysUser::getId).eq(SysUser::getStatus,Integer.parseInt(CommonConstant.STATUS_1)).eq(SysUser::getDelFlag,CommonConstant.DEL_FLAG_0) + .and(wrapper -> wrapper.in(SysUser::getMainDepPostId, deptPostIds)); + List mainDepartPostUserIds = sysUserService.listObjs(mainQueryWrapper,e->e.toString()); + log.info("主岗位对应的用户 mainDepartPostUserIds = "+ JSON.toJSONString(mainDepartPostUserIds)); + + // 3.合并主岗位和兼职岗位对应的用户 + Set userIdSet = new HashSet<>(); + if (otherDepartPostUserIds != null && !otherDepartPostUserIds.isEmpty()) { + userIdSet.addAll(otherDepartPostUserIds); + } + if (mainDepartPostUserIds != null && !mainDepartPostUserIds.isEmpty()) { + userIdSet.addAll(mainDepartPostUserIds); + } + log.info("主岗位和兼职岗位,对应的用户 userIdSet = "+ JSON.toJSONString(userIdSet)); + return new ArrayList<>(userIdSet); + } + + @Override + public List queryUsernameByDepartPositIds(List deptPostIds) { + // 1.查询兼职岗位对应的用户 + QueryWrapper otherQueryWrapper = new QueryWrapper<>(); + otherQueryWrapper.lambda().select(SysUserDepPost::getUserId).in(true, SysUserDepPost::getDepId, deptPostIds); + List otherUserIds = sysUserDepPostService.listObjs(otherQueryWrapper, e -> e.toString()); + log.info("兼职岗位对应的用户 otherUserIds = {},size = {}" + JSON.toJSONString(otherUserIds), oConvertUtils.getCollectionSize(otherUserIds)); + + // 2.查询主岗位和兼职岗位,对应的用户 + QueryWrapper mainQueryWrapper = new QueryWrapper<>(); + mainQueryWrapper.lambda().select(SysUser::getUsername).eq(SysUser::getStatus, Integer.parseInt(CommonConstant.STATUS_1)).eq(SysUser::getDelFlag, CommonConstant.DEL_FLAG_0) + .and(wrapper -> wrapper + .in(SysUser::getMainDepPostId, deptPostIds) + .or() + .in(otherUserIds != null && !otherUserIds.isEmpty(), SysUser::getId, otherUserIds) + ); + List allUsernames = sysUserService.listObjs(mainQueryWrapper, e -> e.toString()); + log.info("主岗位和兼职岗位,对应的用户账号 allUsernames = {},size = {}" ,JSON.toJSONString(allUsernames), oConvertUtils.getCollectionSize(allUsernames)); + return allUsernames; + } + + @Override + public List queryUserIdsByPositionIds(List positionIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().select(SysUserPosition::getUserId).in(true,SysUserPosition::getPositionId,positionIds); + return sysUserPositionService.listObjs(queryWrapper,e->e.toString()); + } + + /** + * 获取带参数的报表地址,因为多个参数可能顺序会变,所以要将参数顺序重排,获取所有可能的地址集合 + * 如下:参数顺序调整使用in查询,就能查询出菜单数据 + * /online/cgreport/1624393012494286850?name=1&age=16 + * /online/cgreport/1624393012494286850?age=16&name=1 + * @param path + * @return + */ + private List getOnlinePossiblePaths(String path){ + List result = new ArrayList<>(); + log.debug(" path = "+ path); + if (path.indexOf("?") >= 0 && (path.contains("/online/cgreport/") || path.contains("/online/cgformList/") || path.contains("/online/graphreport/"))) { + //包含?说明有多个参数 + String[] pathArray = path.split("\\?"); + if(oConvertUtils.isNotEmpty(pathArray[1])){ + String[] params = pathArray[1].split("&"); + if(params.length==1){ + result.add(path); + }else{ + result = anm(pathArray[0], Arrays.asList(params)); + } + }else{ + result.add(path); + } + }else{ + result.add(path); + } + return result; + } + + + /** + * 获取数组元素的 不同排列 a(n,m) + * @param list + * @return + */ + private List anm(String baseUrl, List list) { + int len = list.size(); + int[] destArray = new int[len]; + for (int i = 0; i < len; i++) { + destArray[i] = i; + } + int[] temp = new int[len]; + List result = new ArrayList<>(); + while (temp[0] < len) { + temp[temp.length - 1]++; + for (int i = temp.length - 1; i > 0; i--) { + if (temp[i] == len) { + temp[i] = 0; + temp[i - 1]++; + } + } + int[] tt = temp.clone(); + Arrays.sort(tt); + if (!Arrays.equals(tt, destArray)) { + continue; + } + String str = ""; + for (int i = 0; i < len; i++) { + if(i>0){ + str += "&"; + } + str += list.get(temp[i]); + } + result.add(baseUrl+"?"+str); + } + return result; + } + + @Override + public List getUserAccountsByDepCode(String orgCode) { + return userMapper.getUserAccountsByDepCode(orgCode); + } + + @Override + public boolean dictTableWhiteListCheckBySql(String selectSql) { + return dictTableWhiteListHandler.isPassBySql(selectSql); + } + + @Override + public boolean dictTableWhiteListCheckByDict(String tableOrDictCode, String... fields) { + if (fields == null || fields.length == 0) { + return dictTableWhiteListHandler.isPassByDict(tableOrDictCode); + } else { + return dictTableWhiteListHandler.isPassByDict(tableOrDictCode, fields); + } + } + + /** + * 自动发布流程 + * @param dataId + * @param currentUserName + */ + @Override + public void announcementAutoRelease(String dataId, String currentUserName) { + //根据ID查询通知公告 + SysAnnouncement sysAnnouncement = sysAnnouncementService.getById(dataId); + //流程通过后自动发布通告 + sysAnnouncement.setSendStatus(CommonSendStatus.PUBLISHED_STATUS_1); + sysAnnouncement.setSendTime(new Date()); + sysAnnouncement.setSender(currentUserName); + boolean ok = sysAnnouncementService.updateById(sysAnnouncement); + //推送通知消息 + if(ok) { + if(sysAnnouncement.getMsgType().equals(CommonConstant.MSG_TYPE_ALL)) { + // 补全公告和用户之前的关系 + sysAnnouncementService.batchInsertSysAnnouncementSend(sysAnnouncement.getId(), sysAnnouncement.getTenantId()); + + // 推送websocket通知 + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_TOPIC); + obj.put(WebsocketConst.MSG_ID, sysAnnouncement.getId()); + obj.put(WebsocketConst.MSG_TXT, sysAnnouncement.getTitile()); + webSocket.sendMessage(obj.toJSONString()); + }else { + // 2.插入用户通告阅读标记表记录 + String userId = sysAnnouncement.getUserIds(); + String[] userIds = userId.substring(0, (userId.length()-1)).split(","); + JSONObject obj = new JSONObject(); + obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_USER); + obj.put(WebsocketConst.MSG_ID, sysAnnouncement.getId()); + obj.put(WebsocketConst.MSG_TXT, sysAnnouncement.getTitile()); + webSocket.sendMessage(userIds, obj.toJSONString()); + } + try { + // 同步企业微信、钉钉的消息通知 + Response dtResponse = dingtalkService.sendActionCardMessage(sysAnnouncement, null, true); + wechatEnterpriseService.sendTextCardMessage(sysAnnouncement, null,true); + + if (dtResponse != null && dtResponse.isSuccess()) { + String taskId = dtResponse.getResult(); + sysAnnouncement.setDtTaskId(taskId); + sysAnnouncementService.updateById(sysAnnouncement); + } + } catch (Exception e) { + log.error("同步发送第三方APP消息失败:", e); + } + } + } + + @Override + public SysDepartModel queryCompByOrgCode(String orgCode) { + AssertUtils.assertNotEmpty("请输入部门编码",orgCode); + SysDepart comp = sysDepartService.queryCompByOrgCode(orgCode); + if(comp == null) { + log.error("未查询到对应的公司信息"); + return null; + } + SysDepartModel respData = new SysDepartModel(); + BeanUtils.copyProperties(comp, respData); + return respData; + } + + /** + * 根据部门编码和层次查询上级公司 + * + * @param orgCode 部门编码 + * @param level 可以传空 默认为1 最小值为1 + * @return + */ + @Override + public SysDepartModel queryCompByOrgCodeAndLevel(String orgCode, Integer level) { + if (null == level || 0 == level) { + level = 1; + } + int codeNum = YouBianCodeUtil.ZHANWEI_LENGTH; + + //先判断父级code + String parendCode = ""; + if (orgCode.length() > codeNum) { + parendCode = orgCode.substring(0, codeNum); + } else { + return null; + } + //根据部门编码查询公司和子公司的数据 + List categoryList = new ArrayList<>(); + categoryList.add(DepartCategoryEnum.DEPART_CATEGORY_COMPANY.getValue()); + categoryList.add(DepartCategoryEnum.DEPART_CATEGORY_SUB_COMPANY.getValue()); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.like(SysDepart::getOrgCode, parendCode); + query.in(SysDepart::getOrgCategory, categoryList); + query.orderByAsc(SysDepart::getOrgType); + List sysDepartList = sysDepartService.list(query); + if (!CollectionUtils.isEmpty(sysDepartList)) { + //获取上级公司 + SysDepart depart = getParentCompanyByOrgCode(orgCode, sysDepartList, level, 1); + if(depart == null){ + depart = sysDepartList.get(0); + } + SysDepartModel respData = new SysDepartModel(); + BeanUtils.copyProperties(depart, respData); + return respData; + } + return null; + } + + /** + * uniPush推送消息给APP用户 + * @param pushMessageDTO + */ + @Override + public void uniPushMsgToUser(PushMessageDTO pushMessageDTO) { + log.info("UniappPush推送URL:{}", GhbPushUrl); + try { + if(oConvertUtils.isEmpty(GhbPushUrl) || "''".equals(GhbPushUrl) || "??".equals(GhbPushUrl) ){ + log.warn("yml配置项: Ghb.unicloud.pushUrl 未设置,APP消息UniPush推送功能未启用!"); + return; + } + // 获取推送的用户信息 + List usernames = pushMessageDTO.getUsernames(); + List userIds = pushMessageDTO.getUserIds(); + + // 构建clientIds + List clientIds = getClientIds(usernames, userIds); + + // 构建请求参数 + Map requestBody = new HashMap<>(); + requestBody.put("title", pushMessageDTO.getTitle()); + requestBody.put("content", pushMessageDTO.getContent()); + requestBody.put("data", pushMessageDTO.getPayload()); + requestBody.put("request_id", String.valueOf(System.currentTimeMillis())); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + // 全用户推送不需要clientIds,指定用户推送需要设置clientIds + boolean isAllUserPush = CommonConstant.MSG_TYPE_ALL.equals(pushMessageDTO.getPushType()); + if (!isAllUserPush) { + if (CollectionUtils.isEmpty(clientIds)) { + log.warn("UniPush消息推送clientIds为空"); + return; + } + requestBody.put("cids", clientIds); + } + + // 统一推送逻辑 + HttpEntity> request = new HttpEntity<>(requestBody, headers); + ResponseEntity response = restTemplate.postForEntity(GhbPushUrl, request, Map.class); + + // 统一处理响应 + String pushType = isAllUserPush ? "全用户" : "单用户"; + if (response.getStatusCode().is2xxSuccessful()) { + log.info("{} UniPush消息推送成功 返回response:{}", pushType, response.getBody()); + } else { + log.error("{} UniPush消息推送失败 返回response:{}", pushType, response.getBody()); + } + } catch (RestClientException e) { + log.warn("UniAPP 消息推送异常:"+ e.getMessage()); + } + } + /** + * 根据用户名或用户ID获取clientIds + */ + private List getClientIds(List usernames, List userIds) { + if (!CollectionUtils.isEmpty(usernames)) { + return extractClientIds(this.queryUsersByUsernames(String.join(",", usernames))); + } else if (!CollectionUtils.isEmpty(userIds)) { + return extractClientIds(this.queryUsersByIds(String.join(",", userIds))); + } + return Collections.emptyList(); + } + + /** + * 从用户信息中提取clientIds + */ + private List extractClientIds(List users) { + return users.stream() + .map(user -> user.getString("clientId")) + .filter(clientId -> oConvertUtils.isNotEmpty(clientId) && !clientId.trim().isEmpty()) + .collect(Collectors.toList()); + } + + /** + * 根据orgCode找上级 + * + * @param orgCode + * @param sysDepartList + * @param level 指定那第几级 从下往上 + * @param nowLevel + * @return + */ + public SysDepart getParentCompanyByOrgCode(String orgCode,List sysDepartList, int level, int nowLevel) { + //获取上一级公司的编码 + String code = this.getPrefix(orgCode); + if(oConvertUtils.isEmpty(code)) { + return null; + } + List list = sysDepartList.stream().filter(sysDepart -> sysDepart.getOrgCode().equals(code)).toList(); + //判断去上级的级别 + if(!CollectionUtils.isEmpty(list) && nowLevel == level) { + return list.get(0); + } else { + if(!CollectionUtils.isEmpty(list)) { + nowLevel++; + } + return getParentCompanyByOrgCode(code, sysDepartList, level, nowLevel); + } + } + + /** + * 根据指定值获取编码前缀(每级固定YouBianCodeUtil.ZHANWEI_LENGTH位) + * + * @param fullCode 完整编码(如"A01A01A01") + * @return 提取后的前缀编码(如"A01A01") + */ + private String getPrefix(String fullCode) { + if(fullCode.length() < YouBianCodeUtil.ZHANWEI_LENGTH){ + return ""; + } + // 计算总层级数,根据ZHANWEI_LENGTH + int totalLevels = fullCode.length() / YouBianCodeUtil.ZHANWEI_LENGTH; + int keepLevels = totalLevels - 1; + // 计算需要截取的长度(保留层级数 × YouBianCodeUtil.ZHANWEI_LENGTH) + int prefixLength = keepLevels * YouBianCodeUtil.ZHANWEI_LENGTH; + return prefixLength == 0 ? "" : fullCode.substring(0, prefixLength); + } + + /** + * 根据部门code或部门id获取部门名称(当前和上级部门) + * + * @param orgCode 部门编码 + * @param depId 部门id + * @return String 部门名称 + */ + @Override + public String getDepartPathNameByOrgCode(String orgCode, String depId) { + return sysDepartService.getDepartPathNameByOrgCode(orgCode, depId); + } +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCategoryServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCategoryServiceImpl.java new file mode 100644 index 0000000..91e2a33 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCategoryServiceImpl.java @@ -0,0 +1,250 @@ +package com.ghb.base.modules.system.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.common.constant.FillRuleConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.FillRuleUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysCategory; +import com.ghb.base.modules.system.mapper.SysCategoryMapper; +import com.ghb.base.modules.system.model.TreeSelectModel; +import com.ghb.base.modules.system.service.ISysCategoryService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Description: 分类字典 + * @Author: Ghb-boot + * @Date: 2019-05-29 + * @Version: V1.0 + */ +@Service +public class SysCategoryServiceImpl extends ServiceImpl implements ISysCategoryService { + + @Override + public void addSysCategory(SysCategory sysCategory) { + String categoryCode = ""; + String categoryPid = ISysCategoryService.ROOT_PID_VALUE; + String parentCode = null; + if(oConvertUtils.isNotEmpty(sysCategory.getPid())){ + categoryPid = sysCategory.getPid(); + + //PID 不是根节点 说明需要设置父节点 hasChild 为1 + if(!ISysCategoryService.ROOT_PID_VALUE.equals(categoryPid)){ + SysCategory parent = baseMapper.selectById(categoryPid); + parentCode = parent.getCode(); + if(parent!=null && !ISysCategoryService.HAS_CHILD.equals(parent.getHasChild())){ + parent.setHasChild(ISysCategoryService.HAS_CHILD); + baseMapper.updateById(parent); + } + } + } + // 代码逻辑说明: 分类字典编码规则生成器做成公用配置 + JSONObject formData = new JSONObject(); + formData.put("pid",categoryPid); + categoryCode = (String) FillRuleUtil.executeRule(FillRuleConstant.CATEGORY,formData); + sysCategory.setCode(categoryCode); + sysCategory.setPid(categoryPid); + baseMapper.insert(sysCategory); + } + + @Override + public void updateSysCategory(SysCategory sysCategory) { + if(oConvertUtils.isEmpty(sysCategory.getPid())){ + sysCategory.setPid(ISysCategoryService.ROOT_PID_VALUE); + }else{ + //如果当前节点父ID不为空 则设置父节点的hasChild 为1 + SysCategory parent = baseMapper.selectById(sysCategory.getPid()); + if(parent!=null && !ISysCategoryService.HAS_CHILD.equals(parent.getHasChild())){ + parent.setHasChild(ISysCategoryService.HAS_CHILD); + baseMapper.updateById(parent); + } + } + baseMapper.updateById(sysCategory); + } + + @Override + public List queryListByCode(String pcode) throws GhbBootException{ + String pid = ROOT_PID_VALUE; + if(oConvertUtils.isNotEmpty(pcode)) { + List list = baseMapper.selectList(new LambdaQueryWrapper().eq(SysCategory::getCode, pcode)); + if(list==null || list.size() ==0) { + throw new GhbBootException("该编码【"+pcode+"】不存在,请核实!"); + } + if(list.size()>1) { + throw new GhbBootException("该编码【"+pcode+"】存在多个,请核实!"); + } + pid = list.get(0).getId(); + } + return baseMapper.queryListByPid(pid,null); + } + + @Override + public List queryListByPid(String pid) { + if(oConvertUtils.isEmpty(pid)) { + pid = ROOT_PID_VALUE; + } + return baseMapper.queryListByPid(pid,null); + } + + @Override + public List queryListByPid(String pid, Map condition) { + if(oConvertUtils.isEmpty(pid)) { + pid = ROOT_PID_VALUE; + } + return baseMapper.queryListByPid(pid,condition); + } + + @Override + public String queryIdByCode(String code) { + return baseMapper.queryIdByCode(code); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteSysCategory(String ids) { + String allIds = this.queryTreeChildIds(ids); + String pids = this.queryTreePids(ids); + //1.删除时将节点下所有子节点一并删除 + this.baseMapper.deleteBatchIds(Arrays.asList(allIds.split(","))); + //2.将父节点中已经没有下级的节点,修改为没有子节点 + if(oConvertUtils.isNotEmpty(pids)){ + LambdaUpdateWrapper updateWrapper = new UpdateWrapper() + .lambda() + .in(SysCategory::getId,Arrays.asList(pids.split(","))) + .set(SysCategory::getHasChild,"0"); + this.update(updateWrapper); + } + } + + /** + * 查询节点下所有子节点 + * @param ids + * @return + */ + private String queryTreeChildIds(String ids) { + //获取id数组 + String[] idArr = ids.split(","); + StringBuffer sb = new StringBuffer(); + for (String pidVal : idArr) { + if(pidVal != null){ + if(!sb.toString().contains(pidVal)){ + if(sb.toString().length() > 0){ + sb.append(","); + } + sb.append(pidVal); + this.getTreeChildIds(pidVal,sb); + } + } + } + return sb.toString(); + } + + /** + * 查询需修改标识的父节点ids + * @param ids + * @return + */ + private String queryTreePids(String ids) { + StringBuffer sb = new StringBuffer(); + //获取id数组 + String[] idArr = ids.split(","); + for (String id : idArr) { + if(id != null){ + SysCategory category = this.baseMapper.selectById(id); + //根据id查询pid值 + String metaPid = category.getPid(); + //查询此节点上一级是否还有其他子节点 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysCategory::getPid,metaPid); + queryWrapper.notIn(SysCategory::getId,Arrays.asList(idArr)); + List dataList = this.baseMapper.selectList(queryWrapper); + boolean flag = (dataList == null || dataList.size()==0) && !Arrays.asList(idArr).contains(metaPid) + && !sb.toString().contains(metaPid); + if(flag){ + //如果当前节点原本有子节点 现在木有了,更新状态 + sb.append(metaPid).append(","); + } + } + } + if(sb.toString().endsWith(SymbolConstant.COMMA)){ + sb = sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * 递归 根据父id获取子节点id + * @param pidVal + * @param sb + * @return + */ + private StringBuffer getTreeChildIds(String pidVal,StringBuffer sb){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysCategory::getPid,pidVal); + List dataList = baseMapper.selectList(queryWrapper); + if(dataList != null && dataList.size()>0){ + for(SysCategory category : dataList) { + if(!sb.toString().contains(category.getId())){ + sb.append(",").append(category.getId()); + } + this.getTreeChildIds(category.getId(), sb); + } + } + return sb; + } + + @Override + public List loadDictItem(String ids) { + return this.loadDictItem(ids, true); + } + + @Override + public List loadDictItem(String ids, boolean delNotExist) { + String[] idArray = ids.split(","); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.in(SysCategory::getId, Arrays.asList(idArray)); + // 查询数据 + List list = super.list(query); + // 取出name并返回 + List textList; + // 代码逻辑说明: 新增delNotExist参数,设为false不删除数据库里不存在的key ---- + if (delNotExist) { + textList = list.stream().map(SysCategory::getName).collect(Collectors.toList()); + } else { + textList = new ArrayList<>(); + for (String id : idArray) { + List res = list.stream().filter(i -> id.equals(i.getId())).collect(Collectors.toList()); + textList.add(res.size() > 0 ? res.get(0).getName() : id); + } + } + return textList; + } + + @Override + public List loadDictItemByNames(String names, boolean delNotExist) { + List nameList = Arrays.asList(names.split(SymbolConstant.COMMA)); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.select(SysCategory::getId, SysCategory::getName); + query.in(SysCategory::getName, nameList); + // 查询数据 + List list = super.list(query); + // 取出id并返回 + return nameList.stream().map(name -> { + SysCategory res = list.stream().filter(i -> name.equals(i.getName())).findFirst().orElse(null); + if (res == null) { + return delNotExist ? null : name; + } + return res.getId(); + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCheckRuleServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCheckRuleServiceImpl.java new file mode 100644 index 0000000..0f2851d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCheckRuleServiceImpl.java @@ -0,0 +1,98 @@ +package com.ghb.base.modules.system.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.lang.StringUtils; +import com.ghb.base.modules.system.entity.SysCheckRule; +import com.ghb.base.modules.system.mapper.SysCheckRuleMapper; +import com.ghb.base.modules.system.service.ISysCheckRuleService; +import org.springframework.stereotype.Service; + +import java.util.regex.Pattern; + +/** + * @Description: 编码校验规则 + * @Author: Ghb-boot + * @Date: 2020-02-04 + * @Version: V1.0 + */ +@Service +public class SysCheckRuleServiceImpl extends ServiceImpl implements ISysCheckRuleService { + + /** + * 位数特殊符号,用于检查整个值,而不是裁剪某一段 + */ + private final String CHECK_ALL_SYMBOL = "*"; + + @Override + public SysCheckRule getByCode(String ruleCode) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysCheckRule::getRuleCode, ruleCode); + return super.getOne(queryWrapper); + } + + /** + * 通过用户设定的自定义校验规则校验传入的值 + * + * @param checkRule + * @param value + * @return 返回 null代表通过校验,否则就是返回的错误提示文本 + */ + @Override + public JSONObject checkValue(SysCheckRule checkRule, String value) { + if (checkRule != null && StringUtils.isNotBlank(value)) { + String ruleJson = checkRule.getRuleJson(); + if (StringUtils.isNotBlank(ruleJson)) { + // 开始截取的下标,根据规则的顺序递增,但是 * 号不计入递增范围 + int beginIndex = 0; + JSONArray rules = JSON.parseArray(ruleJson); + for (int i = 0; i < rules.size(); i++) { + JSONObject result = new JSONObject(); + JSONObject rule = rules.getJSONObject(i); + // 位数 + String digits = rule.getString("digits"); + result.put("digits", digits); + // 验证规则 + String pattern = rule.getString("pattern"); + result.put("pattern", pattern); + // 未通过时的提示文本 + String message = rule.getString("message"); + result.put("message", message); + + // 根据用户设定的区间,截取字符串进行验证 + String checkValue; + // 是否检查整个值而不截取 + if (CHECK_ALL_SYMBOL.equals(digits)) { + checkValue = value; + } else { + int num = Integer.parseInt(digits); + int endIndex = beginIndex + num; + // 如果结束下标大于给定的值的长度,则取到最后一位 + endIndex = endIndex > value.length() ? value.length() : endIndex; + // 如果开始下标大于结束下标,则说明用户还尚未输入到该位置,直接赋空值 + if (beginIndex > endIndex) { + checkValue = ""; + } else { + checkValue = value.substring(beginIndex, endIndex); + } + result.put("beginIndex", beginIndex); + result.put("endIndex", endIndex); + beginIndex += num; + } + result.put("checkValue", checkValue); + boolean passed = Pattern.matches(pattern, checkValue); + result.put("passed", passed); + // 如果没有通过校验就返回错误信息 + if (!passed) { + return result; + } + } + } + } + return null; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCommentServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCommentServiceImpl.java new file mode 100644 index 0000000..627d124 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysCommentServiceImpl.java @@ -0,0 +1,409 @@ +package com.ghb.base.modules.system.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.common.api.dto.message.MessageDTO; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.FileTypeEnum; +import com.ghb.base.common.constant.enums.MessageTypeEnum; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.vo.SysFilesModel; +import com.ghb.base.common.util.CommonUtils; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.filter.SsrfFileTypeFilter; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysComment; +import com.ghb.base.modules.system.entity.SysFormFile; +import com.ghb.base.modules.system.mapper.SysCommentMapper; +import com.ghb.base.modules.system.mapper.SysFormFileMapper; +import com.ghb.base.modules.system.service.ISysCommentService; +import com.ghb.base.modules.system.vo.SysCommentFileVo; +import com.ghb.base.modules.system.vo.SysCommentVO; +import com.ghb.base.modules.system.vo.UserAvatar; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @Description: 系统评论回复表 + * @Author: Ghb-boot + * @Date: 2022-07-19 + * @Version: V1.0 + */ +@Service +public class SysCommentServiceImpl extends ServiceImpl implements ISysCommentService { + + @Autowired + private ISysBaseAPI sysBaseApi; + + @Autowired + private SysFormFileMapper sysFormFileMapper; + +// @Autowired +// private IEasyOaBaseApi easyOaBseApi; + + @Autowired + private RedisUtil redisUtil; + + @Value(value = "${ghb.path.upload}") + private String uploadpath; + + @Value(value = "${ghb.uploadType}") + private String uploadType; + + /** + * sysFormFile中的表名 + */ + private static final String SYS_FORM_FILE_TABLE_NAME = "sys_comment"; + + @Override + public List queryFormCommentInfo(SysComment sysComment) { + String tableName = sysComment.getTableName(); + String dataId = sysComment.getTableDataId(); + //获取评论信息 + List list = this.baseMapper.queryCommentList(tableName, dataId); + // 获取评论相关人员 + Set personSet = new HashSet<>(); + if(list!=null && list.size()>0){ + for(SysCommentVO vo: list){ + if(oConvertUtils.isNotEmpty(vo.getFromUserId())){ + personSet.add(vo.getFromUserId()); + } + if(oConvertUtils.isNotEmpty(vo.getToUserId())){ + personSet.add(vo.getToUserId()); + } + } + } + if(personSet.size()>0){ + //获取用户信息 + Map userAvatarMap = queryUserAvatar(personSet); + for(SysCommentVO vo: list){ + String formId = vo.getFromUserId(); + String toId = vo.getToUserId(); + // 设置头像、用户名 + if(oConvertUtils.isNotEmpty(formId)){ + UserAvatar fromUser = userAvatarMap.get(formId); + if(fromUser!=null){ + vo.setFromUserId_dictText(fromUser.getRealname()); + vo.setFromUserAvatar(fromUser.getAvatar()); + } + } + if(oConvertUtils.isNotEmpty(toId)){ + UserAvatar toUser = userAvatarMap.get(toId); + if(toUser!=null){ + vo.setToUserId_dictText(toUser.getRealname()); + vo.setToUserAvatar(toUser.getAvatar()); + } + } + } + } + return list; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveOneFileComment(HttpServletRequest request) { + String existFileId = request.getParameter("fileId"); + if(oConvertUtils.isEmpty(existFileId)){ + String savePath = ""; + // 获取业务路径 + String bizPath = request.getParameter("biz"); + // 获取上传文件对象 + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + MultipartFile file = multipartRequest.getFile("file"); + + // 文件安全校验,防止上传漏洞文件 + try { + SsrfFileTypeFilter.checkUploadFileType(file, bizPath); + } catch (Exception e) { + throw new GhbBootException(e); + } + + if (oConvertUtils.isEmpty(bizPath)) { + bizPath = CommonConstant.UPLOAD_TYPE_OSS.equals(uploadType) ? "upload" : ""; + } + if (CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)) { + savePath = this.uploadLocal(file, bizPath); + } else { + savePath = CommonUtils.upload(file, bizPath, uploadType); + } + + String orgName = file.getOriginalFilename(); + // 获取文件名 + orgName = CommonUtils.getFileName(orgName); + //文件大小 + long size = file.getSize(); + //文件类型 + String type = orgName.substring(orgName.lastIndexOf("."), orgName.length()); + FileTypeEnum fileType = FileTypeEnum.getByType(type); + + //保存至 SysFiles + SysFilesModel sysFiles = new SysFilesModel(); + sysFiles.setFileName(orgName); + sysFiles.setUrl(savePath); + sysFiles.setFileType(fileType.getValue()); + sysFiles.setStoreType("temp"); + if (size > 0) { + sysFiles.setFileSize(Double.parseDouble(String.valueOf(size))); + } + String fileId = String.valueOf(IdWorker.getId()); + sysFiles.setId(fileId); + String tenantId = oConvertUtils.getString(TenantContext.getTenant()); + sysFiles.setTenantId(tenantId); +// //update-begin---author:wangshuai---date:2024-01-04---for:【QQYUN-7821】知识库后端迁移--- +// easyOaBseApi.addSysFiles(sysFiles); +// //update-end---author:wangshuai---date:2024-01-04---for:【QQYUN-7821】知识库后端迁移--- + + //保存至 SysFormFile + String tableName = SYS_FORM_FILE_TABLE_NAME; + String tableDataId = request.getParameter("commentId"); + SysFormFile sysFormFile = new SysFormFile(); + sysFormFile.setTableName(tableName); + sysFormFile.setFileType(fileType.getValue()); + sysFormFile.setTableDataId(tableDataId); + sysFormFile.setFileId(fileId); + sysFormFileMapper.insert(sysFormFile); + + }else{ +// //update-begin---author:wangshuai---date:2024-01-04---for:【QQYUN-7821】知识库后端迁移--- +// SysFilesModel sysFiles = easyOaBseApi.getFileById(existFileId); +// //update-end---author:wangshuai---date:2024-01-04---for:【QQYUN-7821】知识库后端迁移--- +// if(sysFiles!=null){ + //保存至 SysFormFile + String tableName = SYS_FORM_FILE_TABLE_NAME; + String tableDataId = request.getParameter("commentId"); + SysFormFile sysFormFile = new SysFormFile(); + sysFormFile.setTableName(tableName); + sysFormFile.setFileType(""); + sysFormFile.setTableDataId(tableDataId); + sysFormFile.setFileId(existFileId); + sysFormFileMapper.insert(sysFormFile); +// } + } + } + + /** + * app端回复评论保存文件 + * @param request + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void appSaveOneFileComment(HttpServletRequest request) { + + String orgName = request.getParameter("fileName"); + String fileSize = request.getParameter("fileSize"); + String savePath = request.getParameter("savePath"); + // 获取文件名 + orgName = CommonUtils.getFileName(orgName); + //文件大小 + long size = Long.valueOf(fileSize); + //文件类型 + String type = orgName.substring(orgName.lastIndexOf("."), orgName.length()); + FileTypeEnum fileType = FileTypeEnum.getByType(type); + + //保存至 SysFiles + SysFilesModel sysFiles = new SysFilesModel(); + sysFiles.setFileName(orgName); + sysFiles.setUrl(savePath); + sysFiles.setFileType(fileType.getValue()); + sysFiles.setStoreType("temp"); + if (size > 0) { + sysFiles.setFileSize(Double.parseDouble(String.valueOf(size))); + } + String defaultValue = "0"; + String fileId = String.valueOf(IdWorker.getId()); + sysFiles.setId(fileId); + String tenantId = oConvertUtils.getString(TenantContext.getTenant()); + sysFiles.setTenantId(tenantId); +// //update-begin---author:wangshuai---date:2024-01-04---for:【QQYUN-7821】知识库后端迁移--- +// easyOaBseApi.addSysFiles(sysFiles); +// //update-end---author:wangshuai---date:2024-01-04---for:【QQYUN-7821】知识库后端迁移--- + //保存至 SysFormFile + String tableName = SYS_FORM_FILE_TABLE_NAME; + String tableDataId = request.getParameter("commentId"); + SysFormFile sysFormFile = new SysFormFile(); + sysFormFile.setTableName(tableName); + sysFormFile.setFileType(fileType.getValue()); + sysFormFile.setTableDataId(tableDataId); + sysFormFile.setFileId(fileId); + sysFormFileMapper.insert(sysFormFile); + + } + + @Override + public List queryFormFileList(String tableName, String formDataId) { + List list = baseMapper.queryFormFileList(tableName, formDataId); + return list; + } + + @Override + public String saveOne(SysComment sysComment) { + this.save(sysComment); + //发送系统消息 + String content = sysComment.getCommentContent(); + if (content.indexOf("@") >= 0) { + Set set = getCommentUsername(content); + if (set.size() > 0) { + String users = String.join(",", set); + MessageDTO md = new MessageDTO(); + md.setTitle("有人在表单评论中提到了你"); + md.setContent(content); + md.setToAll(false); + md.setToUser(users); + md.setFromUser("system"); + md.setType(MessageTypeEnum.XT.getType()); + + // 代码逻辑说明: QQYUN-4744【系统通知】6、系统通知@人后,对方看不到是哪个表单@的,没有超链接 + String tableName = sysComment.getTableName(); + String prefix = "desform:"; + if (tableName != null) { + // 表单设计器 + if (tableName.startsWith(prefix)) { + Map data = new HashMap<>(); + data.put(CommonConstant.NOTICE_MSG_BUS_TYPE, "comment"); + JSONObject params = new JSONObject(); + params.put("code", tableName.substring(prefix.length())); + params.put("dataId", sysComment.getTableDataId()); + params.put("type", "designForm"); + data.put(CommonConstant.NOTICE_MSG_SUMMARY, params); + md.setData(data); + } + // Online表单,判断是否携带id + else if (oConvertUtils.isNotEmpty(sysComment.getTableId())) { + Map data = new HashMap<>(); + data.put(CommonConstant.NOTICE_MSG_BUS_TYPE, "comment"); + JSONObject params = new JSONObject(); + params.put("code", tableName); + params.put("formId", sysComment.getTableId()); + params.put("dataId", sysComment.getTableDataId()); + params.put("type", "cgform"); + data.put(CommonConstant.NOTICE_MSG_SUMMARY, params); + md.setData(data); + } + } + + sysBaseApi.sendTemplateMessage(md); + } + } + return sysComment.getId(); + } + + @Override + public void deleteOne(String id) { + this.removeById(id); + //还要删除关联文件 + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysFormFile::getTableDataId, id) + .eq(SysFormFile::getTableName, SYS_FORM_FILE_TABLE_NAME); + this.sysFormFileMapper.delete(query); + } + + /** + * 通过正则获取评论中的用户账号 + * + * @return + */ + private Set getCommentUsername(String content) { + Set set = new HashSet(3); + String reg = "(@(.*?)\\[(.*?)\\])"; + Pattern p = Pattern.compile(reg); + Matcher m = p.matcher(content); + while (m.find()) { + if (m.groupCount() == 3) { + String username = m.group(3); + set.add(username); + } + } + return set; + } + + + /** + * 本地文件上传 + * + * @param mf 文件 + * @param bizPath 自定义路径 + * @return + */ + private String uploadLocal(MultipartFile mf, String bizPath) { + try { + // 文件安全校验,防止上传漏洞文件 + SsrfFileTypeFilter.checkUploadFileType(mf, bizPath); + } catch (Exception e) { + throw new GhbBootException(e); + } + + try { + String ctxPath = uploadpath; + String fileName = null; + //update-begin---author:liusq ---date:2026-03-30 for:【issues/9427】修复uploadLocal bizPath路径遍历漏洞(CWE-22)----------- + // 路径遍历校验:规范化后确保目标目录在uploadpath内 + File uploadDir = new File(ctxPath).getCanonicalFile(); + File file = new File(ctxPath + File.separator + bizPath + File.separator).getCanonicalFile(); + if (!file.toPath().startsWith(uploadDir.toPath())) { + throw new GhbBootException("非法业务路径,禁止访问上传目录之外的路径: " + bizPath); + } + //update-end---author:liusq ---date:2026-03-30 for:【issues/9427】修复uploadLocal bizPath路径遍历漏洞(CWE-22)----------- + if (!file.exists()) { + file.mkdirs();// 创建文件根目录 + } + String orgName = mf.getOriginalFilename();// 获取文件名 + orgName = CommonUtils.getFileName(orgName); + if (orgName.indexOf(".") != -1) { + fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.indexOf(".")); + } else { + fileName = orgName + "_" + System.currentTimeMillis(); + } + String savePath = file.getPath() + File.separator + fileName; + File savefile = new File(savePath); + FileCopyUtils.copy(mf.getBytes(), savefile); + String dbpath = null; + if (oConvertUtils.isNotEmpty(bizPath)) { + dbpath = bizPath + File.separator + fileName; + } else { + dbpath = fileName; + } + if (dbpath.contains("\\")) { + dbpath = dbpath.replace("\\", "/"); + } + return dbpath; + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return ""; + } + + /** + * 查询用户信息 + * @param idSet + * @return + */ + private Map queryUserAvatar(Set idSet){ + List list = this.baseMapper.queryUserAvatarList(idSet); + Map map = new HashMap<>(); + if(list!=null && list.size()>0){ + for(UserAvatar user: list){ + map.put(user.getId(), user); + } + } + return map; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDataLogServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDataLogServiceImpl.java new file mode 100644 index 0000000..fb4670f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDataLogServiceImpl.java @@ -0,0 +1,38 @@ +package com.ghb.base.modules.system.service.impl; + +import com.ghb.base.modules.system.entity.SysDataLog; +import com.ghb.base.modules.system.mapper.SysDataLogMapper; +import com.ghb.base.modules.system.service.ISysDataLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: 系统数据日志实现类 + * @author: Ghb-boot + */ +@Service +public class SysDataLogServiceImpl extends ServiceImpl implements ISysDataLogService { + @Autowired + private SysDataLogMapper logMapper; + + /** + * 添加数据日志 + */ + @Override + public void addDataLog(String tableName, String dataId, String dataContent) { + String versionNumber = "0"; + String dataVersion = logMapper.queryMaxDataVer(tableName, dataId); + if(dataVersion != null ) { + versionNumber = String.valueOf(Integer.parseInt(dataVersion)+1); + } + SysDataLog log = new SysDataLog(); + log.setDataTable(tableName); + log.setDataId(dataId); + log.setDataContent(dataContent); + log.setDataVersion(versionNumber); + log.autoSetCreateName(); + this.save(log); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDataSourceServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDataSourceServiceImpl.java new file mode 100644 index 0000000..55fa94a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDataSourceServiceImpl.java @@ -0,0 +1,131 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.creator.druid.DruidDataSourceCreator; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.lang.StringUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.util.dynamic.db.DataSourceCachePool; +import com.ghb.base.modules.system.entity.SysDataSource; +import com.ghb.base.modules.system.mapper.SysDataSourceMapper; +import com.ghb.base.modules.system.service.ISysDataSourceService; +import com.ghb.base.modules.system.util.SecurityUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; + +/** + * @Description: 多数据源管理 + * @Author: Ghb-boot + * @Date: 2019-12-25 + * @Version: V1.0 + */ +@Service +public class SysDataSourceServiceImpl extends ServiceImpl implements ISysDataSourceService { + + @Autowired + private DruidDataSourceCreator dataSourceCreator; + + @Autowired + private DataSource dataSource; + + @Override + public Result saveDataSource(SysDataSource sysDataSource) { + try { + long count = checkDbCode(sysDataSource.getCode()); + if (count > 0) { + return Result.error("数据源编码已存在"); + } + String dbPassword = sysDataSource.getDbPassword(); + if (StringUtils.isNotBlank(dbPassword)) { + String encrypt = SecurityUtil.jiami(dbPassword); + sysDataSource.setDbPassword(encrypt); + } + boolean result = save(sysDataSource); + if (result) { + //动态创建数据源 + //addDynamicDataSource(sysDataSource, dbPassword); + } + } catch (Exception e) { + e.printStackTrace(); + } + return Result.OK("添加成功!"); + } + + @Override + public Result editDataSource(SysDataSource sysDataSource) { + try { + SysDataSource d = getById(sysDataSource.getId()); + DataSourceCachePool.removeCache(d.getCode()); + String dbPassword = sysDataSource.getDbPassword(); + if (StringUtils.isNotBlank(dbPassword)) { + String encrypt = SecurityUtil.jiami(dbPassword); + sysDataSource.setDbPassword(encrypt); + } + Boolean result=updateById(sysDataSource); + if(result){ + //先删除老的数据源 + // removeDynamicDataSource(d.getCode()); + //添加新的数据源 + //addDynamicDataSource(sysDataSource,dbPassword); + } + } catch (Exception e) { + e.printStackTrace(); + } + return Result.OK("编辑成功!"); + } + + @Override + public Result deleteDataSource(String id) { + SysDataSource sysDataSource = getById(id); + DataSourceCachePool.removeCache(sysDataSource.getCode()); + removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 动态添加数据源 【注册mybatis动态数据源】 + * + * @param sysDataSource 添加数据源数据对象 + * @param dbPassword 未加密的密码 + */ + private void addDynamicDataSource(SysDataSource sysDataSource, String dbPassword) { + DataSourceProperty dataSourceProperty = new DataSourceProperty(); + dataSourceProperty.setUrl(sysDataSource.getDbUrl()); + dataSourceProperty.setPassword(dbPassword); + dataSourceProperty.setDriverClassName(sysDataSource.getDbDriver()); + dataSourceProperty.setUsername(sysDataSource.getDbUsername()); + DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; + DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty); + try { + ds.addDataSource(sysDataSource.getCode(), dataSource); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 删除数据源 + * @param code + */ + private void removeDynamicDataSource(String code) { + DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; + ds.removeDataSource(code); + } + + /** + * 检查数据源编码是否存在 + * + * @param dbCode + * @return + */ + private long checkDbCode(String dbCode) { + QueryWrapper qw = new QueryWrapper(); + qw.lambda().eq(true, SysDataSource::getCode, dbCode); + return count(qw); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartPermissionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartPermissionServiceImpl.java new file mode 100644 index 0000000..352bfef --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartPermissionServiceImpl.java @@ -0,0 +1,120 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysDepartPermission; +import com.ghb.base.modules.system.entity.SysDepartRole; +import com.ghb.base.modules.system.entity.SysDepartRolePermission; +import com.ghb.base.modules.system.entity.SysPermissionDataRule; +import com.ghb.base.modules.system.mapper.SysDepartPermissionMapper; +import com.ghb.base.modules.system.mapper.SysDepartRoleMapper; +import com.ghb.base.modules.system.mapper.SysDepartRolePermissionMapper; +import com.ghb.base.modules.system.mapper.SysPermissionDataRuleMapper; +import com.ghb.base.modules.system.service.ISysDepartPermissionService; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Description: 部门权限表 + * @Author: Ghb-boot + * @Date: 2020-02-11 + * @Version: V1.0 + */ +@Service +public class SysDepartPermissionServiceImpl extends ServiceImpl implements ISysDepartPermissionService { + @Resource + private SysPermissionDataRuleMapper ruleMapper; + + @Resource + private SysDepartRoleMapper sysDepartRoleMapper; + + @Resource + private SysDepartRolePermissionMapper departRolePermissionMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveDepartPermission(String departId, String permissionIds, String lastPermissionIds) { + //1.对比要新增的权限 + List add = getDiff(lastPermissionIds,permissionIds); + if(add!=null && add.size()>0) { + List list = new ArrayList(); + for (String p : add) { + if(oConvertUtils.isNotEmpty(p)) { + SysDepartPermission rolepms = new SysDepartPermission(departId, p); + list.add(rolepms); + } + } + this.saveBatch(list); + } + //2.对比要删除的权限 + List delete = getDiff(permissionIds,lastPermissionIds); + if(delete!=null && delete.size()>0) { + for (String permissionId : delete) { + //2.1 删除部门对应的权限 + this.remove(new QueryWrapper().lambda() + .eq(SysDepartPermission::getDepartId, departId) + .eq(SysDepartPermission::getPermissionId, permissionId)); + //2.2 删除部门权限时,删除部门角色中已授权的权限 + List sysDepartRoleList = sysDepartRoleMapper.selectList(new LambdaQueryWrapper().eq(SysDepartRole::getDepartId,departId)); + List roleIds = sysDepartRoleList.stream().map(SysDepartRole::getId).collect(Collectors.toList()); + if(roleIds != null && roleIds.size()>0){ + departRolePermissionMapper.delete(new LambdaQueryWrapper() + .eq(SysDepartRolePermission::getPermissionId,permissionId) + // 代码逻辑说明: [issue/#5339]部门管理下部门赋权代码逻辑缺少判断条件 + .in(SysDepartRolePermission::getRoleId,roleIds) + ); + } + } + } + } + + @Override + public List getPermRuleListByDeptIdAndPermId(String departId, String permissionId) { + SysDepartPermission departPermission = this.getOne(new QueryWrapper().lambda().eq(SysDepartPermission::getDepartId, departId).eq(SysDepartPermission::getPermissionId, permissionId)); + if(departPermission != null && oConvertUtils.isNotEmpty(departPermission.getDataRuleIds())){ + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.in(SysPermissionDataRule::getId, Arrays.asList(departPermission.getDataRuleIds().split(","))); + query.orderByDesc(SysPermissionDataRule::getCreateTime); + List permRuleList = this.ruleMapper.selectList(query); + return permRuleList; + }else{ + return null; + } + } + + /** + * 从diff中找出main中没有的元素 + * @param main + * @param diff + * @return + */ + private List getDiff(String main,String diff){ + if(oConvertUtils.isEmpty(diff)) { + return null; + } + if(oConvertUtils.isEmpty(main)) { + return Arrays.asList(diff.split(",")); + } + + String[] mainArr = main.split(","); + String[] diffArr = diff.split(","); + Map map = new HashMap(5); + for (String string : mainArr) { + map.put(string, 1); + } + List res = new ArrayList(); + for (String key : diffArr) { + if(oConvertUtils.isNotEmpty(key) && !map.containsKey(key)) { + res.add(key); + } + } + return res; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRolePermissionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRolePermissionServiceImpl.java new file mode 100644 index 0000000..779e518 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRolePermissionServiceImpl.java @@ -0,0 +1,87 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.ghb.base.common.util.IpUtils; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysDepartRolePermission; +import com.ghb.base.modules.system.mapper.SysDepartRolePermissionMapper; +import com.ghb.base.modules.system.service.ISysDepartRolePermissionService; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * @Description: 部门角色权限 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +@Service +public class SysDepartRolePermissionServiceImpl extends ServiceImpl implements ISysDepartRolePermissionService { + + @Override + public void saveDeptRolePermission(String roleId, String permissionIds, String lastPermissionIds) { + String ip = ""; + try { + //获取request + HttpServletRequest request = SpringContextUtils.getHttpServletRequest(); + //获取IP地址 + ip = IpUtils.getIpAddr(request); + } catch (Exception e) { + ip = "127.0.0.1"; + } + List add = getDiff(lastPermissionIds,permissionIds); + if(add!=null && add.size()>0) { + List list = new ArrayList(); + for (String p : add) { + if(oConvertUtils.isNotEmpty(p)) { + SysDepartRolePermission rolepms = new SysDepartRolePermission(roleId, p); + rolepms.setOperateDate(new Date()); + rolepms.setOperateIp(ip); + list.add(rolepms); + } + } + this.saveBatch(list); + } + + List delete = getDiff(permissionIds,lastPermissionIds); + if(delete!=null && delete.size()>0) { + for (String permissionId : delete) { + this.remove(new QueryWrapper().lambda().eq(SysDepartRolePermission::getRoleId, roleId).eq(SysDepartRolePermission::getPermissionId, permissionId)); + } + } + } + + /** + * 从diff中找出main中没有的元素 + * @param main + * @param diff + * @return + */ + private List getDiff(String main, String diff){ + if(oConvertUtils.isEmpty(diff)) { + return null; + } + if(oConvertUtils.isEmpty(main)) { + return Arrays.asList(diff.split(",")); + } + + String[] mainArr = main.split(","); + String[] diffArr = diff.split(","); + Map map = new HashMap(5); + for (String string : mainArr) { + map.put(string, 1); + } + List res = new ArrayList(); + for (String key : diffArr) { + if(oConvertUtils.isNotEmpty(key) && !map.containsKey(key)) { + res.add(key); + } + } + return res; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRoleServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRoleServiceImpl.java new file mode 100644 index 0000000..3e44af2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRoleServiceImpl.java @@ -0,0 +1,47 @@ +package com.ghb.base.modules.system.service.impl; + +import com.ghb.base.modules.system.entity.SysDepartRole; +import com.ghb.base.modules.system.mapper.SysDepartRoleMapper; +import com.ghb.base.modules.system.mapper.SysDepartRolePermissionMapper; +import com.ghb.base.modules.system.mapper.SysDepartRoleUserMapper; +import com.ghb.base.modules.system.service.ISysDepartRoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * @Description: 部门角色 + * @Author: Ghb-boot + * @Date: 2020-02-12 + * @Version: V1.0 + */ +@Service +public class SysDepartRoleServiceImpl extends ServiceImpl implements ISysDepartRoleService { + + @Autowired + SysDepartRolePermissionMapper sysDepartRolePermissionMapper; + + @Autowired + SysDepartRoleUserMapper sysDepartRoleUserMapper; + + @Override + public List queryDeptRoleByDeptAndUser(String orgCode, String userId) { + return this.baseMapper.queryDeptRoleByDeptAndUser(orgCode,userId); + } + + /** + * 删除部门角色和对应关联表信息 + * @param ids + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDepartRole(List ids) { + this.baseMapper.deleteBatchIds(ids); + this.sysDepartRolePermissionMapper.deleteByRoleIds(ids); + this.sysDepartRoleUserMapper.deleteByRoleIds(ids); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRoleUserServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRoleUserServiceImpl.java new file mode 100644 index 0000000..749bbbc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartRoleUserServiceImpl.java @@ -0,0 +1,93 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysDepartRole; +import com.ghb.base.modules.system.entity.SysDepartRoleUser; +import com.ghb.base.modules.system.mapper.SysDepartRoleMapper; +import com.ghb.base.modules.system.mapper.SysDepartRoleUserMapper; +import com.ghb.base.modules.system.service.ISysDepartRoleUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Description: 部门角色人员信息 + * @Author: Ghb-boot + * @Date: 2020-02-13 + * @Version: V1.0 + */ +@Service +public class SysDepartRoleUserServiceImpl extends ServiceImpl implements ISysDepartRoleUserService { + @Autowired + private SysDepartRoleMapper sysDepartRoleMapper; + + @Override + public void deptRoleUserAdd(String userId, String newRoleId, String oldRoleId) { + List add = getDiff(oldRoleId,newRoleId); + if(add!=null && add.size()>0) { + List list = new ArrayList<>(); + for (String roleId : add) { + if(oConvertUtils.isNotEmpty(roleId)) { + SysDepartRoleUser rolepms = new SysDepartRoleUser(userId, roleId); + list.add(rolepms); + } + } + this.saveBatch(list); + } + List remove = getDiff(newRoleId,oldRoleId); + if(remove!=null && remove.size()>0) { + for (String roleId : remove) { + this.remove(new QueryWrapper().lambda().eq(SysDepartRoleUser::getUserId, userId).eq(SysDepartRoleUser::getDroleId, roleId)); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDeptRoleUser(List userIds, String depId) { + for(String userId : userIds){ + List sysDepartRoleList = sysDepartRoleMapper.selectList(new QueryWrapper().eq("depart_id",depId)); + List roleIds = sysDepartRoleList.stream().map(SysDepartRole::getId).collect(Collectors.toList()); + if(roleIds != null && roleIds.size()>0){ + QueryWrapper query = new QueryWrapper<>(); + query.eq("user_id",userId).in("drole_id",roleIds); + this.remove(query); + } + } + } + + /** + * 从diff中找出main中没有的元素 + * @param main + * @param diff + * @return + */ + private List getDiff(String main, String diff){ + if(oConvertUtils.isEmpty(diff)) { + return null; + } + if(oConvertUtils.isEmpty(main)) { + return Arrays.asList(diff.split(",")); + } + + String[] mainArr = main.split(","); + String[] diffArr = diff.split(","); + Map map = new HashMap(5); + for (String string : mainArr) { + map.put(string, 1); + } + List res = new ArrayList(); + for (String key : diffArr) { + if(oConvertUtils.isNotEmpty(key) && !map.containsKey(key)) { + res.add(key); + } + } + return res; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartServiceImpl.java new file mode 100644 index 0000000..9af2808 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDepartServiceImpl.java @@ -0,0 +1,2305 @@ +package com.ghb.base.modules.system.service.impl; +import org.jeecg.common.constant.CacheConstant; +import org.jeecg.common.util.RedisUtil; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import io.netty.util.internal.StringUtil; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.FillRuleConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.DepartCategoryEnum; +import com.ghb.base.common.exception.GhbBootBizTipException; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.*; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.mapper.*; +import com.ghb.base.modules.system.model.DepartIdModel; +import com.ghb.base.modules.system.model.SysDepartTreeModel; +import com.ghb.base.modules.system.service.ISysDepartService; +import com.ghb.base.modules.system.util.FindsDepartsChildrenUtil; +import com.ghb.base.modules.system.vo.*; +import com.ghb.base.modules.system.vo.lowapp.ExportDepartVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + *

+ * 部门表 服务实现类 + *

+ * + * @Author Steve + * @Since 2019-01-22 + */ +@Service +public class SysDepartServiceImpl extends ServiceImpl implements ISysDepartService { + + @Autowired + private SysUserDepartMapper userDepartMapper; + @Autowired + private SysDepartRoleMapper sysDepartRoleMapper; + @Autowired + private SysDepartPermissionMapper departPermissionMapper; + @Autowired + private SysDepartRolePermissionMapper departRolePermissionMapper; + @Autowired + private SysDepartRoleUserMapper departRoleUserMapper; + @Autowired + private SysUserMapper sysUserMapper; + @Autowired + private SysDepartMapper departMapper; + @Autowired + private SysPositionMapper sysPositionMapper; + @Autowired + private RedisUtil redisUtil; + @Autowired + private SysUserDepPostMapper sysUserDepPostMapper; + + @Override + public List queryMyDeptTreeList(String departIds) { + //根据部门id获取所负责部门 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + String[] codeArr = this.getMyDeptParentOrgCode(departIds); + // 代码逻辑说明: 【QQYUN-7320】查询部门没数据,导致报错空指针--- + if(ArrayUtil.isEmpty(codeArr)){ + return null; + } + for(int i=0;i listDepts = this.list(query); + for(int i=0;i listResult = FindsDepartsChildrenUtil.wrapTreeDataToTreeList(listDepts); + return listResult; + } + + /** + * queryTreeList 对应 queryTreeList 查询所有的部门数据,以树结构形式响应给前端 + */ + @Override + //@Cacheable(value = CacheConstant.SYS_DEPARTS_CACHE) + public List queryTreeList() { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + query.eq(SysDepart::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + query.eq(SysDepart::getDelFlag, CommonConstant.DEL_FLAG_0.toString()); + // 代码逻辑说明: 【QQYUN-13427】部门选择组件修改:需要过滤掉岗位 只保留 公司 子公司 部门--- + query.ne(SysDepart::getOrgCategory, DepartCategoryEnum.DEPART_CATEGORY_POST.getValue()); + query.orderByAsc(SysDepart::getDepartOrder); + List list = this.list(query); + //设置用户id,让前台显示 + this.setUserIdsByDepList(list); + // 调用wrapTreeDataToTreeList方法生成树状数据 + List listResult = FindsDepartsChildrenUtil.wrapTreeDataToTreeList(list); + return listResult; + } + + /** + * queryTreeList 根据部门id查询,前端回显调用 + */ + @Override + public List queryTreeList(String ids) { + List listResult=new ArrayList<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysDepart::getDelFlag, CommonConstant.DEL_FLAG_0.toString()); + // 代码逻辑说明: 【QQYUN-13427】部门选择组件修改:需要过滤掉岗位 只保留 公司 子公司 部门--- + query.ne(SysDepart::getOrgCategory,DepartCategoryEnum.DEPART_CATEGORY_POST.getValue()); + if(oConvertUtils.isNotEmpty(ids)){ + query.in(true,SysDepart::getId,ids.split(",")); + } + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + query.eq(SysDepart::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + query.orderByAsc(SysDepart::getDepartOrder); + List list= this.list(query); + for (SysDepart depart : list) { + listResult.add(new SysDepartTreeModel(depart)); + } + return listResult; + + } + + //@Cacheable(value = CacheConstant.SYS_DEPART_IDS_CACHE) + @Override + public List queryDepartIdTreeList() { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysDepart::getDelFlag, CommonConstant.DEL_FLAG_0.toString()); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + query.eq(SysDepart::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + query.orderByAsc(SysDepart::getDepartOrder); + List list = this.list(query); + // 调用wrapTreeDataToTreeList方法生成树状数据 + List listResult = FindsDepartsChildrenUtil.wrapTreeDataToDepartIdTreeList(list); + return listResult; + } + + /** + * saveDepartData 对应 add 保存用户在页面添加的新的部门对象数据 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void saveDepartData(SysDepart sysDepart, String username) { + if (sysDepart != null && username != null) { + // 代码逻辑说明: [QQYUN-4163]给部门表加个是否有子节点------------ + if (oConvertUtils.isEmpty(sysDepart.getParentId())) { + sysDepart.setParentId(""); + }else{ + //将父部门的设成不是叶子结点 + departMapper.setMainLeaf(sysDepart.getParentId(),CommonConstant.NOT_LEAF); + } + //String s = UUID.randomUUID().toString().replace("-", ""); + sysDepart.setId(IdWorker.getIdStr(sysDepart)); + // 先判断该对象有无父级ID,有则意味着不是最高级,否则意味着是最高级 + // 获取父级ID + String parentId = sysDepart.getParentId(); + // 代码逻辑说明: 部门编码规则生成器做成公用配置 + JSONObject formData = new JSONObject(); + formData.put("parentId",parentId); + String[] codeArray = (String[]) FillRuleUtil.executeRule(FillRuleConstant.DEPART,formData); + sysDepart.setOrgCode(codeArray[0]); + String orgType = codeArray[1]; + sysDepart.setOrgType(String.valueOf(orgType)); + sysDepart.setCreateTime(new Date()); + sysDepart.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); + //新添加的部门是叶子节点 + sysDepart.setIzLeaf(CommonConstant.IS_LEAF); + // 【QQYUN-7172】数据库默认值兼容 + if (oConvertUtils.isEmpty(sysDepart.getOrgCategory())) { + if (oConvertUtils.isEmpty(sysDepart.getParentId())) { + sysDepart.setOrgCategory("1"); + } else { + sysDepart.setOrgCategory("2"); + } + } + this.save(sysDepart); + //新增部门的时候新增负责部门 + if(oConvertUtils.isNotEmpty(sysDepart.getDirectorUserIds())){ + this.addDepartByUserIds(sysDepart,sysDepart.getDirectorUserIds()); + } + } + + } + + /** + * saveDepartData 的调用方法,生成部门编码和部门类型(作废逻辑) + * @deprecated + * @param parentId + * @return + */ + private String[] generateOrgCode(String parentId) { + // 代码逻辑说明: 组织机构添加数据代码调整 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + LambdaQueryWrapper query1 = new LambdaQueryWrapper(); + String[] strArray = new String[2]; + // 创建一个List集合,存储查询返回的所有SysDepart对象 + List departList = new ArrayList<>(); + // 定义新编码字符串 + String newOrgCode = ""; + // 定义旧编码字符串 + String oldOrgCode = ""; + // 定义部门类型 + String orgType = ""; + // 如果是最高级,则查询出同级的org_code, 调用工具类生成编码并返回 + if (StringUtil.isNullOrEmpty(parentId)) { + // 线判断数据库中的表是否为空,空则直接返回初始编码 + query1.eq(SysDepart::getParentId, "").or().isNull(SysDepart::getParentId); + query1.orderByDesc(SysDepart::getOrgCode); + departList = this.list(query1); + if(departList == null || departList.size() == 0) { + strArray[0] = YouBianCodeUtil.getNextYouBianCode(null); + strArray[1] = "1"; + return strArray; + }else { + SysDepart depart = departList.get(0); + oldOrgCode = depart.getOrgCode(); + orgType = depart.getOrgType(); + newOrgCode = YouBianCodeUtil.getNextYouBianCode(oldOrgCode); + } + } else { // 反之则查询出所有同级的部门,获取结果后有两种情况,有同级和没有同级 + // 封装查询同级的条件 + query.eq(SysDepart::getParentId, parentId); + // 降序排序 + query.orderByDesc(SysDepart::getOrgCode); + // 查询出同级部门的集合 + List parentList = this.list(query); + // 查询出父级部门 + SysDepart depart = this.getById(parentId); + // 获取父级部门的Code + String parentCode = depart.getOrgCode(); + // 根据父级部门类型算出当前部门的类型 + orgType = String.valueOf(Integer.valueOf(depart.getOrgType()) + 1); + // 处理同级部门为null的情况 + if (parentList == null || parentList.size() == 0) { + // 直接生成当前的部门编码并返回 + newOrgCode = YouBianCodeUtil.getSubYouBianCode(parentCode, null); + } else { //处理有同级部门的情况 + // 获取同级部门的编码,利用工具类 + String subCode = parentList.get(0).getOrgCode(); + // 返回生成的当前部门编码 + newOrgCode = YouBianCodeUtil.getSubYouBianCode(parentCode, subCode); + } + } + // 返回最终封装了部门编码和部门类型的数组 + strArray[0] = newOrgCode; + strArray[1] = orgType; + return strArray; + } + + + /** + * removeDepartDataById 对应 delete方法 根据ID删除相关部门数据 + * + */ + /* + * @Override + * + * @Transactional public boolean removeDepartDataById(String id) { + * System.out.println("要删除的ID 为=============================>>>>>"+id); boolean + * flag = this.removeById(id); return flag; } + */ + + /** + * updateDepartDataById 对应 edit 根据部门主键来更新对应的部门数据 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateDepartDataById(SysDepart sysDepart, String username) { + if (sysDepart != null && username != null) { + sysDepart.setUpdateTime(new Date()); + sysDepart.setUpdateBy(username); + //验证部门类型 + this.verifyOrgCategory(sysDepart); + this.updateById(sysDepart); + //修改部门管理的时候,修改负责部门 + this.updateChargeDepart(sysDepart); + //redis清除缓存key + redisUtil.removeAll(CommonConstant.DEPART_NAME_REDIS_KEY_PRE); + return true; + } else { + return false; + } + + } + + /** + * 验证部门类型 + * + * @param sysDepart + */ + private void verifyOrgCategory(SysDepart sysDepart) { + //update-begin---author:wangshuai---date:2025-08-21---for: 当部门类型为岗位的时候,需要查看是否存在下级,存在下级无法变更为岗位--- + //如果是岗位的情况下,不能存在子级 + if (oConvertUtils.isNotEmpty(sysDepart.getOrgCategory()) && DepartCategoryEnum.DEPART_CATEGORY_POST.getValue().equals(sysDepart.getOrgCategory())) { + long count = this.count(new QueryWrapper().lambda().eq(SysDepart::getParentId, sysDepart.getId())); + if (count > 0) { + throw new GhbBootBizTipException("当前子公司/部门下存在子级,无法变更为岗位!"); + } + } + //如果是子公司的情况下,则上级不能为部门或者岗位 + if (oConvertUtils.isNotEmpty(sysDepart.getOrgCategory()) && DepartCategoryEnum.DEPART_CATEGORY_SUB_COMPANY.getValue().equals(sysDepart.getOrgCategory()) + && oConvertUtils.isNotEmpty(sysDepart.getParentId())) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDepart::getId, sysDepart.getParentId()); + queryWrapper.in(SysDepart::getOrgCategory, DepartCategoryEnum.DEPART_CATEGORY_POST.getValue(), DepartCategoryEnum.DEPART_CATEGORY_DEPART.getValue()); + long count = this.count(queryWrapper); + if (count > 0) { + throw new GhbBootBizTipException("当前父级为部门或岗位,无法变更为子公司!"); + } + } + //如果是部门的情况下,下级不能为子公司或者公司 + if (oConvertUtils.isNotEmpty(sysDepart.getOrgCategory()) && DepartCategoryEnum.DEPART_CATEGORY_DEPART.getValue().equals(sysDepart.getOrgCategory())) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDepart::getParentId, sysDepart.getId()); + queryWrapper.in(SysDepart::getOrgCategory, DepartCategoryEnum.DEPART_CATEGORY_COMPANY.getValue(), DepartCategoryEnum.DEPART_CATEGORY_SUB_COMPANY.getValue()); + long count = this.count(queryWrapper); + if (count > 0) { + throw new GhbBootBizTipException("当前子级存在子公司,无法变更为部门!"); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteBatchWithChildren(List ids) { + //存放子级的id + List idList = new ArrayList(); + //存放父级的id + List parentIdList = new ArrayList<>(); + for(String id: ids) { + idList.add(id); + //此步骤是为了删除子级 + this.checkChildrenExists(id, idList); + // 代码逻辑说明: 【QQYUN-5757】批量删除部门时未正确置为叶子节点 ------------ + SysDepart depart = this.getDepartById(id); + if (oConvertUtils.isNotEmpty(depart.getParentId())) { + if (!parentIdList.contains(depart.getParentId())) { + parentIdList.add(depart.getParentId()); + } + } + } + this.removeByIds(idList); + //再删除前需要获取父级id,不然会一直为空 + this.setParentDepartIzLeaf(parentIdList); + //根据部门id获取部门角色id + List roleIdList = new ArrayList<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.select(SysDepartRole::getId).in(SysDepartRole::getDepartId, idList); + List depRoleList = sysDepartRoleMapper.selectList(query); + for(SysDepartRole deptRole : depRoleList){ + roleIdList.add(deptRole.getId()); + } + //根据部门id删除用户与部门关系 + userDepartMapper.delete(new LambdaQueryWrapper().in(SysUserDepart::getDepId,idList)); + //根据部门id删除部门授权 + departPermissionMapper.delete(new LambdaQueryWrapper().in(SysDepartPermission::getDepartId,idList)); + //根据部门id删除部门角色 + sysDepartRoleMapper.delete(new LambdaQueryWrapper().in(SysDepartRole::getDepartId,idList)); + if(roleIdList != null && roleIdList.size()>0){ + //根据角色id删除部门角色授权 + departRolePermissionMapper.delete(new LambdaQueryWrapper().in(SysDepartRolePermission::getRoleId,roleIdList)); + //根据角色id删除部门角色用户信息 + departRoleUserMapper.delete(new LambdaQueryWrapper().in(SysDepartRoleUser::getDroleId,roleIdList)); + } + //删除岗位信息 + this.deleteDepartPostByDepIds(idList); + } + + @Override + public List getSubDepIdsByDepId(String departId) { + return this.baseMapper.getSubDepIdsByDepId(departId); + } + + @Override + public List getMySubDepIdsByDepId(String departIds) { + //根据部门id获取所负责部门 + String[] codeArr = this.getMyDeptParentOrgCode(departIds); + if(codeArr==null || codeArr.length==0){ + return null; + } + return this.baseMapper.getSubDepIdsByOrgCodes(codeArr); + } + + /** + *

+ * 根据关键字搜索相关的部门数据 + *

+ */ + @Override + public List searchByKeyWord(String keyWord, String myDeptSearch, String departIds, String orgCategory, String depIds) { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + List newList = new ArrayList<>(); + //myDeptSearch不为空时为我的部门搜索,只搜索所负责部门 + if(!StringUtil.isNullOrEmpty(myDeptSearch)){ + //departIds 为空普通用户或没有管理部门 + if(StringUtil.isNullOrEmpty(departIds)){ + return newList; + } + //根据部门id获取所负责部门 + String[] codeArr = this.getMyDeptParentOrgCode(departIds); + // 代码逻辑说明: /issues/3311 当用户属于两个部门的时候,且这两个部门没有上下级关系,我的部门-部门名称查询条件模糊搜索失效! + if (codeArr != null && codeArr.length > 0) { + query.nested(i -> { + for (String s : codeArr) { + i.or().likeRight(SysDepart::getOrgCode, s); + } + }); + } + query.eq(SysDepart::getDelFlag, CommonConstant.DEL_FLAG_0.toString()); + } + query.like(SysDepart::getDepartName, keyWord); + //需要根据部门类型进行数据筛选 + if(oConvertUtils.isNotEmpty(orgCategory)){ + query.in(SysDepart::getOrgCategory, Arrays.asList(orgCategory.split(SymbolConstant.COMMA))); + }else{ + query.ne(SysDepart::getOrgCategory,DepartCategoryEnum.DEPART_CATEGORY_POST.getValue()); + } + //如果前端传过来的部门id不为空的时候,说明是系统用户根据所属部门选择主岗位或者兼职岗位,需要进行数据过滤 + if(oConvertUtils.isNotEmpty(depIds)){ + List codeList = baseMapper.getDepCodeByDepIds(Arrays.asList(depIds.split(SymbolConstant.COMMA))); + if(CollectionUtil.isNotEmpty(codeList)){ + query.and(i -> { + for (String code : codeList) { + i.or().likeRight(SysDepart::getOrgCode,code); + } + }); + } + } + // 代码逻辑说明: [bugfree号]组织机构搜索回显优化-------------------- + SysDepartTreeModel model = new SysDepartTreeModel(); + List departList = this.list(query); + if(departList.size() > 0) { + for(SysDepart depart : departList) { + model = new SysDepartTreeModel(depart); + model.setChildren(null); + newList.add(model); + } + return newList; + } + return null; + } + + /** + * 根据部门id删除并且删除其可能存在的子级任何部门 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean delete(String id) { + List idList = new ArrayList<>(); + idList.add(id); + this.checkChildrenExists(id, idList); + //清空部门树内存 + //FindsDepartsChildrenUtil.clearDepartIdModel(); + boolean ok = this.removeByIds(idList); + //根据部门id获取部门角色id + List roleIdList = new ArrayList<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.select(SysDepartRole::getId).in(SysDepartRole::getDepartId, idList); + List depRoleList = sysDepartRoleMapper.selectList(query); + for(SysDepartRole deptRole : depRoleList){ + roleIdList.add(deptRole.getId()); + } + //根据部门id删除用户与部门关系 + userDepartMapper.delete(new LambdaQueryWrapper().in(SysUserDepart::getDepId,idList)); + //根据部门id删除部门授权 + departPermissionMapper.delete(new LambdaQueryWrapper().in(SysDepartPermission::getDepartId,idList)); + //根据部门id删除部门角色 + sysDepartRoleMapper.delete(new LambdaQueryWrapper().in(SysDepartRole::getDepartId,idList)); + if(roleIdList != null && roleIdList.size()>0){ + //根据角色id删除部门角色授权 + departRolePermissionMapper.delete(new LambdaQueryWrapper().in(SysDepartRolePermission::getRoleId,roleIdList)); + //根据角色id删除部门角色用户信息 + departRoleUserMapper.delete(new LambdaQueryWrapper().in(SysDepartRoleUser::getDroleId,roleIdList)); + } + return ok; + } + + /** + * delete 方法调用 + * @param id + * @param idList + */ + private void checkChildrenExists(String id, List idList) { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysDepart::getParentId,id); + List departList = this.list(query); + if(departList != null && departList.size() > 0) { + for(SysDepart depart : departList) { + idList.add(depart.getId()); + this.checkChildrenExists(depart.getId(), idList); + } + } + } + + @Override + public List queryUserDeparts(String userId) { + List sysDeparts = baseMapper.queryUserDeparts(userId); + sysDeparts.stream() + .filter(depart -> oConvertUtils.isNotEmpty(depart) && + oConvertUtils.isNotEmpty(depart.getOrgCode())) + .forEach(depart -> { + String orgCategory = depart.getOrgCategory(); + if(DepartCategoryEnum.DEPART_CATEGORY_DEPART.getValue().equalsIgnoreCase(orgCategory)){ + String departPathName = this.getDepartPathNameByOrgCode(depart.getOrgCode(), ""); + depart.setDepartPathName(departPathName); + } + }); + return sysDeparts; + } + + @Override + public List queryDepartsByUsername(String username) { + return baseMapper.queryDepartsByUsername(username); + } + + @Override + public List queryDepartsByUserId(String userId) { + List list = baseMapper.queryDepartsByUserId(userId); + return list; + } + + @Override + public Map> queryDepartIdsByUserIds(Collection userIds) { + List> mapList = baseMapper.queryDepartIdsByUserIds(userIds); + if (CollectionUtils.isEmpty(mapList)) { + return Map.of(); + } + Map> res = new HashMap<>(); + for (Map map : mapList) { + String userId = map.get("user_id"); + String departId = map.get("depart_id"); + res.computeIfAbsent(userId, k -> new ArrayList<>()).add(departId); + } + return res; + } + + /** + * 根据用户所负责部门ids获取父级部门编码 + * @param departIds + * @return + */ + private String[] getMyDeptParentOrgCode(String departIds){ + //根据部门id查询所负责部门 + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysDepart::getDelFlag, CommonConstant.DEL_FLAG_0.toString()); + if(oConvertUtils.isNotEmpty(departIds)){ + query.in(SysDepart::getId, Arrays.asList(departIds.split(","))); + } + + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + query.eq(SysDepart::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + query.orderByAsc(SysDepart::getOrgCode); + List list = this.list(query); + //查找根部门 + if(list == null || list.size()==0){ + return null; + } + String orgCode = this.getMyDeptParentNode(list); + String[] codeArr = orgCode.split(","); + return codeArr; + } + + /** + * 获取负责部门父节点 + * @param list + * @return + */ + private String getMyDeptParentNode(List list){ + Map map = new HashMap(5); + //1.先将同一公司归类 + for(SysDepart dept : list){ + String code = dept.getOrgCode().substring(0,3); + if(map.containsKey(code)){ + String mapCode = map.get(code)+","+dept.getOrgCode(); + map.put(code,mapCode); + }else{ + map.put(code,dept.getOrgCode()); + } + } + StringBuffer parentOrgCode = new StringBuffer(); + //2.获取同一公司的根节点 + for(String str : map.values()){ + String[] arrStr = str.split(","); + parentOrgCode.append(",").append(this.getMinLengthNode(arrStr)); + } + return parentOrgCode.substring(1); + } + + /** + * 获取同一公司中部门编码长度最小的部门 + * @param str + * @return + */ + private String getMinLengthNode(String[] str){ + int min =str[0].length(); + StringBuilder orgCodeBuilder = new StringBuilder(str[0]); + for(int i =1;i queryTreeByKeyWord(String keyWord) { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysDepart::getDelFlag, CommonConstant.DEL_FLAG_0.toString()); + query.orderByAsc(SysDepart::getDepartOrder); + List list = this.list(query); + // 调用wrapTreeDataToTreeList方法生成树状数据 + List listResult = FindsDepartsChildrenUtil.wrapTreeDataToTreeList(list); + List treelist =new ArrayList<>(); + if(StringUtils.isNotBlank(keyWord)){ + this.getTreeByKeyWord(keyWord,listResult,treelist); + }else{ + return listResult; + } + return treelist; + } + + /** + * 根据parentId查询部门树 + * @param parentId + * @param ids 前端回显传递 + * @param primaryKey 主键字段(id或者orgCode) + * @return + */ + @Override + public List queryTreeListByPid(String parentId,String ids, String primaryKey, String orgCategory) { + Consumer> square = i -> { + if (oConvertUtils.isNotEmpty(ids)) { + if (CommonConstant.DEPART_KEY_ORG_CODE.equals(primaryKey)) { + i.in(SysDepart::getOrgCode, ids.split(SymbolConstant.COMMA)); + } else { + i.in(SysDepart::getId, ids.split(SymbolConstant.COMMA)); + } + } else { + if(oConvertUtils.isEmpty(parentId)){ + i.and(q->q.isNull(true,SysDepart::getParentId).or().eq(true,SysDepart::getParentId,"")); + }else{ + i.eq(true,SysDepart::getParentId,parentId); + } + } + }; + LambdaQueryWrapper lqw=new LambdaQueryWrapper<>(); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的 SASS 控制 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + lqw.eq(SysDepart::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + //------------------------------------------------------------------------------------------------ + lqw.eq(true,SysDepart::getDelFlag,CommonConstant.DEL_FLAG_0.toString()); + // 按 orgCategory 过滤:传入则精确匹配,否则默认排除岗位 + if (oConvertUtils.isNotEmpty(orgCategory)) { + lqw.in(SysDepart::getOrgCategory, (Object[]) orgCategory.split(SymbolConstant.COMMA)); + } else { + // 代码逻辑说明: 【QQYUN-13427】部门选择组件修改:需要过滤掉岗位 只保留 公司 子公司 部门--- + lqw.ne(SysDepart::getOrgCategory, DepartCategoryEnum.DEPART_CATEGORY_POST.getValue()); + } + lqw.func(square); + // 代码逻辑说明: [VUEN-1143]排序不对,vue3和2应该都有问题,应该按照升序排------------ + lqw.orderByAsc(SysDepart::getDepartOrder); + List list = list(lqw); + //设置用户id,让前台显示 + this.setUserIdsByDepList(list); + List records = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + SysDepart depart = list.get(i); + // 代码逻辑说明: 【QQYUN-13427】部门选择组件修改:需要过滤掉岗位 只保留 公司 子公司 部门--- + long count = getNoDepartPostCount(depart.getId()); + if(count == 0){ + depart.setIzLeaf(CommonConstant.IS_LEAF); + } + SysDepartTreeModel treeModel = new SysDepartTreeModel(depart); + //TODO 异步树加载key拼接__+时间戳,以便于每次展开节点会刷新数据 + //treeModel.setKey(treeModel.getKey()+"__"+System.currentTimeMillis()); + records.add(treeModel); + } + return records; + } + + /** + * 获取部门数量 + * @param departId + * @return + */ + private long getNoDepartPostCount(String departId) { + LambdaQueryWrapper queryNoPosition = new LambdaQueryWrapper<>(); + queryNoPosition.ne(SysDepart::getOrgCategory,DepartCategoryEnum.DEPART_CATEGORY_POST.getValue()); + queryNoPosition.eq(SysDepart::getParentId,departId); + return this.count(queryNoPosition); + } + + /** + * 部门管理异步树 + * + * @param parentId + * @param ids + * @param primaryKey + * @param departIds + * @return + */ + @Override + public List queryDepartAndPostTreeSync(String parentId, String ids, String primaryKey, + String departIds, String orgName) { + Consumer> square = i -> { + if (oConvertUtils.isNotEmpty(ids)) { + if (CommonConstant.DEPART_KEY_ORG_CODE.equals(primaryKey)) { + i.in(SysDepart::getOrgCode, ids.split(SymbolConstant.COMMA)); + } else { + i.in(SysDepart::getId, ids.split(SymbolConstant.COMMA)); + } + } else { + if(oConvertUtils.isEmpty(parentId)){ + // 代码逻辑说明: 如果前端传过来的部门id不为空的时候,说明是系统用户根据所属部门选择主岗位或者兼职岗位--- + if(oConvertUtils.isNotEmpty(departIds)){ + i.in(SysDepart::getId,Arrays.asList(departIds.split(SymbolConstant.COMMA))); + }else{ + if(oConvertUtils.isEmpty(orgName)){ + i.and(q->q.isNull(true,SysDepart::getParentId).or().eq(true,SysDepart::getParentId,"")); + }else{ + i.like(SysDepart::getDepartName, orgName); + } + } + }else{ + i.eq(true,SysDepart::getParentId,parentId); + } + } + }; + LambdaQueryWrapper lqw=new LambdaQueryWrapper<>(); + //是否开启系统管理模块的 SASS 控制 + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + lqw.eq(SysDepart::getTenantId, oConvertUtils.getInt(TenantContext.getTenant(), 0)); + } + lqw.eq(true,SysDepart::getDelFlag,CommonConstant.DEL_FLAG_0.toString()); + lqw.func(square); + lqw.orderByAsc(SysDepart::getDepartOrder); + List list = list(lqw); + //设置用户id,让前台显示 + this.setUserIdsByDepList(list); + List departIdList = new ArrayList<>(); + //如果前端传过来的部门id不为空的时候,说明是系统用户根据所属部门选择主岗位或者兼职岗位 + if(oConvertUtils.isNotEmpty(departIds) && oConvertUtils.isEmpty(parentId)){ + departIdList = list.stream().map(SysDepart::getId).toList(); + } + List records = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + SysDepart depart = list.get(i); + //如果部门id和父级部门id再同一列的时候,不用添加到树结构里面去了 + if(oConvertUtils.isNotEmpty(departIds) && oConvertUtils.isEmpty(parentId) + && departIdList.contains(depart.getParentId())){ + continue; + } + SysDepartTreeModel treeModel = new SysDepartTreeModel(depart); + records.add(treeModel); + } + return records; + } + + @Override + public JSONObject queryAllParentIdByDepartId(String departId) { + JSONObject result = new JSONObject(); + for (String id : departId.split(SymbolConstant.COMMA)) { + JSONObject all = this.queryAllParentId("id", id); + result.put(id, all); + } + return result; + } + + @Override + public JSONObject queryAllParentIdByOrgCode(String orgCode) { + JSONObject result = new JSONObject(); + for (String code : orgCode.split(SymbolConstant.COMMA)) { + JSONObject all = this.queryAllParentId("org_code", code); + result.put(code, all); + } + return result; + } + + /** + * 查询某个部门的所有父ID信息 + * + * @param fieldName 字段名 + * @param value 值 + */ + private JSONObject queryAllParentId(String fieldName, String value) { + JSONObject data = new JSONObject(); + // 父ID集合,有序 + data.put("parentIds", new JSONArray()); + // 父ID的部门数据,key是id,value是数据 + data.put("parentMap", new JSONObject()); + this.queryAllParentIdRecursion(fieldName, value, data); + return data; + } + + /** + * 递归调用查询父部门接口 + */ + private void queryAllParentIdRecursion(String fieldName, String value, JSONObject data) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(fieldName, value); + SysDepart depart = super.getOne(queryWrapper); + if (depart != null) { + data.getJSONArray("parentIds").add(0, depart.getId()); + data.getJSONObject("parentMap").put(depart.getId(), depart); + if (oConvertUtils.isNotEmpty(depart.getParentId())) { + this.queryAllParentIdRecursion("id", depart.getParentId(), data); + } + } + } + + @Override + public SysDepart queryCompByOrgCode(String orgCode) { + int length = YouBianCodeUtil.ZHANWEI_LENGTH; + String compyOrgCode = orgCode.substring(0,length); + return this.baseMapper.queryCompByOrgCode(compyOrgCode); + } + /** + * 根据id查询下级部门 + * @param pid + * @return + */ + @Override + public List queryDeptByPid(String pid) { + return this.baseMapper.queryDeptByPid(pid); + } + /** + * 根据关键字筛选部门信息 + * @param keyWord + * @return + */ + public void getTreeByKeyWord(String keyWord,List allResult,List newResult){ + for (SysDepartTreeModel model:allResult) { + if (model.getDepartName().contains(keyWord)){ + newResult.add(model); + continue; + }else if(model.getChildren()!=null){ + getTreeByKeyWord(keyWord,model.getChildren(),newResult); + } + } + } + + /** + * 通过用户id设置负责部门 + * @param sysDepart SysDepart部门对象 + * @param userIds 多个负责用户id + */ + public void addDepartByUserIds(SysDepart sysDepart, String userIds) { + //获取部门id,保存到用户 + String departId = sysDepart.getId(); + //循环用户id + String[] userIdArray = userIds.split(","); + for (String userId:userIdArray) { + //查询用户表增加负责部门 + SysUser sysUser = sysUserMapper.selectById(userId); + //如果部门id不为空,那么就需要拼接 + if(oConvertUtils.isNotEmpty(sysUser.getDepartIds())){ + if(!sysUser.getDepartIds().contains(departId)) { + sysUser.setDepartIds(sysUser.getDepartIds() + "," + departId); + } + }else{ + sysUser.setDepartIds(departId); + } + //设置身份为上级 + sysUser.setUserIdentity(CommonConstant.USER_IDENTITY_2); + //跟新用户表 + sysUserMapper.updateById(sysUser); + //判断当前用户是否包含所属部门 + List userDepartList = userDepartMapper.getUserDepartByUid(userId); + boolean isExistDepId = userDepartList.stream().anyMatch(item -> departId.equals(item.getDepId())); + //如果不存在需要设置所属部门 + if(!isExistDepId){ + userDepartMapper.insert(new SysUserDepart(userId,departId)); + } + } + } + + /** + * 修改用户负责部门 + * @param sysDepart SysDepart对象 + */ + private void updateChargeDepart(SysDepart sysDepart) { + //新的用户id + String directorIds = sysDepart.getDirectorUserIds(); + //旧的用户id(数据库中存在的) + String oldDirectorIds = sysDepart.getOldDirectorUserIds(); + String departId = sysDepart.getId(); + //如果用户id为空,那么用户的负责部门id应该去除 + if(oConvertUtils.isEmpty(directorIds)){ + this.deleteChargeDepId(departId,null); + }else if(oConvertUtils.isNotEmpty(directorIds) && oConvertUtils.isEmpty(oldDirectorIds)){ + //如果用户id不为空但是用户原来负责部门的用户id为空 + this.addDepartByUserIds(sysDepart,directorIds); + }else{ + //都不为空,需要比较,进行添加或删除 + //找到新的负责部门用户id与原来负责部门的用户id,进行删除 + List userIdList = Arrays.stream(oldDirectorIds.split(",")).filter(item -> !directorIds.contains(item)).collect(Collectors.toList()); + for (String userId:userIdList){ + this.deleteChargeDepId(departId,userId); + } + //找到原来负责部门的用户id与新的负责部门用户id,进行新增 + String addUserIds = Arrays.stream(directorIds.split(",")).filter(item -> !oldDirectorIds.contains(item)).collect(Collectors.joining(",")); + if(oConvertUtils.isNotEmpty(addUserIds)){ + this.addDepartByUserIds(sysDepart,addUserIds); + } + } + } + + /** + * 删除用户负责部门 + * @param departId 部门id + * @param userId 用户id + */ + private void deleteChargeDepId(String departId,String userId){ + //先查询负责部门的用户id,因为负责部门的id使用逗号拼接起来的 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.like(SysUser::getDepartIds,departId); + //删除全部的情况下用户id不存在 + if(oConvertUtils.isNotEmpty(userId)){ + query.eq(SysUser::getId,userId); + } + List userList = sysUserMapper.selectList(query); + for (SysUser sysUser:userList) { + //将不存在的部门id删除掉 + String departIds = sysUser.getDepartIds(); + List list = new ArrayList<>(Arrays.asList(departIds.split(","))); + list.remove(departId); + //删除之后再将新的id用逗号拼接起来进行更新 + String newDepartIds = String.join(",",list); + sysUser.setDepartIds(newDepartIds); + sysUserMapper.updateById(sysUser); + } + } + + /** + * 通过部门集合为部门设置用户id,用于前台展示 + * @param departList 部门集合 + */ + private void setUserIdsByDepList(List departList) { + //查询负责部门不为空的情况 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.isNotNull(SysUser::getDepartIds); + List users = sysUserMapper.selectList(query); + Map map = new HashMap(5); + //先循环一遍找到不同的负责部门id + for (SysUser user:users) { + String departIds = user.getDepartIds(); + String[] departIdArray = departIds.split(","); + for (String departId:departIdArray) { + //mao中包含部门key,负责用户直接拼接 + if(map.containsKey(departId)){ + String userIds = map.get(departId) + "," + user.getId(); + map.put(departId,userIds); + }else{ + map.put(departId,user.getId()); + } + } + } + //循环部门集合找到部门id对应的负责用户 + for (SysDepart sysDepart:departList) { + if(map.containsKey(sysDepart.getId())){ + sysDepart.setDirectorUserIds(map.get(sysDepart.getId()).toString()); + } + } + } + + /** + * 获取我的部门已加入的公司 + * @return + */ + @Override + public List getMyDepartList() { + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String userId = user.getId(); + //字典code集合 + List list = new ArrayList<>(); + //查询我加入的部门 + List sysDepartList = this.baseMapper.queryUserDeparts(userId); + for (SysDepart sysDepart : sysDepartList) { + //获取一级部门编码 + String orgCode = sysDepart.getOrgCode(); + if (YouBianCodeUtil.ZHANWEI_LENGTH <= orgCode.length()) { + int length = YouBianCodeUtil.ZHANWEI_LENGTH; + String companyOrgCode = orgCode.substring(0, length); + list.add(companyOrgCode); + } + } + //字典code集合不为空 + if (oConvertUtils.isNotEmpty(list)) { + //查询一级部门的数据 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.select(SysDepart::getDepartName, SysDepart::getId, SysDepart::getOrgCode); + query.eq(SysDepart::getDelFlag, String.valueOf(CommonConstant.DEL_FLAG_0)); + query.in(SysDepart::getOrgCode, list); + return this.baseMapper.selectList(query); + } + return null; + } + + @Override + public void deleteDepart(String id) { + //删除部门设置父级的叶子结点 + this.setIzLeaf(id); + this.delete(id); + //删除部门用户关系表 + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysUserDepart::getDepId, id); + this.userDepartMapper.delete(query); + } + + @Override + public List queryBookDepTreeSync(String parentId, Integer tenantId, String departName) { + List list = departMapper.queryBookDepTreeSync(parentId,tenantId,departName); + List records = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + SysDepart depart = list.get(i); + SysDepartTreeModel treeModel = new SysDepartTreeModel(depart); + records.add(treeModel); + } + return records; + } + + @Override + public SysDepart getDepartById(String id) { + return departMapper.getDepartById(id); + } + + @Override + public IPage getMaxCodeDepart(Page page, String parentId) { + return page.setRecords(departMapper.getMaxCodeDepart(page,parentId)); + } + + @Override + public void updateIzLeaf(String id, Integer izLeaf) { + departMapper.setMainLeaf(id,izLeaf); + } + + /** + * 设置父级节点是否存在叶子结点 + * @param id + */ + private void setIzLeaf(String id) { + SysDepart depart = this.getDepartById(id); + String parentId = depart.getParentId(); + if(oConvertUtils.isNotEmpty(parentId)){ + Long count = this.count(new QueryWrapper().lambda().eq(SysDepart::getParentId, parentId)); + if(count == 1){ + //若父节点无其他子节点,则该父节点是叶子节点 + departMapper.setMainLeaf(parentId, CommonConstant.IS_LEAF); + } + } + } + + //========================begin 零代码下部门与人员导出 ================================================================== + + @Override + public List getExcelDepart(int tenantId) { + //获取父级部门 + List parentDepart = departMapper.getDepartList("",tenantId); + //子部门 + List childrenDepart = new ArrayList<>(); + //把一级部门名称放在里面 + List exportDepartVoList = new ArrayList<>(); + //存放部门一级id避免重复 + List departIdList = new ArrayList<>(); + for (ExportDepartVo departVo:parentDepart) { + departIdList.add(departVo.getId()); + departVo.setDepartNameUrl(departVo.getDepartName()); + exportDepartVoList.add(departVo); + //创建路径 + List path = new ArrayList<>(); + path.add(departVo.getDepartName()); + //创建子部门路径 + findPath(departVo, path, tenantId,childrenDepart,departIdList); + path.clear(); + } + exportDepartVoList.addAll(childrenDepart); + childrenDepart.clear(); + departIdList.clear(); + return exportDepartVoList; + } + + /** + * 寻找部门路径 + * @param departVo 部门vo + * @param path 部门路径 + * @param tenantId 租户id + * @param childrenDepart 子部门 + * @param departIdList 部门id集合 + */ + private void findPath(ExportDepartVo departVo, List path,Integer tenantId,List childrenDepart,List departIdList) { + //获取租户id和部门父id获取的部门数据 + List departList = departMapper.getDepartList(departVo.getId(), tenantId); + //部门为空判断 + if (departList == null || departList.size() <= 0) { + if(!departIdList.contains(departVo.getId())){ + departVo.setDepartNameUrl(String.join(SymbolConstant.SINGLE_SLASH,path)); + childrenDepart.add(departVo); + } + return; + } + + for (int i = 0; i < departList.size(); i++) { + ExportDepartVo exportDepartVo = departList.get(i); + //存放子级路径 + List cPath = new ArrayList<>(); + cPath.addAll(path); + cPath.add(exportDepartVo.getDepartName()); + if(!departIdList.contains(departVo.getId())){ + departIdList.add(departVo.getId()); + departVo.setDepartNameUrl(String.join(SymbolConstant.SINGLE_SLASH,path)); + childrenDepart.add(departVo); + } + findPath(exportDepartVo,cPath ,tenantId, childrenDepart,departIdList); + } + } + //========================end 零代码下部门与人员导出 ================================================================== + + //========================begin 零代码下部门与人员导入 ================================================================== + @Override + public void importExcel(List listSysDeparts, List errorMessageList) { + int num = 0; + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + + //部门路径排序 + Collections.sort(listSysDeparts, new Comparator() { + @Override + public int compare(ExportDepartVo o1, ExportDepartVo o2) { + if(oConvertUtils.isNotEmpty(o1.getDepartNameUrl()) && oConvertUtils.isNotEmpty(o2.getDepartNameUrl())){ + int oldLength = o1.getDepartNameUrl().split(SymbolConstant.SINGLE_SLASH).length; + int newLength = o2.getDepartNameUrl().split(SymbolConstant.SINGLE_SLASH).length; + return oldLength - newLength; + }else{ + return 0; + } + } + }); + //存放部门数据的map + Map departMap = new HashMap<>(); + //循环第二遍导入数据 + for (ExportDepartVo exportDepartVo : listSysDeparts) { + SysDepart sysDepart = new SysDepart(); + // orgCode编码长度 + int codeLength = YouBianCodeUtil.ZHANWEI_LENGTH; + Boolean izExport = false; + try { + izExport = this.addDepartByName(exportDepartVo.getDepartNameUrl(),exportDepartVo.getDepartName(),sysDepart,errorMessageList,tenantId,departMap,num); + } catch (Exception e) { + //没有查找到parentDept + } + //没有错误的时候才会导入数据 + if(izExport){ + sysDepart.setOrgType(sysDepart.getOrgCode().length()/codeLength+""); + sysDepart.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); + sysDepart.setOrgCategory("1"); + sysDepart.setTenantId(tenantId); + ImportExcelUtil.importDateSaveOne(sysDepart, ISysDepartService.class, errorMessageList, num, CommonConstant.SQL_INDEX_UNIQ_DEPART_ORG_CODE); + departMap.put(exportDepartVo.getDepartNameUrl(),sysDepart); + } + num++; + } + } + + /** + * 添加部门 + * @param departNameUrl 部门路径 + * @param departName 部门名称 + * @param sysDepart 部门类 + * @param errorMessageList 错误集合 + * @param tenantId 租户id + * @param departMap 部门数组。避免存在部门信息再次查询 key 存放部门路径 value 存放部门对象 + * @param num 判断第几行有错误信息 + */ + private Boolean addDepartByName(String departNameUrl,String departName,SysDepart sysDepart,List errorMessageList,Integer tenantId,Map departMap, int num) { + int lineNumber = num + 1; + if(oConvertUtils.isEmpty(departNameUrl) && oConvertUtils.isEmpty(departName)){ + //部门路径为空 + errorMessageList.add("第 " + lineNumber + " 行:记录部门路径或者部门名称为空禁止导入"); + return false; + } + //获取部门名称路径 + String name = ""; + if(departNameUrl.contains(SymbolConstant.SINGLE_SLASH)){ + //获取分割的部门名称 + name = departNameUrl.substring(departNameUrl.lastIndexOf(SymbolConstant.SINGLE_SLASH)+1); + }else{ + name = departNameUrl; + } + + if(!name.equals(departName)){ + //部门名称已存在 + errorMessageList.add("第 " + lineNumber + " 行:记录部门路径:”"+departNameUrl+"“"+"和部门名称:“"+departName+"“不一致,请检查!"); + return false; + }else{ + String parentId = ""; + //判断是否包含“/” + if(departNameUrl.contains(SymbolConstant.SINGLE_SLASH)){ + //获取最后一个斜杠之前的路径 + String departNames = departNameUrl.substring(0,departNameUrl.lastIndexOf(SymbolConstant.SINGLE_SLASH)); + //判断是否已经包含部门路径 + if(departMap.containsKey(departNames)){ + SysDepart depart = departMap.get(departNames); + if(null != depart){ + parentId = depart.getId(); + } + }else{ + //分割斜杠路径,查看数据库中是否存在此路径 + String[] departNameUrls = departNameUrl.split(SymbolConstant.SINGLE_SLASH); + String departUrlName = departNameUrls[0]; + //判断是否为最后一位 + int count = 0; + SysDepart depart = new SysDepart(); + depart.setId(""); + String parentIdByName = this.getDepartListByName(departUrlName,tenantId,depart,departNameUrls,count,departNameUrls.length-1,name,departMap); + //如果parentId不为空 + if(oConvertUtils.isNotEmpty(parentIdByName)){ + parentId = parentIdByName; + }else{ + //部门名称已存在 + errorMessageList.add("第 " + lineNumber + " 行:记录部门名称“"+departName+"”上级不存在,请检查!"); + return false; + } + } + } + //查询部门名称是否已存在 + SysDepart parentDept = null; + // 代码逻辑说明: 一个租户部门名称可能有多个------------ + List sysDepartList = departMapper.getDepartByName(departName,tenantId,parentId); + if(CollectionUtil.isNotEmpty(sysDepartList)){ + parentDept = sysDepartList.get(0); + } + if(null != parentDept) { + //部门名称已存在 + errorMessageList.add("第 " + lineNumber + " 行:记录部门名称“"+departName+"”已存在,请检查!"); + return false; + }else{ + Page page = new Page<>(1,1); + //需要获取父级id,查看父级是否已经存在 + //获取一级部门的最大orgCode + List records = departMapper.getMaxCodeDepart(page, parentId); + String newOrgCode = ""; + if(CollectionUtil.isNotEmpty(records)){ + newOrgCode = YouBianCodeUtil.getNextYouBianCode(records.get(0).getOrgCode()); + }else{ + //查询父id + if(oConvertUtils.isNotEmpty(parentId)){ + SysDepart departById = departMapper.getDepartById(parentId); + newOrgCode = YouBianCodeUtil.getSubYouBianCode(departById.getOrgCode(), null); + }else{ + newOrgCode = YouBianCodeUtil.getNextYouBianCode(null); + } + } + if(oConvertUtils.isNotEmpty(parentId)){ + this.updateIzLeaf(parentId,CommonConstant.NOT_LEAF); + sysDepart.setParentId(parentId); + } + sysDepart.setOrgCode(newOrgCode); + sysDepart.setDepartName(departName); + return true; + } + + } + } + + /** + * 获取部门名称url(下级) + * @param departName 部门名称 + * @param tenantId 租户id + * @param sysDepart 部门对象 + * @param count 部门路径下标 + * @param departNameUrls 部门路径 + * @param departNum 部门路径的数量 + * @param name 部门路径的数量 + * @param departMap 存放部门的数据 key 存放部门路径 value 存放部门对象 + */ + private String getDepartListByName(String departName, Integer tenantId, SysDepart sysDepart,String[] departNameUrls, int count, int departNum,String name,Map departMap) { + //递归查找下一级 + // 代码逻辑说明: 一个租户部门名称可能有多个------------ + SysDepart parentDept = null; + List departList = departMapper.getDepartByName(departName,tenantId,sysDepart.getId()); + if(CollectionUtil.isNotEmpty(departList)){ + parentDept = departList.get(0); + } + //判断是否包含/ + if(oConvertUtils.isNotEmpty(name)){ + name = name + SymbolConstant.SINGLE_SLASH + departName; + }else{ + name = departName; + } + if(null != parentDept){ + //如果名称路径key不再在,添加一个,避免再次查询 + if(!departMap.containsKey(name)){ + departMap.put(name,parentDept); + } + //查询出来的部门名称和部门路径中的部门名称作比较,如果不存在直接返回空 + if(parentDept.getDepartName().equals(departNameUrls[count])){ + count = count + 1; + //数量和部门数量相等说明已经到最后一位了,直接返回部门id + if(count == departNum){ + return parentDept.getId(); + }else{ + return this.getDepartListByName(departNameUrls[count],tenantId,parentDept,departNameUrls,count,departNum,name,departMap); + } + }else{ + return ""; + } + }else{ + return ""; + } + } + //========================end 零代码下部门与人员导入 ================================================================== + + /** + * 清空部门id + * + * @param parentIdList + */ + private void setParentDepartIzLeaf(List parentIdList) { + if (CollectionUtil.isNotEmpty(parentIdList)) { + for (String parentId : parentIdList) { + //查询父级id没有子级的时候跟新为叶子节点 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysDepart::getParentId, parentId); + Long count = departMapper.selectCount(query); + //当子级都不存在时,设置当前部门为叶子节点 + if (count == 0) { + departMapper.setMainLeaf(parentId, CommonConstant.IS_LEAF); + } + } + } + } + + //========================begin 系统下部门与人员导入 ================================================================== + /** + * 系统部门导出 + * @param tenantId + * @param idList 需要查询部门sql的id集合 + * @return + */ + @Override + public List getExportDepart(Integer tenantId, List idList) { + String parentId = ""; + if(CollectionUtil.isEmpty(idList)){ + //-1代表父级部门为空的数据 + parentId = "-1"; + } + //获取父级部门 + List parentDepart = departMapper.getSysDepartList(parentId, tenantId, idList); + //子部门 + List childrenDepart = new ArrayList<>(); + //把一级部门名称放在里面 + List exportDepartVoList = new ArrayList<>(); + //存放部门一级id避免重复 + List departIdList = new ArrayList<>(); + for (SysDepartExportVo sysDepart : parentDepart) { + if(CollectionUtil.isNotEmpty(departIdList) && departIdList.contains(sysDepart.getId())){ + continue; + } + //step 1.添加第一级部门 + departIdList.add(sysDepart.getId()); + sysDepart.setDepartNameUrl(sysDepart.getDepartName()); + exportDepartVoList.add(sysDepart); + //step 2.添加自己部门路径,用/分离 + //创建路径 + List path = new ArrayList<>(); + path.add(sysDepart.getDepartName()); + //创建子级部门路径 + // 代码逻辑说明: 【JHHB-222】导出,选中最顶级部门,只能导出选中的部门--- + findSysDepartPath(sysDepart, path, tenantId, childrenDepart, departIdList); + path.clear(); + } + exportDepartVoList.addAll(childrenDepart); + childrenDepart.clear(); + departIdList.clear(); + return exportDepartVoList; + } + + /** + * 系统部门导入 + * @param listSysDeparts + * @param errorMessageList + */ + @Override + public void importSysDepart(List listSysDeparts, List errorMessageList) { + int num = 0; + int tenantId = 0; + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + } + //部门路径排序 + Collections.sort(listSysDeparts, new Comparator() { + @Override + public int compare(SysDepartExportVo o1, SysDepartExportVo o2) { + if(oConvertUtils.isNotEmpty(o1.getDepartNameUrl()) && oConvertUtils.isNotEmpty(o2.getDepartNameUrl())){ + int oldLength = o1.getDepartNameUrl().split(SymbolConstant.SINGLE_SLASH).length; + int newLength = o2.getDepartNameUrl().split(SymbolConstant.SINGLE_SLASH).length; + return oldLength - newLength; + }else{ + return 0; + } + } + }); + //存放部门数据的map + Map departMap = new HashMap<>(); + // orgCode编码长度 + int codeLength = YouBianCodeUtil.ZHANWEI_LENGTH; + //循环第二遍导入数据 + for (SysDepartExportVo departExportVo : listSysDeparts) { + SysDepart sysDepart = new SysDepart(); + boolean izExport = false; + try { + izExport = this.addDepartByName(departExportVo.getDepartNameUrl(),departExportVo.getDepartName(),sysDepart,errorMessageList,tenantId,departMap,num); + } catch (Exception e) { + //没有查找到parentDept + } + //没有错误的时候才会导入数据 + if(izExport){ + if(oConvertUtils.isNotEmpty(departExportVo.getOrgCode())){ + SysDepart depart = this.baseMapper.queryCompByOrgCode(departExportVo.getOrgCode()); + if(null != depart){ + if(oConvertUtils.isNotEmpty(sysDepart.getParentId())){ + //更新上级部门为叶子节点 + this.updateIzLeaf(sysDepart.getParentId(),CommonConstant.IS_LEAF); + } + //部门名称已存在 + errorMessageList.add("第 " + num + " 行:记录部门名称“"+departExportVo.getDepartName()+"”部门编码重复,请检查!"); + continue; + } + String departNameUrl = departExportVo.getDepartNameUrl(); + //包含/说明是多级 + if(departNameUrl.contains(SymbolConstant.SINGLE_SLASH)){ + //判断添加部门的规则是否和生成的一致 + if(!sysDepart.getOrgCode().equals(departExportVo.getOrgCode())){ + if(oConvertUtils.isNotEmpty(sysDepart.getParentId())){ + //更新上级部门为叶子节点 + this.updateIzLeaf(sysDepart.getParentId(),CommonConstant.IS_LEAF); + } + //部门名称已存在 + errorMessageList.add("第 " + num + " 行:记录部门名称“"+departExportVo.getDepartName()+"”部门编码规则不匹配,请检查!"); + continue; + } + } + sysDepart.setOrgCode(departExportVo.getOrgCode()); + if(oConvertUtils.isNotEmpty(sysDepart.getParentId())){ + //上级 + sysDepart.setOrgType("2"); + }else{ + //下级 + sysDepart.setOrgType("1"); + } + }else{ + sysDepart.setOrgType(sysDepart.getOrgCode().length()/codeLength+""); + } + sysDepart.setDelFlag(CommonConstant.DEL_FLAG_0.toString()); + sysDepart.setDepartNameEn(departExportVo.getDepartNameEn()); + sysDepart.setDepartOrder(departExportVo.getDepartOrder()); + sysDepart.setOrgCategory(oConvertUtils.getString(departExportVo.getOrgCategory(),"1")); + sysDepart.setMobile(departExportVo.getMobile()); + sysDepart.setFax(departExportVo.getFax()); + sysDepart.setAddress(departExportVo.getAddress()); + sysDepart.setMemo(departExportVo.getMemo()); + sysDepart.setPositionId(departExportVo.getPositionId()); + ImportExcelUtil.importDateSaveOne(sysDepart, ISysDepartService.class, errorMessageList, num, CommonConstant.SQL_INDEX_UNIQ_DEPART_ORG_CODE); + departMap.put(departExportVo.getDepartNameUrl(),sysDepart); + } + num++; + } + } + + /** + * 寻找部门路径 + * + * @param departVo 部门vo + * @param path 部门路径 + * @param tenantId 租户id + * @param childrenDepart 子部门 + * @param departIdList 部门id集合 + */ + private void findSysDepartPath(SysDepartExportVo departVo, List path, Integer tenantId, List childrenDepart, List departIdList) { + //step 1.查询子部门的数据 + //获取租户id和部门父id获取的部门数据 + List departList = departMapper.getSysDepartList(departVo.getId(), tenantId, null); + //部门为空判断 + if (departList == null || departList.size() <= 0) { + //判断最后一个子部门是否已拼接 + if (!departIdList.contains(departVo.getId())) { + departVo.setDepartNameUrl(String.join(SymbolConstant.SINGLE_SLASH, path)); + childrenDepart.add(departVo); + } + return; + } + + for (SysDepartExportVo exportDepartVo : departList) { + //存放子级路径 + List cPath = new ArrayList<>(path); + cPath.add(exportDepartVo.getDepartName()); + //step 2.拼接子部门路径 + if (!departIdList.contains(departVo.getId())) { + departIdList.add(departVo.getId()); + departVo.setDepartNameUrl(String.join(SymbolConstant.SINGLE_SLASH, path)); + childrenDepart.add(departVo); + } + //step 3.递归查询子路径,直到找不到为止 + // 代码逻辑说明: 【JHHB-222】导出,选中最顶级部门,只能导出选中的部门--- + findSysDepartPath(exportDepartVo, cPath, tenantId, childrenDepart, departIdList); + } + } + //========================end 系统下部门与人员导入 ================================================================== + + + //=========================begin 部门岗位改造 ================================================================== + @Override + public List getPositionByDepartId(String parentId, String departId, String positionId) { + //step1 根据职级获取当前岗位的级别 + SysPosition sysPosition = sysPositionMapper.selectById(positionId); + if(null == sysPosition){ + return null; + } + Integer postLevel = sysPosition.getPostLevel(); + //先获取上级部门的信息 + SysDepart sysDepart = baseMapper.getDepartById(parentId); + //step2 如果是总公司 即数据为空的时候,则说明没有上级领导了 + if (null == sysDepart) { + return null; + } + + //可能是老数据 + if(null == postLevel){ + throw new GhbBootBizTipException("当前选择职级的职务等级为空,请前往职务管理进行修改!"); + } + return this.getParentDepartPosition(sysDepart, postLevel, departId); + } + + /* + * 获取上级部门岗位 或者当前部门下级别高的 + * + * @param sysDepart + * @param postLevel + * @param id + * @return + */ + private List getParentDepartPosition(SysDepart sysDepart, Integer postLevel, String id) { + //step1 先获取上级部门下的数据 + //已经存在的code + List existCodeList = new ArrayList<>(); + List departPosition = getDepartPosition(sysDepart, postLevel, id, existCodeList); + //step2 获取上级部门信息,一直获取到子公司或者总公司为止 + //父级id不为空并且当前部门不是子公司或者总公司,则需要寻上顶级公司 + // 代码逻辑说明: 【JHHB-501】三级子公司的董事长岗位,存在向一级公司总经理汇报的职级关系,现在无法配置--- + String parentId = sysDepart.getParentId(); + SysDepart depart = departMapper.getDepartById(parentId); + if(null != depart){ + //获取长度 + int codeNum = YouBianCodeUtil.ZHANWEI_LENGTH; + List codeList = getCodeHierarchy(depart.getOrgCode(), codeNum); + if(null != codeList && codeList.size() > 1){ + //需要将当前和上级部门的排除掉 + existCodeList.add(codeList.get(codeList.size() - 1)); + //从上向下找存在子级的code + List parentDepartPost = this.getParentDepartPost(codeList, existCodeList); + //当前父级部门下存在职级必当前高的需要同事渲染 + if (CollectionUtil.isNotEmpty(departPosition)) { + departPosition.addAll(parentDepartPost); + return buildTree(departPosition); + }else if(CollectionUtil.isNotEmpty(parentDepartPost)){ + return buildTree(parentDepartPost); + } + } + } + // 代码逻辑说明: 【JHHB-501】三级子公司的董事长岗位,存在向一级公司总经理汇报的职级关系,现在无法配置--- + if(CollectionUtil.isNotEmpty(departPosition)){ + return getSuperiorCompany(departPosition); + } + return null; + } + + /** + * 获取上级公司 + * + * @param departPosition + */ + private List getSuperiorCompany(List departPosition) { + String parentId = departPosition.get(0).getParentId(); + SysDepart depart = baseMapper.getDepartById(parentId); + if (null == depart) { + return departPosition; + } + List childrenList = new ArrayList<>(); + SysPositionSelectTreeVo childrenTreeModel = new SysPositionSelectTreeVo(depart); + childrenTreeModel.setChildren(departPosition); + childrenList.add(childrenTreeModel); + if (DepartCategoryEnum.DEPART_CATEGORY_COMPANY.getValue().equals(depart.getOrgCategory()) || DepartCategoryEnum.DEPART_CATEGORY_SUB_COMPANY.getValue().equals(depart.getOrgCategory())) { + return childrenList; + } else { + return this.getSuperiorCompany(childrenList); + } + } + + /** + * 获取父级部门下的一级岗位 + * + * @param codeList + * @param existCodeList 已存在的部门编码 + */ + private List getParentDepartPost(List codeList, List existCodeList) { + List list = new ArrayList<>(); + for (String orgCode : codeList){ + if(existCodeList.contains(orgCode)){ + continue; + } + //当前部门 + SysDepart depart = departMapper.queryDepartByOrgCode(orgCode); + SysPositionSelectTreeVo sysPositionSelectTreeVo = new SysPositionSelectTreeVo(depart); + sysPositionSelectTreeVo.setLeaf(false); + list.add(sysPositionSelectTreeVo); + //查找当前部门下一级部门存在岗位的部门信息 + List departByParentId = departMapper.getDepartByParentId(depart.getId()); + List parentIds = new ArrayList<>(); + for (SysDepart sysDepart : departByParentId) { + list.add(new SysPositionSelectTreeVo(sysDepart)); + // 代码逻辑说明: 上级岗位太慢,sql优化--- + parentIds.add(sysDepart.getId()); + } + // 代码逻辑说明: 上级岗位太慢,sql优化--- + if(CollectionUtil.isNotEmpty(parentIds)){ + //根据父级id获取部门岗位信息 + List departPositionList = departMapper.getDepartPositionByParentIds(parentIds); + if(CollectionUtil.isNotEmpty(departPositionList)){ + List sysDepartTreeModels = sysDepartToTreeModel(departPositionList); + list.addAll(sysDepartTreeModels); + } + } + } + return list; + } + + /** + * 获取部门职务 + * + * @param sysDepart + * @param postLevel + * @param existCodeList + */ + private List getDepartPosition(SysDepart sysDepart, Integer postLevel, String id, List existCodeList) { + //step1 获取部门下的所有部门 + String parentId = sysDepart.getParentId(); + List departList = baseMapper.getDepartByParentId(parentId); + List treeModels = new ArrayList<>(); + for (int i = 0; i < departList.size(); i++) { + SysDepart depart = departList.get(i); + existCodeList.add(depart.getOrgCode()); + //如果是叶子节点说明没有岗位直接跳出循环 + if (depart.getIzLeaf() == 1) { + if (DepartCategoryEnum.DEPART_CATEGORY_POST.getValue().equals(depart.getOrgCategory())) { + SysPositionSelectTreeVo sysDepartTreeModel = new SysPositionSelectTreeVo(depart); + treeModels.add(sysDepartTreeModel); + } + continue; + } + //step2 查找子部门下大于当前职别的数据 + List departParentPosition = baseMapper.getDepartPositionByParentId(depart.getId(), postLevel, id); + if (CollectionUtil.isNotEmpty(departParentPosition)) { + List sysDepartTreeModels = sysDepartToTreeModel(departParentPosition); + SysPositionSelectTreeVo parentDepartTree = new SysPositionSelectTreeVo(depart); + parentDepartTree.setChildren(sysDepartTreeModels); + treeModels.add(parentDepartTree); + } + } + return treeModels; + } + + /** + * 将SysDepart中的属性转到SysDepartTreeModel中 + * + * @return + */ + private List sysDepartToTreeModel(List sysDeparts) { + List records = new ArrayList<>(); + for (int i = 0; i < sysDeparts.size(); i++) { + SysDepart depart = sysDeparts.get(i); + SysPositionSelectTreeVo treeModel = new SysPositionSelectTreeVo(depart); + records.add(treeModel); + } + return records; + } + + /** + * 获取公司或者子公司的id + * + * @param parentDepartId + * @return + */ + private String getCompanyDepartId(String parentDepartId) { + SysDepart sysDepart = baseMapper.getDepartById(parentDepartId); + if (sysDepart != null) { + if (DepartCategoryEnum.DEPART_CATEGORY_COMPANY.getValue().equals(sysDepart.getOrgCategory()) || DepartCategoryEnum.DEPART_CATEGORY_SUB_COMPANY.getValue().equals(sysDepart.getOrgCategory())) { + return sysDepart.getId(); + } + //如果不是公司或者子公司的时候,需要递归查询 + if (oConvertUtils.isNotEmpty(sysDepart.getParentId())) { + return getCompanyDepartId(sysDepart.getParentId()); + } else { + return parentDepartId; + } + } else { + return ""; + } + } + + @Override + public List getRankRelation(String departId) { + //记录当前部门 key为部门id,value为部门名称 + Map departNameMap = new HashMap<>(5); + //step1 根据id查询部门信息 + SysDepartPositionVo sysDepartPosition = baseMapper.getDepartPostByDepartId(departId); + if (null == sysDepartPosition) { + throw new GhbBootBizTipException("当前所选部门数据为空"); + } + List selectTreeVos = new ArrayList<>(); + //step2 查看是否有子级部门,存在递归查询职位 + if (!CommonConstant.IS_LEAF.equals(sysDepartPosition.getIzLeaf())) { + //获取子级职位根据部门编码 + this.getChildrenDepartPositionByOrgCode(selectTreeVos, departNameMap, sysDepartPosition,departId); + return buildTree(selectTreeVos); + } + return new ArrayList<>(); + } + + /** + * 获取所有部门职务 + * @param departId + * @return + */ + @Override + public List getALLRankRelation(String departId) { + //记录当前部门 key为部门id,value为部门名称 + Map departNameMap = new HashMap<>(5); + //step1 根据id查询部门信息 + List departPositionList = baseMapper.getAllDepartPost(departId); + List selectTreeVos = new ArrayList<>(); + departPositionList.forEach(position -> { + //step2 查看是否有子级部门,存在递归查询职位 + if (!CommonConstant.IS_LEAF.equals(position.getIzLeaf())) { + //获取子级职位根据部门编码 + this.getChildrenDepartPositionByOrgCode(selectTreeVos, departNameMap, position,departId); + } + }); + return buildTree(selectTreeVos); + } + + /** + * 获取子级职位根据部门编码 + * + * @param selectTreeVos + * @param departNameMap + * @param sysDepartPosition + */ + private void getChildrenDepartPositionByOrgCode(List selectTreeVos, Map departNameMap, SysDepartPositionVo sysDepartPosition,String departId) { + String orgCode = sysDepartPosition.getOrgCode(); + //step1 根据父级id获取子级部门信息 + List positionList = baseMapper.getDepartPostByOrgCode(orgCode + "%"); + if (CollectionUtil.isNotEmpty(positionList)) { + for (SysDepartPositionVo position : positionList) { + //初始化map + if (departNameMap == null) { + departNameMap = new HashMap<>(5); + } + SysDepart depart = baseMapper.getDepartById(position.getParentId()); + if(null != depart && oConvertUtils.isNotEmpty(departId)) { + position.setDepartName(depart.getDepartName()); + }else{ + position.setDepartName(this.getDepartPathNameByOrgCode(depart.getOrgCode(),null)); + } + if(oConvertUtils.isNotEmpty(position.getDepPostParentId())){ + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysDepart::getId,position.getDepPostParentId()); + query.likeRight(SysDepart::getOrgCode,orgCode); + Long count = baseMapper.selectCount(query); + if(null== count || count == 0){ + position.setDepPostParentId(null); + } + } + departNameMap.put(position.getParentId(), position.getPositionName()); + //查看是否为部门岗位,不是则不需要处理 + SysPositionSelectTreeVo treeVo = new SysPositionSelectTreeVo(position); + if(oConvertUtils.isEmpty(departId)){ + treeVo.setOrgCode(position.getOrgCode()); + } + selectTreeVos.add(treeVo); + } + } + } + + + /** + * 构建树形结构,只返回没有父级的一级节点 + */ + public static List buildTree(List nodes) { + // 1. 去重:根据ID去重,保留第一个 + Map uniqueNodes = nodes.stream() + .filter(Objects::nonNull) + .filter(node -> node.getId() != null && !node.getId().trim().isEmpty()) + .collect(Collectors.toMap( + SysPositionSelectTreeVo::getId, + node -> node, + (existing, replacement) -> existing, + LinkedHashMap::new + )); + // 2. 初始化所有节点的children列表 + uniqueNodes.values().forEach(node -> { + if (node.getChildren() == null) { + node.setChildren(new ArrayList<>()); + } + }); + // 3. 构建树形结构 + List rootNodes = new ArrayList<>(); + for (SysPositionSelectTreeVo node : uniqueNodes.values()) { + String parentId = node.getParentId(); + + if (parentId == null || parentId.trim().isEmpty()) { + // 根节点 + rootNodes.add(node); + } else { + // 子节点,查找父节点 + SysPositionSelectTreeVo parent = uniqueNodes.get(parentId); + if (parent != null) { + parent.getChildren().add(node); + } else { + // 父节点不存在,当作根节点处理 + rootNodes.add(node); + } + } + } + return rootNodes; + } + + //=========================end 部门岗位改造 ================================================================== + + @Override + public String getDepartPathNameByOrgCode(String orgCode, String depId) { + //部门id为空需要查询当前部门下的编码 + if(oConvertUtils.isNotEmpty(depId)){ + SysDepart departById = baseMapper.getDepartById(depId); + if(null != departById){ + orgCode = departById.getOrgCode(); + } + } + if(oConvertUtils.isEmpty(orgCode)){ + return ""; + } + //从redis 获取不为空直接返回 + Object departName = redisUtil.get(CommonConstant.DEPART_NAME_REDIS_KEY_PRE + orgCode); + if(null != departName){ + return String.valueOf(departName); + } + //获取长度 + int codeNum = YouBianCodeUtil.ZHANWEI_LENGTH; + List list = this.getCodeHierarchy(orgCode, codeNum); + //根据部门编码查询公司和子公司的数据 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.in(SysDepart::getOrgCode, list); + query.orderByAsc(SysDepart::getOrgCode); + List sysDepartList = departMapper.selectList(query); + if(!CollectionUtils.isEmpty(sysDepartList)){ + //获取部门名称拼接返回给前台 + // 代码逻辑说明: 【JHHB-631】【部门管理】存在缩写使用缩写来显示--- + List departNameList = sysDepartList.stream().map(item-> oConvertUtils.getString(item.getDepartNameAbbr(),item.getDepartName())).toList(); + String departNames = String.join("/", departNameList); + redisUtil.set(CommonConstant.DEPART_NAME_REDIS_KEY_PRE + orgCode,departNames); + return departNames; + } + return ""; + } + + /** + * 获取编码及其所有上级编码 + * + * @param code 完整编码,如 "A01A01A01" + * @param fixedLength 固定位数,如 3 + * @return 包含所有上级编码的列表,如 ['A01','A01A01','A01A01A01'] + */ + public List getCodeHierarchy(String code, int fixedLength) { + List hierarchy = new ArrayList<>(); + if (code == null || code.isEmpty() || fixedLength <= 0) { + return hierarchy; + } + // 检查编码长度是否能被固定位数整除 + if (code.length() % fixedLength != 0) { + throw new IllegalArgumentException("编码长度必须能被固定位数整除"); + } + // 按固定位数分割并生成所有上级编码 + for (int i = fixedLength; i <= code.length(); i += fixedLength) { + hierarchy.add(code.substring(0, i)); + } + return hierarchy; + } + + /** + * 根据多个部门id删除主岗位和兼职岗位 + * + * @param idList + */ + private void deleteDepartPostByDepIds(List idList) { + //更新用户主岗位位空,使用LambdaUpdateWrapper,避免为空时受全局 updateStrategy 影响导致误更新 + LambdaUpdateWrapper userQuery = new LambdaUpdateWrapper<>(); + userQuery.in(SysUser::getMainDepPostId, idList); + userQuery.set(SysUser::getMainDepPostId, null); + sysUserMapper.update(userQuery); + //删除兼职岗位 + LambdaQueryWrapper postQuery = new LambdaQueryWrapper<>(); + postQuery.in(SysUserDepPost::getDepId, idList); + sysUserDepPostMapper.delete(postQuery); + //redis清除缓存key + redisUtil.removeAll(CommonConstant.DEPART_NAME_REDIS_KEY_PRE); + } + + /** + * 根据部门id获取部门下的岗位id + * + * @param depIds 当前选择的公司、子公司、部门id + * @return + */ + @Override + public String getDepPostIdByDepId(String depIds) { + if (oConvertUtils.isEmpty(depIds)) { + return ""; + } + //step1 先根据部门id获取编码 + List departIdList = departMapper.getDepartByIds(Arrays.asList(depIds.split(SymbolConstant.COMMA))); + if (CollectionUtil.isNotEmpty(departIdList)) { + //step2 根据部门编码查询岗位id + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.select(SysDepart::getId); + departIdList.forEach(item -> { + query.or(lq-> lq.likeRight(SysDepart::getOrgCode, item.getOrgCode())); + }); + query.eq(SysDepart::getOrgCategory, DepartCategoryEnum.DEPART_CATEGORY_POST.getValue()); + List departList = departMapper.selectList(query); + //step3 返回部门id + if (CollectionUtil.isNotEmpty(departList)) { + return departList.stream().map(SysDepart::getId).collect(Collectors.joining(SymbolConstant.COMMA)); + } + } + return ""; + } + + /** + * 变更部门位置 + * + * @param changeDepartVo + * @return orgCode 部门id + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void updateChangeDepart(SysChangeDepartVo changeDepartVo) { + String dragId = changeDepartVo.getDragId(); + // 1. 获取被拖拽的部门 + SysDepart dragDept = baseMapper.getDepartById(dragId); + if (null == dragDept) { + throw new GhbBootBizTipException("被拖拽的部门不存在"); + } + // 2. 获取目标部门 + String dropId = changeDepartVo.getDropId(); + SysDepart targetDept = baseMapper.getDepartById(dropId); + if (null == targetDept) { + throw new GhbBootBizTipException("目标部门不存在"); + } + //3. 验证拖拽操作是否合法 + validateDragOperation(dragDept, targetDept, changeDepartVo.getDropPosition()); + //4. 根据dropPosition调整部门顺序 + Integer dropPosition = changeDepartVo.getDropPosition(); + switch (dropPosition) { + case -1: + // 拖拽到上方 + moveToAbove(dragDept, targetDept); + break; + case 0: + // 拖拽到内部(作为子部门) + moveAsChild(dragDept, targetDept); + break; + case 1: + //拖拽到下方 + moveToBelow(dragDept, targetDept, changeDepartVo.getSort()); + break; + default: + throw new RuntimeException("无效的拖拽位置"); + } + //5. 清空缓存 + redisUtil.removeAll(CommonConstant.DEPART_NAME_REDIS_KEY_PRE); + } + + /** + * 验证拖拽操作是否合法 + * + * @param dragDept 被拖拽的部门 + * @param targetDept 目标部门 + * @param dropPosition 拖拽位置 + */ + private void validateDragOperation(SysDepart dragDept, SysDepart targetDept, Integer dropPosition) { + // 禁止拖拽到自身 + if (dragDept.getId().equals(targetDept.getId())) { + throw new RuntimeException("不能拖拽到自身"); + } + // 禁止拖拽到自身子部门 + if (isDescendant(dragDept, targetDept.getId())) { + throw new RuntimeException("不能拖拽到自身子部门"); + } + //公司岗位判断 + String orgCategory = targetDept.getOrgCategory(); + String oldOrgCategory = dragDept.getOrgCategory(); + //部门为公司 + if(0 != dropPosition && DepartCategoryEnum.DEPART_CATEGORY_COMPANY.getValue().equals(orgCategory)){ + //当前部门不能为子公司、部门和岗位 + if(!DepartCategoryEnum.DEPART_CATEGORY_COMPANY.getValue().equals(oldOrgCategory)){ + throw new GhbBootBizTipException("当前部门类型为【"+DepartCategoryEnum.getNameByValue(oldOrgCategory)+"】,不允许移动到公司"); + } + } + //部门为岗位不允许移入 + if(0 == dropPosition && DepartCategoryEnum.DEPART_CATEGORY_POST.getValue().equals(orgCategory)) { + throw new GhbBootBizTipException("岗位不允许存在子级"); + } + //公司不能做为子级 + if(oConvertUtils.isNotEmpty(targetDept.getParentId()) && DepartCategoryEnum.DEPART_CATEGORY_COMPANY.getValue().equals(oldOrgCategory)){ + throw new GhbBootBizTipException("公司不允许作为子级"); + } + } + + /** + * 判断目标部门是否是被拖拽部门的子部门 + */ + private boolean isDescendant(SysDepart dragDept, String targetId) { + List children = departMapper.getDepartByParentId(dragDept.getId()); + for (SysDepart child : children) { + if (child.getId().equals(targetId)) { + return true; + } + if (isDescendant(child, targetId)) { + return true; + } + } + return false; + } + + /** + * 拖拽到上方:将部门移动到目标部门上方(只有最上级 即公司才会走这个逻辑) + * @param dragDept 被拖拽的部门 + * @param targetDept 目标部门 + */ + private void moveToAbove(SysDepart dragDept, SysDepart targetDept) { + // 获取目标部门同级的所有部门 + List siblings = departMapper.getDepartNoParent(); + // 计算新的排序值 + Integer newDepartOrder = targetDept.getDepartOrder(); + // 更新被拖拽部门的排序值 + dragDept.setDepartOrder(newDepartOrder); + // 更新被拖拽部门的排序值 + dragDept.setDepartOrder(0); + if(CollectionUtil.isNotEmpty(siblings)){ + // 计算新的排序值 + this.computingSort(siblings,0,dragDept.getId()); + // 保存所有更新的部门 + this.updateBatchById(siblings); + } + departMapper.updateById(dragDept); + } + + /** + * 拖拽到下方:将部门移动到目标部门下方 + * + * @param dragDept 被拖拽的部门 + * @param targetDept 目标部门 + * @param sort 排序 + */ + private void moveToBelow(SysDepart dragDept, SysDepart targetDept, Integer sort) { + String parentId = targetDept.getParentId(); + List siblings = null; + if(oConvertUtils.isNotEmpty(parentId)){ + // 获取目标部门同级的所有部门 + siblings = departMapper.getDepartByParentId(parentId); + }else{ + siblings = departMapper.getDepartNoParent(); + } + String oldParentId = dragDept.getParentId(); + //判断父级部门id是否相同,不同则更新为目标部门的父部门id + if(oConvertUtils.isNotEmpty(dragDept.getParentId()) && + oConvertUtils.isNotEmpty(parentId) && + !dragDept.getParentId().equals(parentId)){ + String oldOrgCode = dragDept.getOrgCode(); + //设置父级id和部门code + this.setDepartParentAndOrgCode(dragDept, parentId); + //修改子级的部门编码 + this.updateChildOrgCode(dragDept.getOrgCode(), oldOrgCode); + } + // 更新被拖拽部门的排序值 + dragDept.setDepartOrder(sort); + if(CollectionUtil.isNotEmpty(siblings)){ + // 计算新的排序值 + this.computingSort(siblings,sort,dragDept.getId()); + // 保存所有更新的部门 + this.updateBatchById(siblings); + } + departMapper.updateById(dragDept); + if(oConvertUtils.isNotEmpty(oldParentId)){ + long count = departMapper.countByParentId(oldParentId); + if(count == 0){ + this.updateIzLeaf(oldParentId,CommonConstant.IS_LEAF); + } + } + } + + /** + * 拖拽到内部:将部门移动到目标部门内部(作为子部门) + */ + private void moveAsChild(SysDepart dragDept, SysDepart targetDept) { + // 更新父部门ID + String parentId = targetDept.getId(); + String oldParentId = dragDept.getParentId(); + // 获取目标部门同级的所有部门 + List siblings = departMapper.getDepartByParentId(parentId); + //判断父级部门id是否相同,不同则更新为目标部门的父部门id + if(oConvertUtils.isNotEmpty(dragDept.getParentId()) && + oConvertUtils.isNotEmpty(parentId) && + !dragDept.getParentId().equals(parentId)){ + String oldOrgCode = dragDept.getOrgCode(); + //设置父级id和部门code + this.setDepartParentAndOrgCode(dragDept, parentId); + //修改子级的部门编码 + this.updateChildOrgCode(dragDept.getOrgCode(), oldOrgCode); + } + //内部排序为0 + Integer sort = 0; + // 设置新的排序值 + dragDept.setDepartOrder(sort); + if(CollectionUtil.isNotEmpty(siblings)){ + // 计算新的排序值 + this.computingSort(siblings,sort,dragDept.getId()); + // 保存所有更新的部门 + this.updateBatchById(siblings); + } + departMapper.updateById(dragDept); + this.updateIzLeaf(parentId, CommonConstant.NOT_LEAF); + if(oConvertUtils.isNotEmpty(oldParentId)){ + long count = departMapper.countByParentId(oldParentId); + if(count == 0){ + this.updateIzLeaf(oldParentId,CommonConstant.IS_LEAF); + } + } + } + + /** + * 计算排序值 + * + * @param siblings + * @param sort + * @param id + */ + private void computingSort(List siblings, Integer sort, String id) { + for (int i = 0; i < siblings.size(); i++) { + SysDepart depart = siblings.get(i); + if(id.equals(depart.getId())){ + continue; + } + //如果当前循环的sort大等于传入的sort值 则需要+1 + if(i >= sort){ + depart.setDepartOrder(sort + 1); + sort++; + } else { + depart.setDepartOrder(i); + } + } + } + + /** + * 设置被拖拽部门的父级id和部门编码 + * + * @param dragDept 被拖拽的部门 + * @param parentId 目标部门的父级id + */ + private void setDepartParentAndOrgCode(SysDepart dragDept, String parentId) { + // 更新父部门ID(与目标部门相同) + dragDept.setParentId(parentId); + Page page = new Page<>(1, 1); + //需要获取父级id,查看父级是否已经存在 + //获取一级部门的最大orgCode + List records = departMapper.getMaxCodeDepart(page, parentId); + String newOrgCode = ""; + if (CollectionUtil.isNotEmpty(records)) { + newOrgCode = YouBianCodeUtil.getNextYouBianCode(records.get(0).getOrgCode()); + } else { + //查询父id + if (oConvertUtils.isNotEmpty(parentId)) { + SysDepart departById = departMapper.getDepartById(parentId); + newOrgCode = YouBianCodeUtil.getSubYouBianCode(departById.getOrgCode(), null); + } else { + newOrgCode = YouBianCodeUtil.getNextYouBianCode(null); + } + } + dragDept.setOrgCode(newOrgCode); + } + + /** + * 修改子级的部门编码 + * + * @param newOrgCode 当前父级新的部门编码 + * @param oldOrgCode 当前父级旧的部门编码 + */ + private void updateChildOrgCode(String newOrgCode, String oldOrgCode) { + //查询当前部门下的所有子级部门 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.likeRight(SysDepart::getOrgCode, oldOrgCode); + query.orderByAsc(SysDepart::getDepartOrder); + query.orderByDesc(SysDepart::getCreateTime); + query.select(SysDepart::getId, SysDepart::getOrgCode); + List childDeparts = departMapper.selectList(query); + if (CollectionUtil.isNotEmpty(childDeparts)) { + for (SysDepart depart : childDeparts) { + String orgCode = depart.getOrgCode(); + if (orgCode.startsWith(oldOrgCode)) { + orgCode = newOrgCode + orgCode.substring(oldOrgCode.length()); + } + depart.setOrgCode(orgCode); + } + } + this.updateBatchById(childDeparts); + } + + /** + * 获取部门负责人 + * + * @param departId + * @param page + * @return + */ + @Override + public IPage getDepartmentHead(String departId, Page page) { + List departmentHead = departMapper.getDepartmentHead(page, departId); + if(CollectionUtil.isNotEmpty(departmentHead)){ + departmentHead.forEach(item->{ + //兼职岗位 + List depPostList = sysUserDepPostMapper.getDepPostByUserId(item.getId()); + if(CollectionUtil.isNotEmpty(depPostList)){ + item.setOtherDepPostId(StringUtils.join(depPostList.toArray(), SymbolConstant.COMMA)); + } + }); + } + return page.setRecords(departmentHead); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDictItemServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDictItemServiceImpl.java new file mode 100644 index 0000000..c6931c6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDictItemServiceImpl.java @@ -0,0 +1,30 @@ +package com.ghb.base.modules.system.service.impl; + +import com.ghb.base.modules.system.entity.SysDictItem; +import com.ghb.base.modules.system.mapper.SysDictItemMapper; +import com.ghb.base.modules.system.service.ISysDictItemService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + *

+ * 服务实现类 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +@Service +public class SysDictItemServiceImpl extends ServiceImpl implements ISysDictItemService { + + @Autowired + private SysDictItemMapper sysDictItemMapper; + + @Override + public List selectItemsByMainId(String mainId) { + return sysDictItemMapper.selectItemsByMainId(mainId); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDictServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDictServiceImpl.java new file mode 100644 index 0000000..706781d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysDictServiceImpl.java @@ -0,0 +1,932 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.util.RandomUtil; +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.jeecg.common.config.TenantContext; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.DataBaseConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.system.util.ResourceUtil; +import com.ghb.base.common.system.vo.DictModel; +import com.ghb.base.common.system.vo.DictModelMany; +import com.ghb.base.common.system.vo.DictQuery; +import com.ghb.base.common.util.CommonUtils; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.SqlInjectionUtil; +import com.ghb.base.common.util.dynamic.db.DbTypeUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysDict; +import com.ghb.base.modules.system.entity.SysDictItem; +import com.ghb.base.modules.system.mapper.SysDictItemMapper; +import com.ghb.base.modules.system.mapper.SysDictMapper; +import com.ghb.base.modules.system.model.DuplicateCheckVo; +import com.ghb.base.modules.system.model.TreeSelectModel; +import com.ghb.base.modules.system.security.DictQueryBlackListHandler; +import com.ghb.base.modules.system.service.ISysDictService; +import com.ghb.base.modules.system.vo.lowapp.SysDictVo; +import org.mybatis.spring.MyBatisSystemException; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 字典表 服务实现类 + *

+ * + * @Author zhangweijian + * @since 2018-12-28 + */ +@Service +@Slf4j +public class SysDictServiceImpl extends ServiceImpl implements ISysDictService { + + @Autowired + private SysDictMapper sysDictMapper; + @Autowired + private SysDictItemMapper sysDictItemMapper; + @Autowired + private DictQueryBlackListHandler dictQueryBlackListHandler; + + @Lazy + @Autowired + private ISysBaseAPI sysBaseAPI; + @Lazy + @Autowired + private RedisUtil redisUtil; + + @Override + public boolean duplicateCheckData(DuplicateCheckVo duplicateCheckVo) { + Long count = null; + + // 1.针对采用 ${}写法的表名和字段进行转义和check + String table = SqlInjectionUtil.getSqlInjectTableName(duplicateCheckVo.getTableName()); + String fieldName = SqlInjectionUtil.getSqlInjectField(duplicateCheckVo.getFieldName()); + duplicateCheckVo.setTableName(table); + duplicateCheckVo.setFieldName(fieldName); + + // 2.SQL注入check(只限制非法串改数据库) + //关联表字典(举例:sys_user,realname,id) + SqlInjectionUtil.filterContentMulti(table, fieldName); + + String checkSql = table + SymbolConstant.COMMA + fieldName + SymbolConstant.COMMA; + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, fieldName); + // 3.表字典黑名单check + dictQueryBlackListHandler.isPass(checkSql); + + // 4.执行SQL 查询是否存在值 + try{ + // 代码逻辑说明: [TV360X-49]postgres日期、年月日时分秒唯一校验报错------------ + if(DbTypeUtils.dbTypeIsPostgre(CommonUtils.getDatabaseTypeEnum())){ + duplicateCheckVo.setFieldName("CAST("+duplicateCheckVo.getFieldName()+" as text)"); + } + if (StringUtils.isNotBlank(duplicateCheckVo.getDataId())) { + // [1].编辑页面校验 + count = sysDictMapper.duplicateCheckCountSql(duplicateCheckVo); + } else { + // [2].添加页面校验 + count = sysDictMapper.duplicateCheckCountSqlNoDataId(duplicateCheckVo); + } + }catch(MyBatisSystemException e){ + log.error(e.getMessage(), e); + String errorCause = "查询异常,请检查唯一校验的配置!"; + throw new GhbBootException(errorCause); + } + + // 4.返回结果 + if (count == null || count == 0) { + // 该值可用 + return true; + } else { + // 该值不可用 + log.info("该值不可用,系统中已存在!"); + return false; + } + } + + + /** + * 通过查询指定code 获取字典 + * @param code + * @return + */ + @Override + @Cacheable(value = CacheConstant.SYS_DICT_CACHE,key = "#code", unless = "#result == null ") + public List queryDictItemsByCode(String code) { + log.debug("无缓存dictCache的时候调用这里!"); + return sysDictMapper.queryDictItemsByCode(code); + } + + @Override + @Cacheable(value = CacheConstant.SYS_ENABLE_DICT_CACHE,key = "#code", unless = "#result == null ") + public List queryEnableDictItemsByCode(String code) { + log.debug("无缓存dictCache的时候调用这里!"); + return sysDictMapper.queryEnableDictItemsByCode(code); + } + + @Override + public Map> queryDictItemsByCodeList(List dictCodeList) { + List list = sysDictMapper.queryDictItemsByCodeList(dictCodeList); + Map> dictMap = new HashMap(5); + for (DictModelMany dict : list) { + List dictItemList = dictMap.computeIfAbsent(dict.getDictCode(), i -> new ArrayList<>()); + + // 代码逻辑说明: QQYUN-5183【简流】多字段拼接-多选框、下拉框 等需要翻译的字段 + //dict.setDictCode(null); + + dictItemList.add(new DictModel(dict.getValue(), dict.getText(), dict.getColor())); + } + return dictMap; + } + + @Override + public Map> queryAllDictItems() { + log.debug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + long start = System.currentTimeMillis(); + Map> sysAllDictItems = new HashMap(5); + List tenantIds = null; + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + tenantIds = new ArrayList<>(); + tenantIds.add(0); + if (TenantContext.getTenant() != null) { + tenantIds.add(oConvertUtils.getInt(TenantContext.getTenant())); + } + } + //------------------------------------------------------------------------------------------------ + List sysDictItemList = sysDictMapper.queryAllDictItems(tenantIds); + // 使用groupingBy根据dictCode分组 + sysAllDictItems = sysDictItemList.stream() + .collect(Collectors.groupingBy(DictModelMany::getDictCode, + Collectors.mapping(d -> new DictModel(d.getValue(), d.getText(), d.getColor()), Collectors.toList()))); + log.debug(" >>> 1 获取系统字典项耗时(SQL):" + (System.currentTimeMillis() - start) + "毫秒"); + + Map> enumRes = ResourceUtil.getEnumDictData(); + sysAllDictItems.putAll(enumRes); + log.debug(" >>> 2 获取系统字典项耗时(Enum):" + (System.currentTimeMillis() - start) + "毫秒"); + + log.debug(" >>> end 获取系统字典库总耗时:" + (System.currentTimeMillis() - start) + "毫秒"); + log.debug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + //log.debug("-------登录加载系统字典-----" + sysAllDictItems.toString()); + return sysAllDictItems; + } + + /** + * 通过查询指定code 获取字典值text + * @param code + * @param key + * @return + */ + + @Override + @Cacheable(value = CacheConstant.SYS_DICT_CACHE,key = "#code+':'+#key", unless = "#result == null ") + public String queryDictTextByKey(String code, String key) { + log.debug("无缓存dictText的时候调用这里!"); + return sysDictMapper.queryDictTextByKey(code, key); + } + + @Override + public Map> queryManyDictByKeys(List dictCodeList, List keys) { + List list = sysDictMapper.queryManyDictByKeys(dictCodeList, keys); + Map> dictMap = new HashMap(5); + for (DictModelMany dict : list) { + List dictItemList = dictMap.computeIfAbsent(dict.getDictCode(), i -> new ArrayList<>()); + dictItemList.add(new DictModel(dict.getValue(), dict.getText())); + } + // 代码逻辑说明: 系统字典数据应该包括自定义的java类-枚举 + Map> enumRes = ResourceUtil.queryManyDictByKeys(dictCodeList, keys); + dictMap.putAll(enumRes); + return dictMap; + } + + /** + * 通过查询指定table的 text code 获取字典 + * dictTableCache采用redis缓存有效期10分钟 + * @param tableFilterSql + * @param text + * @param code + * @return + */ + @Override + @Deprecated + public List queryTableDictItemsByCode(String tableFilterSql, String text, String code) { + log.debug("无缓存dictTableList的时候调用这里!"); + String str = tableFilterSql+","+text+","+code; + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(tableFilterSql, text, code); + // 1.表字典黑名单check + if(!dictQueryBlackListHandler.isPass(str)){ + log.error(dictQueryBlackListHandler.getError()); + return null; + } + + // 2.分割SQL获取表名和条件 + String table = null; + String filterSql = null; + if(tableFilterSql.toLowerCase().indexOf(DataBaseConstant.SQL_WHERE)>0){ + String[] arr = tableFilterSql.split(" (?i)where "); + table = arr[0]; + filterSql = oConvertUtils.getString(arr[1], null); + }else{ + table = tableFilterSql; + } + + // 3.SQL注入check + SqlInjectionUtil.filterContentMulti(table, text, code); + SqlInjectionUtil.specialFilterContentForDictSql(filterSql); + + // 4.针对采用 ${}写法的表名和字段进行转义和check + table = SqlInjectionUtil.getSqlInjectTableName(table); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + + //return sysDictMapper.queryTableDictItemsByCode(tableFilterSql,text,code); + table = table.toLowerCase(); + return sysDictMapper.queryTableDictWithFilter(table,text,code,filterSql); + } + + @Override + public List queryTableDictItemsByCodeAndFilter(String table, String text, String code, String filterSql) { + log.debug("无缓存dictTableList的时候调用这里!"); + + // 1.SQL注入校验(只限制非法串改数据库) + SqlInjectionUtil.specialFilterContentForDictSql(table); + SqlInjectionUtil.filterContentMulti(text, code); + SqlInjectionUtil.specialFilterContentForDictSql(filterSql); + + String str = table+","+text+","+code; + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); + // 2.表字典黑名单 Check + if(!dictQueryBlackListHandler.isPass(str)){ + log.error(dictQueryBlackListHandler.getError()); + return null; + } + + // 3.针对采用 ${}写法的表名和字段进行转义和check + table = SqlInjectionUtil.getSqlInjectTableName(table); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + table = table.toLowerCase(); + return sysDictMapper.queryTableDictWithFilter(table,text,code,filterSql); + } + + /** + * 通过查询指定table的 text code 获取字典值text + * dictTableCache采用redis缓存有效期10分钟 + * @param table + * @param text + * @param code + * @param key + * @return + */ + @Override + @Cacheable(value = CacheConstant.SYS_DICT_TABLE_CACHE, unless = "#result == null ") + public String queryTableDictTextByKey(String table,String text,String code, String key) { + log.debug("无缓存dictTable的时候调用这里!"); + + String str = table+","+text+","+code; + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); + // 1.表字典黑名单check + if(!dictQueryBlackListHandler.isPass(str)){ + log.error(dictQueryBlackListHandler.getError()); + return null; + } + // 2.sql注入check + SqlInjectionUtil.filterContentMulti(table, text, code, key); + + // 3.针对采用 ${}写法的表名和字段进行转义和check + table = SqlInjectionUtil.getSqlInjectTableName(table); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + + List dictModeList = sysDictMapper.queryTableDictByKeysAndFilterSql(table, text, code, null, Arrays.asList(key)); + if(CollectionUtils.isEmpty(dictModeList)){ + return null; + }else{ + return dictModeList.get(0).getText(); + } + + //此方法删除(20230902) + //return sysDictMapper.queryTableDictTextByKey(table,text,code,key); + } + + @Override + public List queryTableDictTextByKeys(String table, String text, String code, List codeValues, String dataSource) { + String str = table+","+text+","+code; + //update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------ + // 是否自定义数据源 + boolean isCustomDataSource = oConvertUtils.isNotEmpty(dataSource); + // 如果是自定义数据源就不检查表字典白名单 + if (!isCustomDataSource) { + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); + // 1.表字典黑名单check + if (!dictQueryBlackListHandler.isPass(str)) { + log.error(dictQueryBlackListHandler.getError()); + return null; + } + } + + // 2.分割SQL获取表名和条件 + String filterSql = null; + if(table.toLowerCase().indexOf(DataBaseConstant.SQL_WHERE)>0){ + String[] arr = table.split(" (?i)where "); + table = arr[0]; + filterSql = arr[1]; + } + + // 3.SQL注入check + SqlInjectionUtil.filterContentMulti(table, text, code); + SqlInjectionUtil.specialFilterContentForDictSql(filterSql); + + // 4.针对采用 ${}写法的表名和字段进行转义和check + table = SqlInjectionUtil.getSqlInjectTableName(table); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + + // 切换为字典表的数据源 + if (isCustomDataSource) { + DynamicDataSourceContextHolder.push(dataSource); + } + //update-begin---author:jarysun ---date:20251020 for:[issues/#9002]解决表字典查询出现异常之后,数据源不能恢复问题------------ + List restData = null; + + try { + restData = sysDictMapper.queryTableDictByKeysAndFilterSql(table, text, code, filterSql, codeValues); + } finally { + // 清理自定义的数据源 + if (isCustomDataSource) { + DynamicDataSourceContextHolder.clear(); + } + } + //update-end---author:jarysun ---date:20251020 for:[issues/#9002]解决表字典查询出现异常之后,数据源不能恢复问题------------ + + return restData; + } + + @Override + public List queryTableDictByKeys(String table, String text, String code, String keys) { + String str = table+","+text+","+code; + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); + // 1.表字典黑名单check + if(!dictQueryBlackListHandler.isPass(str)){ + log.error(dictQueryBlackListHandler.getError()); + return null; + } + + return this.queryTableDictByKeys(table, text, code, keys, true); + } + + /** + * 通过查询指定table的 text code 获取字典,包含text和value + * dictTableCache采用redis缓存有效期10分钟 + * @param table + * @param text + * @param code + * @param codeValuesStr (逗号分隔) + * @param delNotExist 是否移除不存在的项,默认为true,设为false如果某个key不存在数据库中,则直接返回key本身 + * @return + */ + @Override + public List queryTableDictByKeys(String table, String text, String code, String codeValuesStr, boolean delNotExist) { + if(oConvertUtils.isEmpty(codeValuesStr)){ + return null; + } + + //1.分割sql获取表名 和 条件sql + String filterSql = null; + if(table.toLowerCase().indexOf("where")!=-1){ + String[] arr = table.split(" (?i)where "); + table = arr[0]; + filterSql = arr[1]; + } + + // 2.SQL注入check + SqlInjectionUtil.filterContentMulti(table, text, code); + SqlInjectionUtil.specialFilterContentForDictSql(filterSql); + + String str = table+","+text+","+code; + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); + // 3.表字典黑名单check + if(!dictQueryBlackListHandler.isPass(str)){ + log.error(dictQueryBlackListHandler.getError()); + return null; + } + + // 4.针对采用 ${}写法的表名和字段进行转义和check + table = SqlInjectionUtil.getSqlInjectTableName(table); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + + //字典条件值 + String[] codeValues = codeValuesStr.split(","); + // 5.查询字典数据 + List dicts = sysDictMapper.queryTableDictByKeysAndFilterSql(SqlInjectionUtil.getSqlInjectTableName(table), + SqlInjectionUtil.getSqlInjectField(text), SqlInjectionUtil.getSqlInjectField(code), filterSql, Arrays.asList(codeValues)); + + List texts = new ArrayList<>(dicts.size()); + // 6.查询出来的顺序可能是乱的,需要排个序 + for (String conditionalVal : codeValues) { + List res = dicts.stream().filter(i -> conditionalVal.equals(i.getValue())).collect(Collectors.toList()); + if (res.size() > 0) { + texts.add(res.get(0).getText()); + } else if (!delNotExist) { + texts.add(conditionalVal); + } + } + return texts; + } + + /** + * 根据字典类型id删除关联表中其对应的数据 + */ + @Override + public boolean deleteByDictId(SysDict sysDict) { + sysDict.setDelFlag(CommonConstant.DEL_FLAG_1); + return this.updateById(sysDict); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer saveMain(SysDict sysDict, List sysDictItemList) { + int insert=0; + try{ + insert = sysDictMapper.insert(sysDict); + if (sysDictItemList != null) { + for (SysDictItem entity : sysDictItemList) { + // 代码逻辑说明: [JTC-1168]如果字典项值为空,则字典项忽略导入------------ + if(oConvertUtils.isEmpty(entity.getItemValue())){ + return -1; + } + entity.setDictId(sysDict.getId()); + entity.setStatus(1); + sysDictItemMapper.insert(entity); + } + } + }catch(Exception e){ + return insert; + } + return insert; + } + + @Override + public List queryAllDepartBackDictModel() { + return baseMapper.queryAllDepartBackDictModel(); + } + + @Override + public List queryAllUserBackDictModel() { + return baseMapper.queryAllUserBackDictModel(); + } + +// @Override +// public List queryTableDictItems(String table, String text, String code, String keyword) { +// return baseMapper.queryTableDictItems(table, text, code, "%"+keyword+"%"); +// } + + @Override + public List queryLittleTableDictItems(String tableSql, String text, String code, String condition, String keyword, int pageNo, int pageSize) { + int current = oConvertUtils.getInt(pageNo, 1); + Page page = new Page(current, pageSize); + page.setSearchCount(false); + + //为了防止sql(Ghb提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击) + // 1. 针对采用 ${}写法的表名和字段进行转义和check + String table = SqlInjectionUtil.getSqlInjectTableName(CommonUtils.getTableNameByTableSql(tableSql)); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + + // 2. 查询条件SQL (获取条件sql方法含sql注入校验) + String filterSql = getFilterSql(tableSql, text, code, condition, keyword); + + // 3. 返回表字典数据 + IPage pageList = baseMapper.queryPageTableDictWithFilter(page, table, text, code, filterSql); + return pageList.getRecords(); + } + + /** + * 获取条件语句 (下拉搜索组件 支持传入排序信息 查询排序) + * + * @param text + * @param code + * @param condition + * @param keyword + * @return + */ + private String getFilterSql(String tableSql, String text, String code, String condition, String keyword){ + String filterSql = ""; + String keywordSql = null; + String sqlWhere = "where "; + String sqlAnd = " and "; + + //【JTC-631】判断如果 table 携带了 where 条件,那么就使用 and 查询,防止报错 + boolean tableHasWhere = tableSql.toLowerCase().contains(sqlWhere); + if (tableHasWhere) { + sqlWhere = CommonUtils.getFilterSqlByTableSql(tableSql); + } + + // 下拉搜索组件 支持传入排序信息 查询排序 + String orderField = "", orderType = ""; + if (oConvertUtils.isNotEmpty(keyword)) { + // 关键字里面如果写入了 排序信息 xxxxx[orderby:create_time,desc] + String orderKey = "[orderby"; + if (keyword.indexOf(orderKey) >= 0 && keyword.endsWith("]")) { + String orderInfo = keyword.substring(keyword.indexOf(orderKey) + orderKey.length() + 1, keyword.length() - 1); + keyword = keyword.substring(0, keyword.indexOf(orderKey)); + String[] orderInfoArray = orderInfo.split(SymbolConstant.COMMA); + // 【issue/9570】排序字段和排序方向使用白名单校验,防止 boolean-blind SQL 注入(CASE WHEN/LIKE/database() 等绕过黑名单) + orderField = SqlInjectionUtil.getSqlInjectField(orderInfoArray[0]); + orderType = SqlInjectionUtil.getSqlInjectOrderType(orderInfoArray[1]); + } + + if (oConvertUtils.isNotEmpty(keyword)) { + // 【安全】对keyword进行SQL注入检测和单引号转义,防止通过keyword参数进行SQL注入 + keyword = keyword.replace("'", "''"); + + // 判断是否是多选 + if (keyword.contains(SymbolConstant.COMMA)) { + // 代码逻辑说明: JTC-529【表单设计器】 编辑页面报错,in参数采用双引号导致 ---- + String inKeywords = "'" + String.join("','", keyword.split(",")) + "'"; + keywordSql = "(" + text + " in (" + inKeywords + ") or " + code + " in (" + inKeywords + "))"; + } else { + keywordSql = "("+text + " like '%"+keyword+"%' or "+ code + " like '%"+keyword+"%')"; + } + } + } + + //下拉搜索组件 支持传入排序信息 查询排序 + // 代码逻辑说明: [QQYUN-8514]Online表单中 下拉搜索框 搜索时报sql错误,生成的SQL多了一个 “and" ------------ + if (oConvertUtils.isNotEmpty(condition) && oConvertUtils.isNotEmpty(keywordSql)) { + filterSql += sqlWhere + (tableHasWhere ? sqlAnd : " ") + condition + sqlAnd + keywordSql; + } else if (oConvertUtils.isNotEmpty(condition)) { + filterSql += sqlWhere + (tableHasWhere ? sqlAnd : " ") + condition; + } else if (oConvertUtils.isNotEmpty(keywordSql)) { + filterSql += sqlWhere + (tableHasWhere ? sqlAnd : " ") + keywordSql; + } else if (tableHasWhere) { + filterSql += sqlWhere; + } + // 增加排序逻辑 + if (oConvertUtils.isNotEmpty(orderField)) { + filterSql += " order by " + orderField + " " + orderType; + } + + // 处理返回条件 + // 1.1 返回条件SQL(去掉开头的 where ) + final String wherePrefix = "(?i)where "; // (?i) 表示不区分大小写 + String filterSqlString = filterSql.trim().replaceAll(wherePrefix, ""); + // 1.2 条件SQL进行漏洞 check + SqlInjectionUtil.specialFilterContentForDictSql(filterSqlString); + // 1.3 判断如何返回条件是 order by开头则前面拼上 1=1 + if (oConvertUtils.isNotEmpty(filterSqlString) && filterSqlString.trim().toUpperCase().startsWith("ORDER")) { + filterSqlString = " 1=1 " + filterSqlString; + } + return filterSqlString; + } + + + @Override + public List queryAllTableDictItems(String table, String text, String code, String condition, String keyword) { + // 1.获取条件sql + String filterSql = getFilterSql(table, text, code, condition, keyword); + + // 为了防止sql(Ghb提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击) + // 2.针对采用 ${}写法的表名和字段进行转义和check + table = SqlInjectionUtil.getSqlInjectTableName(table); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + + List ls = baseMapper.queryTableDictWithFilter(table, text, code, filterSql); + return ls; + } + + @Override + public List queryTreeList(Map query, String table, String text, String code, String pidField, String pid, String hasChildField, int converIsLeafVal) { + //为了防止sql(Ghb提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击) + // 1.针对采用 ${}写法的表名和字段进行转义和check + //update-begin---author:chenrui ---date:20251015 for:[QQYUN-13741]【客户问题 南自】online表单自定义树 表后边加条件时 不生效------------ + // 分割SQL获取表名和条件 + String filterSql = null; + if(table.toLowerCase().indexOf(DataBaseConstant.SQL_WHERE)>0){ + String[] arr = table.split(" (?i)where "); + table = arr[0]; + filterSql = oConvertUtils.getString(arr[1], null); + } + table = SqlInjectionUtil.getSqlInjectTableName(table); + text = SqlInjectionUtil.getSqlInjectField(text); + code = SqlInjectionUtil.getSqlInjectField(code); + pidField = SqlInjectionUtil.getSqlInjectField(pidField); + hasChildField = SqlInjectionUtil.getSqlInjectField(hasChildField); + + if(oConvertUtils.isEmpty(text) || oConvertUtils.isEmpty(code)){ + log.warn("text={},code={}", text, code); + log.warn("加载树字典参数有误,text和code不允许为空!"); + return null; + } + + // 2.检测最终SQL是否存在SQL注入风险 + String dictCode = table + "," + text + "," + code; + SqlInjectionUtil.filterContentMulti(dictCode); + SqlInjectionUtil.specialFilterContentForDictSql(filterSql); + + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); + // 3.表字典SQL表名黑名单 Check + if(!dictQueryBlackListHandler.isPass(dictCode)){ + log.error("Sql异常:{}", dictQueryBlackListHandler.getError()); + return null; + } + // 4.检测查询条件是否存在SQL注入 + Map queryParams = queryParams = new HashMap<>(4); + if (query != null) { + for (Map.Entry searchItem : query.entrySet()) { + String fieldName = searchItem.getKey(); + // update-begin---author:sjlei---date:20260413 for:【#9524】修复 SQL _tableFilterSql 注入漏洞 + // _tableFilterSql 是服务端内部专用 key,对应 Mapper 中的 ${value} 裸拼接, + // 禁止从外部 condition 参数传入,防止 SQL 注入(#9520) + if ("_tableFilterSql".equals(fieldName)) { + continue; + } + // update-end-----author:sjlei---date:20260413 for:【#9520】修复 SQL _tableFilterSql 注入漏洞 + queryParams.put(SqlInjectionUtil.getSqlInjectField(fieldName), searchItem.getValue()); + } + } + // 代码逻辑说明: [QQYUN-13741]【客户问题 南自】online表单自定义树 表后边加条件时 不生效------------ + if(oConvertUtils.isNotEmpty(filterSql)){ + queryParams.put("_tableFilterSql", filterSql); + } + + return baseMapper.queryTreeList(queryParams, table, text, code, pidField, pid, hasChildField, converIsLeafVal); + } + + @Override + public void deleteOneDictPhysically(String id) { + this.baseMapper.deleteOneById(id); + this.sysDictItemMapper.delete(new LambdaQueryWrapper().eq(SysDictItem::getDictId,id)); + } + + @Override + public void updateDictDelFlag(int delFlag, String id) { + baseMapper.updateDictDelFlag(delFlag,id); + } + + @Override + public List queryDeleteList(String tenantId) { + // 代码逻辑说明: 【QQYUN-8340】回收站查找软删除记录时,没有判断是否启用多租户,造成可以查找并回收其他租户的数据 #5907--- + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + if(oConvertUtils.isEmpty(tenantId)){ + return new ArrayList<>(); + } + return baseMapper.queryDeleteListBtTenantId(oConvertUtils.getInt(tenantId)); + } + return baseMapper.queryDeleteList(); + } + + @Override + public List queryDictTablePageList(DictQuery query, int pageSize, int pageNo) { + Page page = new Page(pageNo,pageSize,false); + + //为了防止sql(Ghb提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击) + // 1. 针对采用 ${}写法的表名和字段进行转义和check + String table = SqlInjectionUtil.getSqlInjectTableName(query.getTable()); + String text = SqlInjectionUtil.getSqlInjectTableName(query.getText()); + String code = SqlInjectionUtil.getSqlInjectTableName(query.getCode()); + query.setCode(table); + query.setTable(text); + query.setText(code); + + String dictCode = table+","+text+","+code; + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code); + // 2.表字典黑名单check + if(!dictQueryBlackListHandler.isPass(dictCode)){ + log.error(dictQueryBlackListHandler.getError()); + return null; + } + + // 3.SQL注入check + SqlInjectionUtil.filterContentMulti(dictCode); + + Page pageList = baseMapper.queryDictTablePageList(page, query); + return pageList.getRecords(); + } + + @Override + public List getDictItems(String dictCode) { + List ls; + if (dictCode.contains(SymbolConstant.COMMA)) { + //关联表字典(举例:sys_user,realname,id) + String[] params = dictCode.split(","); + if (params.length < 3) { + // 字典Code格式不正确 + return null; + } + + if (params.length == 4) { + ls = this.queryTableDictItemsByCodeAndFilter(params[0], params[1], params[2], params[3]); + } else if (params.length == 3) { + ls = this.queryTableDictItemsByCode(params[0], params[1], params[2]); + } else { + // 字典Code格式不正确 + return null; + } + } else { + //字典表 + ls = this.queryDictItemsByCode(dictCode); + } + // 代码逻辑说明: 字典获取可以获取枚举类的数据 + if (ls == null || ls.size() == 0) { + Map> map = ResourceUtil.getEnumDictData(); + if (map.containsKey(dictCode)) { + return map.get(dictCode); + } + } + return ls; + } + + @Override + public List loadDict(String dictCode, String keyword, Integer pageNo, Integer pageSize) { + // 【QQYUN-6533】表字典白名单check + sysBaseAPI.dictTableWhiteListCheckByDict(dictCode); + // 1.表字典黑名单check + if(!dictQueryBlackListHandler.isPass(dictCode)){ + log.error(dictQueryBlackListHandler.getError()); + return null; + } + + // 2.字典SQL注入风险check + SqlInjectionUtil.specialFilterContentForDictSql(dictCode); + + if (dictCode.contains(SymbolConstant.COMMA)) { + // 代码逻辑说明: 下拉搜索不支持表名后加查询条件 + String[] params = dictCode.split(","); + String condition = null; + if (params.length != 3 && params.length != 4) { + // 字典Code格式不正确 + return null; + } else if (params.length == 4) { + condition = params[3]; + // 代码逻辑说明: online表单下拉搜索框表字典配置#{sys_org_code}报错 #3500 + if(condition.indexOf(SymbolConstant.SYS_VAR_PREFIX)>=0){ + condition = QueryGenerator.getSqlRuleValue(condition); + } + } + + // 字典Code格式不正确 [表名为空] + if(oConvertUtils.isEmpty(params[0])){ + return null; + } + List ls; + if (pageSize != null) { + ls = this.queryLittleTableDictItems(params[0], params[1], params[2], condition, keyword, pageNo,pageSize); + } else { + ls = this.queryAllTableDictItems(params[0], params[1], params[2], condition, keyword); + } + return ls; + } else { + // 字典Code格式不正确 + return null; + } + } + + @Override + public List getDictListByLowAppId(String lowAppId) { + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + List list = baseMapper.getDictListByLowAppId(lowAppId,tenantId); + //查询字典下面的字典项 + List dictVoList = new ArrayList<>(); + for (SysDict dict:list) { + SysDictVo dictVo = new SysDictVo(); + BeanUtils.copyProperties(dict,dictVo); + List sysDictItems = sysDictItemMapper.selectItemsByMainId(dict.getId()); + dictVo.setDictItemsList(sysDictItems); + dictVoList.add(dictVo); + } + return dictVoList; + } + + @Override + public String addDictByLowAppId(SysDictVo sysDictVo) { + String[] dictResult = this.addDict(sysDictVo.getDictName(),sysDictVo.getLowAppId(),sysDictVo.getTenantId()); + String id = dictResult[0]; + String code = dictResult[1]; + this.addDictItem(id,sysDictVo.getDictItemsList()); + return code; + } + + @Override + public void editDictByLowAppId(SysDictVo sysDictVo) { + String id = sysDictVo.getId(); + SysDict dict = baseMapper.selectById(id); + if(null == dict){ + throw new GhbBootException("字典数据不存在"); + } + //判断应用id和数据库中的是否一致,不一致不让修改 + if(!dict.getLowAppId().equals(sysDictVo.getLowAppId())){ + throw new GhbBootException("字典数据不存在"); + } + SysDict sysDict = new SysDict(); + sysDict.setDictName(sysDictVo.getDictName()); + sysDict.setId(id); + baseMapper.updateById(sysDict); + this.updateDictItem(id,sysDictVo.getDictItemsList()); + // 删除字典缓存 + redisUtil.removeAll(CacheConstant.SYS_DICT_CACHE + "::" + dict.getDictCode()); + } + + /** + * 还原逻辑删除 + * @param ids + */ + @Override + public boolean revertLogicDeleted(List ids) { + return baseMapper.revertLogicDeleted(ids) > 0; + } + + /** + * 彻底删除 + * @param ids + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeLogicDeleted(List ids) { + // 1. 删除字典 + int line = this.baseMapper.removeLogicDeleted(ids); + // 2. 删除字典选项配置 + line += this.sysDictItemMapper.delete(new LambdaQueryWrapper().in(SysDictItem::getDictId, ids)); + return line > 0; + } + + /** + * 添加字典 + * @param dictName + */ + private String[] addDict(String dictName,String lowAppId, Integer tenantId) { + SysDict dict = new SysDict(); + dict.setDictName(dictName); + dict.setDictCode(RandomUtil.randomString(10)); + dict.setDelFlag(Integer.valueOf(CommonConstant.STATUS_0)); + dict.setLowAppId(lowAppId); + dict.setTenantId(tenantId); + baseMapper.insert(dict); + String[] dictResult = new String[]{dict.getId(), dict.getDictCode()}; + return dictResult; + } + + /** + * 添加字典子项 + * @param id + * @param dictItemList + */ + private void addDictItem(String id,List dictItemList) { + if(null!=dictItemList && dictItemList.size()>0){ + for (SysDictItem dictItem:dictItemList) { + SysDictItem sysDictItem = new SysDictItem(); + BeanUtils.copyProperties(dictItem,sysDictItem); + sysDictItem.setDictId(id); + sysDictItem.setId(""); + sysDictItem.setStatus(Integer.valueOf(CommonConstant.STATUS_1)); + sysDictItemMapper.insert(sysDictItem); + } + } + } + + /** + * 更新字典子项 + * @param id + * @param dictItemList + */ + private void updateDictItem(String id,List dictItemList){ + //先删除在新增 因为排序可能不一致 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysDictItem::getDictId,id); + sysDictItemMapper.delete(query); + //新增子项 + this.addDictItem(id,dictItemList); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysFillRuleServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysFillRuleServiceImpl.java new file mode 100644 index 0000000..2771e38 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysFillRuleServiceImpl.java @@ -0,0 +1,18 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.modules.system.entity.SysFillRule; +import com.ghb.base.modules.system.mapper.SysFillRuleMapper; +import com.ghb.base.modules.system.service.ISysFillRuleService; +import org.springframework.stereotype.Service; + +/** + * @Description: 填值规则 + * @Author: Ghb-boot + * @Date: 2019-11-07 + * @Version: V1.0 + */ +@Service("sysFillRuleServiceImpl") +public class SysFillRuleServiceImpl extends ServiceImpl implements ISysFillRuleService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysFormFileServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysFormFileServiceImpl.java new file mode 100644 index 0000000..73911e3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysFormFileServiceImpl.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.modules.system.entity.SysFormFile; +import com.ghb.base.modules.system.mapper.SysFormFileMapper; +import com.ghb.base.modules.system.service.ISysFormFileService; +import org.springframework.stereotype.Service; + + +/** + * @Description: 表单评论文件 + * @Author: Ghb-boot + * @Date: 2022-07-21 + * @Version: V1.0 + */ +@Service +public class SysFormFileServiceImpl extends ServiceImpl implements ISysFormFileService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysGatewayRouteServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysGatewayRouteServiceImpl.java new file mode 100644 index 0000000..79fcf35 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysGatewayRouteServiceImpl.java @@ -0,0 +1,187 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.base.BaseMap; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import org.jeecg.common.constant.GlobalConstants; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysGatewayRoute; +import com.ghb.base.modules.system.mapper.SysGatewayRouteMapper; +import com.ghb.base.modules.system.service.ISysGatewayRouteService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * @Description: gateway路由管理 + * @Author: Ghb-boot + * @Date: 2020-05-26 + * @Version: V1.0 + */ +@Service +@Slf4j +public class SysGatewayRouteServiceImpl extends ServiceImpl implements ISysGatewayRouteService { + + @Autowired + private RedisTemplate redisTemplate; + + private static final String STRING_STATUS = "status"; + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MMdd"); + @Override + public void addRoute2Redis(String key) { + List ls = this.list(new LambdaQueryWrapper()); + redisTemplate.opsForValue().set(key, JSON.toJSONString(ls)); + } + + @Override + public void deleteById(String id) { + //1.将状态修改成禁用 + SysGatewayRoute route = new SysGatewayRoute(); + route.setId(id); + route.setStatus(0); + this.baseMapper.updateById(route); + this.removeById(id); + //2.刷新路由 + this.resreshRouter(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAll(JSONObject json) { + log.info("--gateway 路由配置修改--"); + try { + json = json.getJSONObject("router"); + String id = json.getString("id"); + // 代码逻辑说明: oracle路由网关新增小bug /issues/I4EV2J + SysGatewayRoute route; + if(oConvertUtils.isEmpty(id)){ + route = new SysGatewayRoute(); + }else{ + route = getById(id); + } + if (ObjectUtil.isEmpty(route)) { + route = new SysGatewayRoute(); + } + route.setRouterId(json.getString("routerId")); + route.setName(json.getString("name")); + route.setPredicates(json.getString("predicates")); + //初始化删除状态 + route.setDelFlag(CommonConstant.DEL_FLAG_0); + String filters = json.getString("filters"); + if (ObjectUtil.isEmpty(filters)) { + filters = "[]"; + } + route.setFilters(filters); + route.setUri(json.getString("uri")); + if (json.get(STRING_STATUS) == null) { + route.setStatus(1); + } else { + route.setStatus(json.getInteger(STRING_STATUS)); + } + this.saveOrUpdate(route); + resreshRouter(null); + } catch (Exception e) { + log.error("路由配置解析失败", e); + resreshRouter(null); + e.printStackTrace(); + } + } + + /** + * 更新redis路由缓存 + */ + private void resreshRouter(String delRouterId) { + //更新redis路由缓存 + addRoute2Redis(CacheConstant.GATEWAY_ROUTES); + BaseMap params = new BaseMap(); + params.put(GlobalConstants.HANDLER_NAME, GlobalConstants.LODER_ROUDER_HANDLER); + params.put("delRouterId", delRouterId); + //刷新网关 + redisTemplate.convertAndSend(GlobalConstants.REDIS_TOPIC_NAME, params); + } + + @Override + public void clearRedis() { + redisTemplate.opsForValue().set(CacheConstant.GATEWAY_ROUTES, null); + } + + /** + * 还原逻辑删除 + * @param ids + */ + @Override + public void revertLogicDeleted(List ids) { + this.baseMapper.revertLogicDeleted(ids); + resreshRouter(null); + } + + /** + * 彻底删除 + * @param ids + */ + @Override + public void deleteLogicDeleted(List ids) { + this.baseMapper.deleteLogicDeleted(ids); + resreshRouter(ids.get(0)); + } + + /** + * 路由复制 + * @param id + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public SysGatewayRoute copyRoute(String id) { + log.info("--gateway 路由复制--"); + SysGatewayRoute targetRoute = new SysGatewayRoute(); + try { + SysGatewayRoute sourceRoute = this.baseMapper.selectById(id); + //1.复制路由 + BeanUtils.copyProperties(sourceRoute,targetRoute); + //1.1 获取当前日期 + String formattedDate = dateFormat.format(new Date()); + String copyRouteName = sourceRoute.getName() + "_copy_"; + //1.2 判断数据库是否存在 + Long count = this.baseMapper.selectCount(new LambdaQueryWrapper().eq(SysGatewayRoute::getName, copyRouteName + formattedDate)); + //1.3 新的路由名称 + copyRouteName += count > 0?RandomUtil.randomNumbers(4):formattedDate; + + targetRoute.setId(null); + targetRoute.setName(copyRouteName); + targetRoute.setCreateTime(new Date()); + targetRoute.setStatus(0); + targetRoute.setDelFlag(CommonConstant.DEL_FLAG_0); + this.baseMapper.insert(targetRoute); + //2.刷新路由 + resreshRouter(null); + } catch (Exception e) { + log.error("路由配置解析失败", e); + resreshRouter(null); + e.printStackTrace(); + } + return targetRoute; + } + + /** + * 查询删除列表 + * @return + */ + @Override + public List getDeletelist() { + return baseMapper.queryDeleteList(); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysLogServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysLogServiceImpl.java new file mode 100644 index 0000000..051344b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysLogServiceImpl.java @@ -0,0 +1,63 @@ +package com.ghb.base.modules.system.service.impl; + +import java.sql.SQLException; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import jakarta.annotation.Resource; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.util.CommonUtils; +import com.ghb.base.modules.system.entity.SysLog; +import com.ghb.base.modules.system.mapper.SysLogMapper; +import com.ghb.base.modules.system.service.ISysLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + *

+ * 系统日志表 服务实现类 + *

+ * + * @Author zhangweijian + * @since 2018-12-26 + */ +@Service +public class SysLogServiceImpl extends ServiceImpl implements ISysLogService { + + @Resource + private SysLogMapper sysLogMapper; + + /** + * @功能:清空所有日志记录 + */ + @Override + public void removeAll() { + sysLogMapper.removeAll(); + } + + @Override + public Long findTotalVisitCount() { + return sysLogMapper.findTotalVisitCount(); + } + + @Override + public Long findTodayVisitCount(Date dayStart, Date dayEnd) { + return sysLogMapper.findTodayVisitCount(dayStart,dayEnd); + } + + @Override + public Long findTodayIp(Date dayStart, Date dayEnd) { + return sysLogMapper.findTodayIp(dayStart,dayEnd); + } + + @Override + public List> findVisitCount(Date dayStart, Date dayEnd) { + DbType dbType = CommonUtils.getDatabaseTypeEnum(); + return sysLogMapper.findVisitCount(dayStart, dayEnd,dbType.getDb()); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPackPermissionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPackPermissionServiceImpl.java new file mode 100644 index 0000000..817f490 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPackPermissionServiceImpl.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.system.service.impl; + +import com.ghb.base.modules.system.entity.SysPackPermission; +import com.ghb.base.modules.system.mapper.SysPackPermissionMapper; +import com.ghb.base.modules.system.service.ISysPackPermissionService; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: 产品包菜单关系表 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +@Service +public class SysPackPermissionServiceImpl extends ServiceImpl implements ISysPackPermissionService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPermissionDataRuleImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPermissionDataRuleImpl.java new file mode 100644 index 0000000..26d3ae4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPermissionDataRuleImpl.java @@ -0,0 +1,116 @@ +package com.ghb.base.modules.system.service.impl; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.annotation.Resource; + +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.system.query.QueryGenerator; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysPermission; +import com.ghb.base.modules.system.entity.SysPermissionDataRule; +import com.ghb.base.modules.system.mapper.SysPermissionDataRuleMapper; +import com.ghb.base.modules.system.mapper.SysPermissionMapper; +import com.ghb.base.modules.system.service.ISysPermissionDataRuleService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + *

+ * 菜单权限规则 服务实现类 + *

+ * + * @Author huangzhilin + * @since 2019-04-01 + */ +@Service +public class SysPermissionDataRuleImpl extends ServiceImpl + implements ISysPermissionDataRuleService { + + @Resource + private SysPermissionMapper sysPermissionMapper; + + /** + * 根据菜单id查询其对应的权限数据 + */ + @Override + public List getPermRuleListByPermId(String permissionId) { + LambdaQueryWrapper query = new LambdaQueryWrapper(); + query.eq(SysPermissionDataRule::getPermissionId, permissionId); + query.orderByDesc(SysPermissionDataRule::getCreateTime); + List permRuleList = this.list(query); + return permRuleList; + } + + /** + * 根据前端传递的权限名称和权限值参数来查询权限数据 + */ + @Override + public List queryPermissionRule(SysPermissionDataRule permRule) { + QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(permRule, null); + return this.list(queryWrapper); + } + + @Override + public List queryPermissionDataRules(String username,String permissionId) { + List idsList = this.baseMapper.queryDataRuleIds(username, permissionId); + // 代码逻辑说明: 数据权限失效问题处理-------------------- + if(idsList==null || idsList.size()==0) { + return null; + } + Set set = new HashSet(); + for (String ids : idsList) { + if(oConvertUtils.isEmpty(ids)) { + continue; + } + String[] arr = ids.split(","); + for (String id : arr) { + if(oConvertUtils.isNotEmpty(id) && !set.contains(id)) { + set.add(id); + } + } + } + if(set.size()==0) { + return null; + } + return this.baseMapper.selectList(new QueryWrapper().in("id", set).eq("status",CommonConstant.STATUS_1)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void savePermissionDataRule(SysPermissionDataRule sysPermissionDataRule) { + this.save(sysPermissionDataRule); + SysPermission permission = sysPermissionMapper.selectById(sysPermissionDataRule.getPermissionId()); + boolean flag = permission != null && (permission.getRuleFlag() == null || permission.getRuleFlag().equals(CommonConstant.RULE_FLAG_0)); + if(flag) { + permission.setRuleFlag(CommonConstant.RULE_FLAG_1); + sysPermissionMapper.updateById(permission); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deletePermissionDataRule(String dataRuleId) { + SysPermissionDataRule dataRule = this.baseMapper.selectById(dataRuleId); + if(dataRule!=null) { + this.removeById(dataRuleId); + Long count = this.baseMapper.selectCount(new LambdaQueryWrapper().eq(SysPermissionDataRule::getPermissionId, dataRule.getPermissionId())); + //注:同一个事务中删除后再查询是会认为数据已被删除的 若事务回滚上述删除无效 + if(count==null || count==0) { + SysPermission permission = sysPermissionMapper.selectById(dataRule.getPermissionId()); + if(permission!=null && permission.getRuleFlag().equals(CommonConstant.RULE_FLAG_1)) { + permission.setRuleFlag(CommonConstant.RULE_FLAG_0); + sysPermissionMapper.updateById(permission); + } + } + } + + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPermissionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPermissionServiceImpl.java new file mode 100644 index 0000000..79bd808 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPermissionServiceImpl.java @@ -0,0 +1,326 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.jeecg.common.config.TenantContext; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysPermission; +import com.ghb.base.modules.system.entity.SysPermissionDataRule; +import com.ghb.base.modules.system.entity.SysRoleIndex; +import com.ghb.base.modules.system.mapper.SysDepartPermissionMapper; +import com.ghb.base.modules.system.mapper.SysDepartRolePermissionMapper; +import com.ghb.base.modules.system.mapper.SysPermissionMapper; +import com.ghb.base.modules.system.mapper.SysRolePermissionMapper; +import com.ghb.base.modules.system.model.TreeModel; +import com.ghb.base.modules.system.service.ISysPermissionDataRuleService; +import com.ghb.base.modules.system.service.ISysPermissionService; +import com.ghb.base.modules.system.service.ISysRoleIndexService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.util.*; + +/** + *

+ * 菜单权限表 服务实现类 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +@Service +public class SysPermissionServiceImpl extends ServiceImpl implements ISysPermissionService { + + @Resource + private SysPermissionMapper sysPermissionMapper; + + @Resource + private ISysPermissionDataRuleService permissionDataRuleService; + + @Resource + private SysRolePermissionMapper sysRolePermissionMapper; + + @Resource + private SysDepartPermissionMapper sysDepartPermissionMapper; + + @Resource + private SysDepartRolePermissionMapper sysDepartRolePermissionMapper; + + @Autowired + private ISysRoleIndexService roleIndexService; + + @Override + public void switchVue3Menu() { + sysPermissionMapper.backupVue2Menu(); + sysPermissionMapper.changeVue3Menu(); + } + + @Override + public List queryListByParentId(String parentId) { + return sysPermissionMapper.queryListByParentId(parentId); + } + + /** + * 真实删除 + */ + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value = CacheConstant.SYS_DATA_PERMISSIONS_CACHE,allEntries=true) + public void deletePermission(String id) throws GhbBootException { + SysPermission sysPermission = this.getById(id); + if(sysPermission==null) { + throw new GhbBootException("未找到菜单信息"); + } + String pid = sysPermission.getParentId(); + if(oConvertUtils.isNotEmpty(pid)) { + Long count = this.count(new QueryWrapper().lambda().eq(SysPermission::getParentId, pid)); + if(count==1) { + //若父节点无其他子节点,则该父节点是叶子节点 + this.sysPermissionMapper.setMenuLeaf(pid, 1); + } + } + sysPermissionMapper.deleteById(id); + // 该节点可能是子节点但也可能是其它节点的父节点,所以需要级联删除 + this.removeChildrenBy(sysPermission.getId()); + //关联删除 + Map map = new HashMap(5); + map.put("permission_id",id); + //删除数据规则 + this.deletePermRuleByPermId(id); + //删除角色授权表 + sysRolePermissionMapper.deleteByMap(map); + //删除部门权限表 + sysDepartPermissionMapper.deleteByMap(map); + //删除部门角色授权 + sysDepartRolePermissionMapper.deleteByMap(map); + } + + /** + * 根据父id删除其关联的子节点数据 + * + * @return + */ + public void removeChildrenBy(String parentId) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + // 封装查询条件parentId为主键, + query.eq(SysPermission::getParentId, parentId); + // 查出该主键下的所有子级 + List permissionList = this.list(query); + if (permissionList != null && permissionList.size() > 0) { + // id + String id = ""; + // 查出的子级数量 + Long num = Long.valueOf(0); + // 如果查出的集合不为空, 则先删除所有 + this.remove(query); + // 再遍历刚才查出的集合, 根据每个对象,查找其是否仍有子级 + for (int i = 0, len = permissionList.size(); i < len; i++) { + id = permissionList.get(i).getId(); + Map map = new HashMap(5); + map.put("permission_id",id); + //删除数据规则 + this.deletePermRuleByPermId(id); + //删除角色授权表 + sysRolePermissionMapper.deleteByMap(map); + //删除部门权限表 + sysDepartPermissionMapper.deleteByMap(map); + //删除部门角色授权 + sysDepartRolePermissionMapper.deleteByMap(map); + num = this.count(new LambdaQueryWrapper().eq(SysPermission::getParentId, id)); + // 如果有, 则递归 + if (num > 0) { + this.removeChildrenBy(id); + } + } + } + } + + /** + * 逻辑删除 + */ + @Override + @CacheEvict(value = CacheConstant.SYS_DATA_PERMISSIONS_CACHE,allEntries=true) + //@CacheEvict(value = CacheConstant.SYS_DATA_PERMISSIONS_CACHE,allEntries=true,condition="#sysPermission.menuType==2") + public void deletePermissionLogical(String id) throws GhbBootException { + SysPermission sysPermission = this.getById(id); + if(sysPermission==null) { + throw new GhbBootException("未找到菜单信息"); + } + String pid = sysPermission.getParentId(); + Long count = this.count(new QueryWrapper().lambda().eq(SysPermission::getParentId, pid)); + if(count==1) { + //若父节点无其他子节点,则该父节点是叶子节点 + this.sysPermissionMapper.setMenuLeaf(pid, 1); + } + sysPermission.setDelFlag(1); + this.updateById(sysPermission); + } + + @Override + @CacheEvict(value = CacheConstant.SYS_DATA_PERMISSIONS_CACHE,allEntries=true) + public void addPermission(SysPermission sysPermission) throws GhbBootException { + //---------------------------------------------------------------------- + //判断是否是一级菜单,是的话清空父菜单 + if(CommonConstant.MENU_TYPE_0.equals(sysPermission.getMenuType())) { + sysPermission.setParentId(null); + } + //---------------------------------------------------------------------- + String pid = sysPermission.getParentId(); + if(oConvertUtils.isNotEmpty(pid)) { + //设置父节点不为叶子节点 + this.sysPermissionMapper.setMenuLeaf(pid, 0); + } + sysPermission.setCreateTime(new Date()); + sysPermission.setDelFlag(0); + sysPermission.setLeaf(true); + this.save(sysPermission); + } + + @Override + @CacheEvict(value = CacheConstant.SYS_DATA_PERMISSIONS_CACHE,allEntries=true) + public void editPermission(SysPermission sysPermission) throws GhbBootException { + SysPermission p = this.getById(sysPermission.getId()); + //TODO 该节点判断是否还有子节点 + if(p==null) { + throw new GhbBootException("未找到菜单信息"); + }else { + sysPermission.setUpdateTime(new Date()); + //---------------------------------------------------------------------- + //Step1.判断是否是一级菜单,是的话清空父菜单ID + if(CommonConstant.MENU_TYPE_0.equals(sysPermission.getMenuType())) { + sysPermission.setParentId(""); + } + //Step2.判断菜单下级是否有菜单,无则设置为叶子节点 + Long count = this.count(new QueryWrapper().lambda().eq(SysPermission::getParentId, sysPermission.getId())); + if(count==0) { + sysPermission.setLeaf(true); + } + //---------------------------------------------------------------------- + this.updateById(sysPermission); + + //如果当前菜单的父菜单变了,则需要修改新父菜单和老父菜单的,叶子节点状态 + String pid = sysPermission.getParentId(); + boolean flag = (oConvertUtils.isNotEmpty(pid) && !pid.equals(p.getParentId())) || oConvertUtils.isEmpty(pid)&&oConvertUtils.isNotEmpty(p.getParentId()); + if (flag) { + //a.设置新的父菜单不为叶子节点 + this.sysPermissionMapper.setMenuLeaf(pid, 0); + //b.判断老的菜单下是否还有其他子菜单,没有的话则设置为叶子节点 + Long cc = this.count(new QueryWrapper().lambda().eq(SysPermission::getParentId, p.getParentId())); + if(cc==0) { + if(oConvertUtils.isNotEmpty(p.getParentId())) { + this.sysPermissionMapper.setMenuLeaf(p.getParentId(), 1); + } + } + + } + + // 同步更改默认菜单 + SysRoleIndex defIndexCfg = this.roleIndexService.queryDefaultIndex(); + boolean isDefIndex = defIndexCfg.getUrl().equals(p.getUrl()); + if (isDefIndex) { + this.roleIndexService.updateDefaultIndex(sysPermission.getUrl(), sysPermission.getComponent(), sysPermission.isRoute()); + } + + } + + } + + @Override + public List queryByUser(String userId) { + //update-begin---author:scott ---date:2026-04-16 for:【pull/9445】开启多租户模式时,获取用户权限时加入tenant_id判断----------- + List permissionList; + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), -1); + if (tenantId != -1) { + permissionList = this.sysPermissionMapper.queryByUserWithTenantId(userId, tenantId); + } else { + permissionList = this.sysPermissionMapper.queryByUser(userId); + } + } else { + permissionList = this.sysPermissionMapper.queryByUser(userId); + } + //update-end---author:scott ---date:2026-04-16 for:【pull/9445】开启多租户模式时,获取用户权限时加入tenant_id判断----------- + //================= begin 开启租户的时候 如果没有test角色,默认加入test角色================ + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + if (permissionList == null) { + permissionList = new ArrayList<>(); + } + List testRoleList = sysPermissionMapper.queryPermissionByTestRoleId(); + // 代码逻辑说明: [QQYUN-5168]【vue3】为什么出现两个菜单 菜单根据id去重 + for (SysPermission permission: testRoleList) { + boolean hasPerm = permissionList.stream().anyMatch(a->a.getId().equals(permission.getId())); + if(!hasPerm){ + permissionList.add(permission); + } + } + } + //================= end 开启租户的时候 如果没有test角色,默认加入test角色================ + return permissionList; + } + + /** + * 根据permissionId删除其关联的SysPermissionDataRule表中的数据 + */ + @Override + public void deletePermRuleByPermId(String id) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysPermissionDataRule::getPermissionId, id); + Long countValue = this.permissionDataRuleService.count(query); + if(countValue > 0) { + this.permissionDataRuleService.remove(query); + } + } + + /** + * 获取模糊匹配规则的数据权限URL + */ + @Override + @Cacheable(value = CacheConstant.SYS_DATA_PERMISSIONS_CACHE) + public List queryPermissionUrlWithStar() { + return this.baseMapper.queryPermissionUrlWithStar(); + } + + @Override + public boolean hasPermission(String username, SysPermission sysPermission) { + int count = baseMapper.queryCountByUsername(username,sysPermission); + if(count>0){ + return true; + }else{ + return false; + } + } + + @Override + public boolean hasPermission(String username, String url) { + SysPermission sysPermission = new SysPermission(); + sysPermission.setUrl(url); + int count = baseMapper.queryCountByUsername(username,sysPermission); + if(count>0){ + return true; + }else{ + return false; + } + } + + @Override + public List queryDepartPermissionList(String departId) { + return sysPermissionMapper.queryDepartPermissionList(departId); + } + + @Override + public boolean checkPermDuplication(String id, String url,Boolean alwaysShow) { + QueryWrapper qw=new QueryWrapper(); + qw.lambda().eq(true,SysPermission::getUrl,url).ne(oConvertUtils.isNotEmpty(id),SysPermission::getId,id).eq(true,SysPermission::isAlwaysShow,alwaysShow); + return count(qw)==0; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPositionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPositionServiceImpl.java new file mode 100644 index 0000000..4c803ee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysPositionServiceImpl.java @@ -0,0 +1,55 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.modules.system.entity.SysPosition; +import com.ghb.base.modules.system.mapper.SysPositionMapper; +import com.ghb.base.modules.system.service.ISysPositionService; +import com.ghb.base.modules.system.vo.SysPositionVO; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @Description: 职务表 + * @Author: Ghb-boot + * @Date: 2019-09-19 + * @Version: V1.0 + */ +@Service +public class SysPositionServiceImpl extends ServiceImpl implements ISysPositionService { + + @Override + public SysPosition getByCode(String code) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysPosition::getCode, code); + return super.getOne(queryWrapper); + } + + @Override + public List getPositionList(String userId) { + return this.baseMapper.getPositionList(userId); + } + + @Override + public String getPositionName(List postList) { + List positionNameList = this.baseMapper.getPositionName(postList); + if (null != positionNameList && positionNameList.size()>0) { + return positionNameList.stream().map(SysPosition::getName).collect(Collectors.joining(SymbolConstant.COMMA)); + } + return ""; + } + + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + @Override + public List getPositionListByUserIds(List userIds) { + if (userIds == null || userIds.isEmpty()) { + return Collections.emptyList(); + } + return this.baseMapper.getPositionListByUserIds(userIds); + } + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRoleIndexServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRoleIndexServiceImpl.java new file mode 100644 index 0000000..5a03f50 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRoleIndexServiceImpl.java @@ -0,0 +1,184 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.common.constant.CommonConstant; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.constant.DefIndexConst; +import com.ghb.base.modules.system.entity.SysRoleIndex; +import com.ghb.base.modules.system.mapper.SysRoleIndexMapper; +import com.ghb.base.modules.system.service.ISysRoleIndexService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; +/** + * @Description: 角色首页配置 + * @Author: Ghb-boot + * @Date: 2022-03-25 + * @Version: V1.0 + */ +@Service("sysRoleIndexServiceImpl") +public class SysRoleIndexServiceImpl extends ServiceImpl implements ISysRoleIndexService { + + @Autowired + private RedisUtil redisUtil; + + @Override + @Cacheable(cacheNames = DefIndexConst.CACHE_KEY + "#3600", key = "'" + DefIndexConst.DEF_INDEX_ALL + "'") + public SysRoleIndex queryDefaultIndex() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysRoleIndex::getRoleCode, DefIndexConst.DEF_INDEX_ALL); + queryWrapper.eq(SysRoleIndex::getStatus, CommonConstant.STATUS_1); + SysRoleIndex entity = super.getOne(queryWrapper); + // 保证不为空 + if (entity == null) { + entity = this.initDefaultIndex(); + } + return entity; + } + + @Override + public boolean updateDefaultIndex(String url, String component, boolean isRoute) { + // 1. 先查询出配置信息 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysRoleIndex::getRoleCode, DefIndexConst.DEF_INDEX_ALL); + SysRoleIndex entity = super.getOne(queryWrapper); + boolean success = false; + // 2. 如果不存在则新增 + if (entity == null) { + entity = this.newDefIndexConfig(url, component, isRoute); + success = super.save(entity); + } else { + // 3. 如果存在则更新 + entity.setUrl(url); + entity.setComponent(component); + entity.setRoute(isRoute); + entity.setRelationType(CommonConstant.HOME_RELATION_DEFAULT); + success = super.updateById(entity); + } + // 4. 清理缓存 + if (success) { + this.cleanDefaultIndexCache(); + } + return success; + } + + @Override + public SysRoleIndex initDefaultIndex() { + return this.newDefIndexConfig(DefIndexConst.DEF_INDEX_URL, DefIndexConst.DEF_INDEX_COMPONENT, true); + } + + /** + * 创建默认首页配置 + * + * @param indexComponent + * @return + */ + private SysRoleIndex newDefIndexConfig(String indexUrl, String indexComponent, boolean isRoute) { + SysRoleIndex entity = new SysRoleIndex(); + entity.setRoleCode(DefIndexConst.DEF_INDEX_ALL); + entity.setUrl(indexUrl); + entity.setComponent(indexComponent); + entity.setRoute(isRoute); + entity.setStatus(CommonConstant.STATUS_1); + entity.setRelationType(CommonConstant.HOME_RELATION_DEFAULT); + return entity; + } + + @Override + public void cleanDefaultIndexCache() { + redisUtil.del(DefIndexConst.CACHE_KEY + "::" + DefIndexConst.DEF_INDEX_ALL); + } + + /** + * 切换默认门户 + * @param sysRoleIndex + */ + @Override + public void changeDefHome(SysRoleIndex sysRoleIndex) { + // 1. 先查询出配置信息 + String username = sysRoleIndex.getRoleCode(); + //当前状态(1:工作台/门户 0:菜单默认) + String status = sysRoleIndex.getStatus(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysRoleIndex::getRoleCode, username); + queryWrapper.eq(SysRoleIndex::getRelationType,CommonConstant.HOME_RELATION_USER); + queryWrapper.orderByAsc(SysRoleIndex::getPriority); + List list = super.list(queryWrapper); + boolean success = false; + if(CommonConstant.STATUS_1.equalsIgnoreCase(status)){ + // 2. 如果存在则编辑 + if (!CollectionUtils.isEmpty(list)) { + sysRoleIndex.setId(list.get(0).getId()); + sysRoleIndex.setStatus(CommonConstant.STATUS_1); + sysRoleIndex.setRoute(true); + success = super.updateById(sysRoleIndex); + } else { + // 3. 如果不存在则新增 + sysRoleIndex.setRelationType(CommonConstant.HOME_RELATION_USER); + sysRoleIndex.setStatus(CommonConstant.STATUS_1); + sysRoleIndex.setRoute(true); + success = super.save(sysRoleIndex); + } + }else { + // 0:菜单默认,则是菜单默认首页 + if (!CollectionUtils.isEmpty(list)) { + //将用户级别的首页配置状态设置成0 + for (int i = 0; i < list.size(); i++) { + SysRoleIndex roleIndex = list.get(i); + roleIndex.setStatus(CommonConstant.STATUS_0); + success = super.updateById(roleIndex); + } + } + } + // 4. 清理缓存 + if (success) { + this.cleanDefaultIndexCache(); + redisUtil.del(DefIndexConst.CACHE_TYPE + username); + } + // 5. 缓存类型 + //当前地址 + String url = sysRoleIndex.getUrl(); + //首页类型(默认首页) + String type = DefIndexConst.HOME_TYPE_MENU; + if(oConvertUtils.isNotEmpty(url) && CommonConstant.STATUS_1.equalsIgnoreCase(status)){ + type = url.contains(DefIndexConst.HOME_TYPE_SYSTEM) ? DefIndexConst.HOME_TYPE_SYSTEM : DefIndexConst.HOME_TYPE_PERSONAL; + } + redisUtil.set(DefIndexConst.CACHE_TYPE + username,type); + } + + /** + * 更新其他全局默认的状态值 + * + * @param roleCode + * @param status + * @param id + */ + @Override + public void updateOtherDefaultStatus(String roleCode, String status, String id) { + //roleCode是全局默认 + if(oConvertUtils.isNotEmpty(roleCode) && DefIndexConst.DEF_INDEX_ALL.equals(roleCode)){ + //状态为开启状态 + if(oConvertUtils.isNotEmpty(status) && CommonConstant.STATUS_1.equals(status)){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysRoleIndex::getRoleCode,roleCode); + queryWrapper.eq(SysRoleIndex::getStatus,CommonConstant.STATUS_1); + queryWrapper.ne(SysRoleIndex::getId,id); + queryWrapper.select(SysRoleIndex::getId); + List list = this.list(queryWrapper); + if(CollectionUtil.isNotEmpty(list)){ + list.forEach(sysRoleIndex -> { + sysRoleIndex.setStatus(CommonConstant.STATUS_0); + }); + this.updateBatchById(list); + } + } + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRolePermissionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRolePermissionServiceImpl.java new file mode 100644 index 0000000..1578e04 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRolePermissionServiceImpl.java @@ -0,0 +1,119 @@ +package com.ghb.base.modules.system.service.impl; + +import java.util.*; + +import com.ghb.base.common.util.IpUtils; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysRolePermission; +import com.ghb.base.modules.system.mapper.SysRolePermissionMapper; +import com.ghb.base.modules.system.service.ISysRolePermissionService; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import org.springframework.stereotype.Service; + +import jakarta.servlet.http.HttpServletRequest; + +/** + *

+ * 角色权限表 服务实现类 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +@Service +public class SysRolePermissionServiceImpl extends ServiceImpl implements ISysRolePermissionService { + + @Override + public void saveRolePermission(String roleId, String permissionIds) { + String ip = ""; + try { + //获取request + HttpServletRequest request = SpringContextUtils.getHttpServletRequest(); + //获取IP地址 + ip = IpUtils.getIpAddr(request); + } catch (Exception e) { + ip = "127.0.0.1"; + } + LambdaQueryWrapper query = new QueryWrapper().lambda().eq(SysRolePermission::getRoleId, roleId); + this.remove(query); + List list = new ArrayList(); + String[] arr = permissionIds.split(","); + for (String p : arr) { + if(oConvertUtils.isNotEmpty(p)) { + SysRolePermission rolepms = new SysRolePermission(roleId, p); + rolepms.setOperateDate(new Date()); + rolepms.setOperateIp(ip); + list.add(rolepms); + } + } + this.saveBatch(list); + } + + @Override + public void saveRolePermission(String roleId, String permissionIds, String lastPermissionIds) { + String ip = ""; + try { + //获取request + HttpServletRequest request = SpringContextUtils.getHttpServletRequest(); + //获取IP地址 + ip = IpUtils.getIpAddr(request); + } catch (Exception e) { + ip = "127.0.0.1"; + } + List add = getDiff(lastPermissionIds,permissionIds); + if(add!=null && add.size()>0) { + List list = new ArrayList(); + for (String p : add) { + if(oConvertUtils.isNotEmpty(p)) { + SysRolePermission rolepms = new SysRolePermission(roleId, p); + rolepms.setOperateDate(new Date()); + rolepms.setOperateIp(ip); + list.add(rolepms); + } + } + this.saveBatch(list); + } + + List delete = getDiff(permissionIds,lastPermissionIds); + if(delete!=null && delete.size()>0) { + for (String permissionId : delete) { + this.remove(new QueryWrapper().lambda().eq(SysRolePermission::getRoleId, roleId).eq(SysRolePermission::getPermissionId, permissionId)); + } + } + } + + /** + * 从diff中找出main中没有的元素 + * @param main + * @param diff + * @return + */ + private List getDiff(String main,String diff){ + if(oConvertUtils.isEmpty(diff)) { + return null; + } + if(oConvertUtils.isEmpty(main)) { + return Arrays.asList(diff.split(",")); + } + + String[] mainArr = main.split(","); + String[] diffArr = diff.split(","); + Map map = new HashMap(5); + for (String string : mainArr) { + map.put(string, 1); + } + List res = new ArrayList(); + for (String key : diffArr) { + if(oConvertUtils.isNotEmpty(key) && !map.containsKey(key)) { + res.add(key); + } + } + return res; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRoleServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..9637097 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,119 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.ImportExcelUtil; +import com.ghb.base.modules.system.entity.SysRole; +import com.ghb.base.modules.system.mapper.SysRoleMapper; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import com.ghb.base.modules.system.service.ISysRoleService; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + *

+ * 角色表 服务实现类 + *

+ * + * @Author scott + * @since 2018-12-19 + */ +@Service +public class SysRoleServiceImpl extends ServiceImpl implements ISysRoleService { + @Autowired + SysRoleMapper sysRoleMapper; + @Autowired + SysUserMapper sysUserMapper; + + + @Override + public Page listAllSysRole(Page page, SysRole role) { + return page.setRecords(sysRoleMapper.listAllSysRole(page,role)); + } + + @Override + public SysRole getRoleNoTenant(String roleCode) { + return sysRoleMapper.getRoleNoTenant(roleCode); + } + + @Override + public Result importExcelCheckRoleCode(MultipartFile file, ImportParams params) throws Exception { + List listSysRoles = ExcelImportUtil.importExcel(file.getInputStream(), SysRole.class, params); + int totalCount = listSysRoles.size(); + List errorStrs = new ArrayList<>(); + + // 去除 listSysRoles 中重复的数据 + for (int i = 0; i < listSysRoles.size(); i++) { + String roleCodeI =((SysRole)listSysRoles.get(i)).getRoleCode(); + for (int j = i + 1; j < listSysRoles.size(); j++) { + String roleCodeJ =((SysRole)listSysRoles.get(j)).getRoleCode(); + // 发现重复数据 + if (roleCodeI.equals(roleCodeJ)) { + errorStrs.add("第 " + (j + 1) + " 行的 roleCode 值:" + roleCodeI + " 已存在,忽略导入"); + listSysRoles.remove(j); + break; + } + } + } + // 去掉 sql 中的重复数据 + Integer errorLines=0; + Integer successLines=0; + List list = ImportExcelUtil.importDateSave(listSysRoles, ISysRoleService.class, errorStrs, CommonConstant.SQL_INDEX_UNIQ_SYS_ROLE_CODE); + errorLines+=list.size(); + successLines+=(listSysRoles.size()-errorLines); + return ImportExcelUtil.imporReturnRes(errorLines,successLines,list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteRole(String roleid) { + //1.删除角色和用户关系 + sysRoleMapper.deleteRoleUserRelation(roleid); + //2.删除角色和权限关系 + sysRoleMapper.deleteRolePermissionRelation(roleid); + //3.删除角色 + this.removeById(roleid); + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteBatchRole(String[] roleIds) { + //1.删除角色和用户关系 + sysUserMapper.deleteBathRoleUserRelation(roleIds); + //2.删除角色和权限关系 + sysUserMapper.deleteBathRolePermissionRelation(roleIds); + //3.删除角色 + this.removeByIds(Arrays.asList(roleIds)); + return true; + } + + @Override + public Long getRoleCountByTenantId(String id, Integer tenantId) { + return sysRoleMapper.getRoleCountByTenantId(id,tenantId); + } + + @Override + public void checkAdminRoleRejectDel(String ids) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.in(SysRole::getId,Arrays.asList(ids.split(SymbolConstant.COMMA))); + query.eq(SysRole::getRoleCode,"admin"); + Long adminRoleCount = sysRoleMapper.selectCount(query); + if(adminRoleCount>0){ + throw new GhbBootException("admin角色,不允许删除!"); + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTableWhiteListServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTableWhiteListServiceImpl.java new file mode 100644 index 0000000..081b922 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTableWhiteListServiceImpl.java @@ -0,0 +1,149 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.firewall.SqlInjection.IDictTableWhiteListHandler; +import com.ghb.base.modules.system.entity.SysTableWhiteList; +import com.ghb.base.modules.system.mapper.SysTableWhiteListMapper; +import com.ghb.base.modules.system.service.ISysTableWhiteListService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * @Description: 系统表白名单 + * @Author: Ghb-boot + * @Date: 2023-09-12 + * @Version: V1.0 + */ +@Slf4j +@Service +public class SysTableWhiteListServiceImpl extends ServiceImpl implements ISysTableWhiteListService { + + @Lazy + @Autowired + IDictTableWhiteListHandler whiteListHandler; + + @Override + public boolean add(SysTableWhiteList sysTableWhiteList) { + this.checkEntity(sysTableWhiteList); + if (super.save(sysTableWhiteList)) { + // 清空缓存 + whiteListHandler.clear(); + return true; + } + return false; + } + + @Override + public boolean edit(SysTableWhiteList sysTableWhiteList) { + this.checkEntity(sysTableWhiteList); + if (super.updateById(sysTableWhiteList)) { + // 清空缓存 + whiteListHandler.clear(); + return true; + } + return false; + } + + /** + * 检查需要新增或更新的实体是否符合规范 + * + * @param sysTableWhiteList + */ + private void checkEntity(SysTableWhiteList sysTableWhiteList) { + if (sysTableWhiteList == null) { + throw new GhbBootException("操作失败,实体为空!"); + } + if (oConvertUtils.isEmpty(sysTableWhiteList.getTableName())) { + throw new GhbBootException("操作失败,表名不能为空!"); + } + if (oConvertUtils.isEmpty(sysTableWhiteList.getFieldName())) { + throw new GhbBootException("操作失败,字段名不能为空!"); + } + // 将表名和字段名转换成小写 + sysTableWhiteList.setTableName(sysTableWhiteList.getTableName().toLowerCase()); + sysTableWhiteList.setFieldName(sysTableWhiteList.getFieldName().toLowerCase()); + // 如果status为空,则默认启用 + if (oConvertUtils.isEmpty(sysTableWhiteList.getStatus())) { + sysTableWhiteList.setStatus(CommonConstant.STATUS_1); + } + } + + @Override + public boolean deleteByIds(String ids) { + if (oConvertUtils.isEmpty(ids)) { + return false; + } + List idList = Arrays.asList(ids.split(",")); + if (super.removeByIds(idList)) { + // 清空缓存 + whiteListHandler.clear(); + return true; + } + return false; + } + + @Override + public SysTableWhiteList autoAdd(String tableName, String fieldName) { + if (oConvertUtils.isEmpty(tableName)) { + throw new GhbBootException("操作失败,表名不能为空!"); + } + if (oConvertUtils.isEmpty(fieldName)) { + throw new GhbBootException("操作失败,字段名不能为空!"); + } + // 统一转换成小写 + tableName = tableName.toLowerCase(); + fieldName = fieldName.toLowerCase(); + // 查询是否已经存在 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysTableWhiteList::getTableName, tableName); + SysTableWhiteList getEntity = super.getOne(queryWrapper); + if (getEntity != null) { + // 如果已经存在,并且已禁用,则抛出异常 + if (CommonConstant.STATUS_0.equals(getEntity.getStatus())) { + throw new GhbBootException("[白名单] 表名已存在,但是已被禁用,请先启用!tableName=" + tableName); + } + // 合并字段 + Set oldFieldSet = new HashSet<>(Arrays.asList(getEntity.getFieldName().split(","))); + Set newFieldSet = new HashSet<>(Arrays.asList(fieldName.split(","))); + oldFieldSet.addAll(newFieldSet); + getEntity.setFieldName(String.join(",", oldFieldSet)); + this.checkEntity(getEntity); + super.updateById(getEntity); + log.info("修改表单白名单项,表名:{},oldFieldSet: {},newFieldSet:{}", tableName, oldFieldSet.toArray(), newFieldSet.toArray()); + return getEntity; + } else { + // 新增白名单项 + SysTableWhiteList saveEntity = new SysTableWhiteList(); + saveEntity.setTableName(tableName); + saveEntity.setFieldName(fieldName); + saveEntity.setStatus(CommonConstant.STATUS_1); + this.checkEntity(saveEntity); + super.save(saveEntity); + log.info("新增表单白名单项: 表名:{},配置 > {}", tableName, saveEntity.toString()); + return saveEntity; + } + } + + @Override + public Map getAllConfigMap() { + Map map = new HashMap<>(); + List allData = super.list(); + for (SysTableWhiteList item : allData) { + // 只有启用的才放入map + if (CommonConstant.STATUS_1.equals(item.getStatus())) { + // 表名和字段名都转成小写,防止大小写不一致 + map.put(item.getTableName().toLowerCase(), item.getFieldName().toLowerCase()); + } + } + return map; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTenantPackServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTenantPackServiceImpl.java new file mode 100644 index 0000000..a36e414 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTenantPackServiceImpl.java @@ -0,0 +1,496 @@ +package com.ghb.base.modules.system.service.impl; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.TenantConstant; +import com.ghb.base.common.exception.GhbBootBizTipException; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.aop.TenantLog; +import com.ghb.base.modules.system.entity.SysPackPermission; +import com.ghb.base.modules.system.entity.SysTenantPack; +import com.ghb.base.modules.system.entity.SysTenantPackUser; +import com.ghb.base.modules.system.entity.SysUserTenant; +import com.ghb.base.modules.system.mapper.*; +import com.ghb.base.modules.system.service.ISysTenantPackService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Description: 租户产品包 + * @Author: Ghb-boot + * @Date: 2022-12-31 + * @Version: V1.0 + */ +@Service +public class SysTenantPackServiceImpl extends ServiceImpl implements ISysTenantPackService { + + @Autowired + private SysTenantPackMapper sysTenantPackMapper; + + @Autowired + private SysTenantPackUserMapper sysTenantPackUserMapper; + + @Autowired + private SysPackPermissionMapper sysPackPermissionMapper; + + @Autowired + private SysRoleMapper sysRoleMapper; + + @Autowired + private SysUserTenantMapper sysUserTenantMapper; + + @Override + public void addPackPermission(SysTenantPack sysTenantPack) { + //如果是默认租户套餐包,则需要设置code编码,再编辑默认套餐找自定义套餐的时候用到 + if(CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())){ + String packCode = CommonConstant.TENANT_PACK_DEFAULT + RandomUtil.randomNumbers(4).toLowerCase(); + sysTenantPack.setPackCode(packCode); + } + sysTenantPackMapper.insert(sysTenantPack); + String permissionIds = sysTenantPack.getPermissionIds(); + if (oConvertUtils.isNotEmpty(permissionIds)) { + String packId = sysTenantPack.getId(); + String[] permissionIdArray = permissionIds.split(SymbolConstant.COMMA); + for (String permissionId : permissionIdArray) { + this.addPermission(packId, permissionId); + } + } + + //如果是自定义套餐包的情况下再将新增套餐和用户关系 + if(!CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())) { + //如果需要自动分配给用户时候再去添加用户与套餐的关系数据 + if(oConvertUtils.isNotEmpty(sysTenantPack.getIzSysn()) && CommonConstant.STATUS_1.equals(sysTenantPack.getIzSysn())) { + //根据租户id和套餐id添加用户与套餐关系数据 + this.addPackUserByPackTenantId(sysTenantPack.getTenantId(), sysTenantPack.getId()); + } + } + } + + /** + * 根据租户id和套餐id添加用户与套餐关系数据 + * + * @param tenantId + * @param packId + */ + private void addPackUserByPackTenantId(Integer tenantId, String packId) { + if (null != tenantId && tenantId != 0) { + List userIds = sysUserTenantMapper.getUserIdsByTenantId(tenantId); + if (CollectionUtil.isNotEmpty(userIds)) { + // 查询已存在的用户 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysTenantPackUser::getTenantId, tenantId); + query.eq(SysTenantPackUser::getPackId, packId); + query.in(SysTenantPackUser::getUserId, userIds); + List existingUsers = sysTenantPackUserMapper.selectList(query); + // 提取已存在的用户ID + List existingUserIds = existingUsers.stream() + .map(SysTenantPackUser::getUserId) + .toList(); + // 过滤出需要新增的用户ID + List newUserIds = userIds.stream() + .filter(userId -> !existingUserIds.contains(userId)) + .toList(); + for (String userId : newUserIds) { + SysTenantPackUser tenantPackUser = new SysTenantPackUser(tenantId, packId, userId); + sysTenantPackUserMapper.insert(tenantPackUser); + } + } + } + } + + @Override + public List setPermissions(List records) { + for (SysTenantPack pack : records) { + List permissionIds = sysPackPermissionMapper.getPermissionsByPackId(pack.getId()); + if (null != permissionIds && permissionIds.size() > 0) { + String ids = String.join(SymbolConstant.COMMA, permissionIds); + pack.setPermissionIds(ids); + } + } + return records; + } + + @Override + public void editPackPermission(SysTenantPack sysTenantPack) { + //数据库汇总的id + List oldPermissionIds = sysPackPermissionMapper.getPermissionsByPackId(sysTenantPack.getId()); + //前台传过来的需要修改的id + String permissionIds = sysTenantPack.getPermissionIds(); + //如果传过来的菜单id为空,那么就删除数据库中所有菜单 + if (oConvertUtils.isEmpty(permissionIds)) { + this.deletePackPermission(sysTenantPack.getId(), null); + //如果是默认套餐包,需要删除其他关联默认产品包下的角色与菜单的关系 + if(CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())){ + this.deleteDefaultPackPermission(sysTenantPack.getPackCode(), null); + } + } else if (oConvertUtils.isNotEmpty(permissionIds) && oConvertUtils.isEmpty(oldPermissionIds)) { + //如果传过来的菜单id不为空但是数据库的菜单id为空,那么就新增 + this.addPermission(sysTenantPack.getId(), permissionIds); + //如果是默认套餐包,需要新增其他关联默认产品包下的角色与菜单的关系 + if(CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())){ + this.addDefaultPackPermission(sysTenantPack.getPackCode(), permissionIds); + } + } else { + //都不为空,需要比较,进行添加或删除 + if (oConvertUtils.isNotEmpty(oldPermissionIds)) { + //找到新的租户id与原来的租户id不同之处,进行删除 + List permissionList = oldPermissionIds.stream().filter(item -> !permissionIds.contains(item)).collect(Collectors.toList()); + if (permissionList.size() > 0) { + for (String permission : permissionList) { + this.deletePackPermission(sysTenantPack.getId(), permission); + //如果是默认套餐包,需要删除其他关联默认产品包下的角色与菜单的关系 + if(CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())){ + this.deleteDefaultPackPermission(sysTenantPack.getPackCode(), permission); + } + } + } + + //找到原来菜单id与新的菜单id不同之处,进行新增 + List permissionAddList = Arrays.stream(permissionIds.split(SymbolConstant.COMMA)).filter(item -> !oldPermissionIds.contains(item)).collect(Collectors.toList()); + if (permissionAddList.size() > 0) { + for (String permission : permissionAddList) { + this.addPermission(sysTenantPack.getId(), permission); + //如果是默认套餐包,需要新增其他关联默认产品包下的角色与菜单的关系 + if(CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())){ + this.addDefaultPackPermission(sysTenantPack.getPackCode(), permission); + } + } + } + } + } + sysTenantPackMapper.updateById(sysTenantPack); + //如果是默认套餐包,则更新和当前匹配默认套餐包匹配的数据 + if(CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())){ + //同步同 packCode 下的相关套餐包数据 + this.syncRelatedPackDataByDefaultPack(sysTenantPack); + } + + //如果是自定义套餐包的情况下再将新增套餐和用户关系 + if(!CommonConstant.TENANT_PACK_DEFAULT.equals(sysTenantPack.getPackType())) { + //如果需要自动分配给用户时候再去添加用户与套餐的关系数据 + if(oConvertUtils.isNotEmpty(sysTenantPack.getIzSysn()) && CommonConstant.STATUS_1.equals(sysTenantPack.getIzSysn())) { + //根据租户id和套餐id添加用户与套餐关系数据 + this.addPackUserByPackTenantId(sysTenantPack.getTenantId(), sysTenantPack.getId()); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteTenantPack(String ids) { + String[] idsArray = ids.split(SymbolConstant.COMMA); + for (String id : idsArray) { + this.deletePackPermission(id,null); + //删除产品包下面的用户 + this.deletePackUser(id); + sysTenantPackMapper.deleteById(id); + } + } + + @Override + public void exitTenant(String tenantId, String userId) { + this.getById(tenantId); + } + + @Override + public void addDefaultTenantPack(Integer tenantId) { + ISysTenantPackService currentService = SpringContextUtils.getApplicationContext().getBean(ISysTenantPackService.class); + // 创建租户超级管理员 + SysTenantPack superAdminPack = new SysTenantPack(tenantId, "超级管理员", TenantConstant.SUPER_ADMIN); + superAdminPack.setIzSysn(CommonConstant.STATUS_0); + //step.1 创建租户套餐包(超级管理员) + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysTenantPack::getTenantId,tenantId); + query.eq(SysTenantPack::getPackCode, TenantConstant.SUPER_ADMIN); + SysTenantPack sysTenantPackSuperAdmin = currentService.getOne(query); + String packId = ""; + if(null == sysTenantPackSuperAdmin){ + packId = currentService.saveOne(superAdminPack); + }else{ + packId = sysTenantPackSuperAdmin.getId(); + } + //step.1.2 补充人员与套餐包的关系数据 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + SysTenantPackUser packUser = new SysTenantPackUser(tenantId, packId, sysUser.getId()); + packUser.setRealname(sysUser.getRealname()); + packUser.setPackName(superAdminPack.getPackName()); + currentService.savePackUser(packUser); + + //step.2 创建租户套餐包(组织账户管理员)和 添加人员关系数据 + query.eq(SysTenantPack::getTenantId,tenantId); + query.eq(SysTenantPack::getPackCode, TenantConstant.ACCOUNT_ADMIN); + SysTenantPack sysTenantPackAccountAdmin = currentService.getOne(query); + if(null == sysTenantPackAccountAdmin){ + // 创建超级管理员 + SysTenantPack accountAdminPack = new SysTenantPack(tenantId, "组织账户管理员", TenantConstant.ACCOUNT_ADMIN); + accountAdminPack.setIzSysn(CommonConstant.STATUS_0); + currentService.saveOne(accountAdminPack); + } + + //step.3 创建租户套餐包(组织应用管理员) + query.eq(SysTenantPack::getTenantId,tenantId); + query.eq(SysTenantPack::getPackCode, TenantConstant.APP_ADMIN); + SysTenantPack sysTenantPackAppAdmin = currentService.getOne(query); + if(null == sysTenantPackAppAdmin){ + // 创建超级管理员 + SysTenantPack appAdminPack = new SysTenantPack(tenantId, "组织应用管理员", TenantConstant.APP_ADMIN); + appAdminPack.setIzSysn(CommonConstant.STATUS_0); + currentService.saveOne(appAdminPack); + } + + } + + @TenantLog(2) + @Override + public String saveOne(SysTenantPack sysTenantPack) { + sysTenantPackMapper.insert(sysTenantPack); + return sysTenantPack.getId(); + } + + @TenantLog(2) + @Override + public void savePackUser(SysTenantPackUser sysTenantPackUser) { + sysTenantPackUser.setStatus(1); + sysTenantPackUserMapper.insert(sysTenantPackUser); + } + + @Override + public SysTenantPack getSysTenantPack(Integer tenantId, String packCode) { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysTenantPack::getPackCode, packCode) + .eq(SysTenantPack::getTenantId, tenantId); + List list = baseMapper.selectList(query); + if(list!=null && list.size()>0){ + SysTenantPack pack = list.get(0); + if(pack!=null && pack.getId()!=null){ + return pack; + } + } + return null; + } + + /** + * 添加菜单 + * + * @param packId + * @param permissionId + */ + public void addPermission(String packId, String permissionId) { + SysPackPermission permission = new SysPackPermission(); + permission.setPermissionId(permissionId); + permission.setPackId(packId); + sysPackPermissionMapper.insert(permission); + } + + /** + * 根据包名id和菜单id删除关系表 + * + * @param packId + * @param permissionId + */ + public void deletePackPermission(String packId, String permissionId) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysPackPermission::getPackId, packId); + if (oConvertUtils.isNotEmpty(permissionId)) { + query.eq(SysPackPermission::getPermissionId, permissionId); + } + sysPackPermissionMapper.delete(query); + } + + @Override + public void addTenantDefaultPack(Integer tenantId) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysTenantPack::getPackType,"default"); + List sysTenantPacks = sysTenantPackMapper.selectList(query); + // 取当前租户用户列表 + List userIds = sysUserTenantMapper.getUserIdsByTenantId(tenantId); + for (SysTenantPack sysTenantPack: sysTenantPacks) { + // 代码逻辑说明: 【QQYUN-14007】演示系统,初始化租户套餐很慢--- + syncDefaultPack2CurrentTenant(tenantId, sysTenantPack, userIds); + } + } + + @Override + public void syncDefaultPack(Integer tenantId) { + // 查询默认套餐包 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysTenantPack::getPackType,"default"); + List sysDefaultTenantPacks = sysTenantPackMapper.selectList(query); + // 查询当前租户套餐包 + query = new LambdaQueryWrapper<>(); + query.eq(SysTenantPack::getPackType,"custom"); + query.eq(SysTenantPack::getTenantId, tenantId); + List currentTenantPacks = sysTenantPackMapper.selectList(query); + // 代码逻辑说明: 【QQYUN-14007】演示系统,初始化租户套餐很慢--- + Map currentTenantPackMap; + if (oConvertUtils.listIsNotEmpty(currentTenantPacks)) { + currentTenantPackMap = currentTenantPacks.stream().collect(Collectors.toMap(SysTenantPack::getPackName, o -> o, (existing, replacement) -> existing)); + } else { + currentTenantPackMap = new HashMap(); + } + // 预取当前租户用户列表,避免在循环中重复查询 + List userIds = sysUserTenantMapper.getUserIdsByTenantId(tenantId); + // 计算需要同步的默认套餐包列表 + List packsToSync = sysDefaultTenantPacks.stream() + .filter(p -> !currentTenantPackMap.containsKey(p.getPackName())) + .collect(Collectors.toList()); + + // 并行同步缺失的套餐包 + packsToSync.parallelStream().forEach(defaultPacks -> { + syncDefaultPack2CurrentTenant(tenantId, defaultPacks, userIds); + }); + } + + /** + * 同步默认套餐包到当前租户 + * for [QQYUN-11032]【Ghb】租户套餐管理增加初始化套餐包按钮 + * @param tenantId 目标租户 + * @param defaultPacks 默认套餐包 + * @author chenrui + * @date 2025/2/5 19:41 + */ + private void syncDefaultPack2CurrentTenant(Integer tenantId, SysTenantPack defaultPacks, List userIds) { + SysTenantPack pack = new SysTenantPack(); + BeanUtils.copyProperties(defaultPacks,pack); + pack.setTenantId(tenantId); + pack.setPackType("custom"); + pack.setId(""); + sysTenantPackMapper.insert(pack); + List permissionsByPackId = sysPackPermissionMapper.getPermissionsByPackId(defaultPacks.getId()); + List permissionList = new ArrayList<>(); + for (String permission:permissionsByPackId) { + SysPackPermission packPermission = new SysPackPermission(); + packPermission.setPackId(pack.getId()); + packPermission.setPermissionId(permission); + permissionList.add(packPermission); + } + if(CollectionUtil.isNotEmpty(permissionList)){ + sysPackPermissionMapper.insert(permissionList); + } + //如果需要自动分配给用户时候再去添加用户与套餐的关系数据 + if(oConvertUtils.isNotEmpty(defaultPacks.getIzSysn()) && CommonConstant.STATUS_1.equals(defaultPacks.getIzSysn())) { + List packUserList = new ArrayList<>(); + if (oConvertUtils.isNotEmpty(userIds)) { + for (String userId : userIds) { + //根据租户id和套餐id添加用户与套餐关系数据 + SysTenantPackUser tenantPackUser = new SysTenantPackUser(tenantId, pack.getId(), userId); + packUserList.add(tenantPackUser); + } + sysTenantPackUserMapper.insert(packUserList); + } + } + } + + /** + * 删除产品包下面的用户 + * @param packId + */ + private void deletePackUser(String packId) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysTenantPackUser::getPackId, packId); + sysTenantPackUserMapper.delete(query); + } + + @Override + public List getPackIdByUserIdAndTenantId(String userId, Integer tenantId) { + return sysTenantPackUserMapper.getPackIdByTenantIdAndUserId(tenantId, userId); + } + + @Override + public List getPackListByTenantId(String tenantId) { + return sysTenantPackUserMapper.getPackListByTenantId(oConvertUtils.getInt(tenantId)); + } + + /** + * 根据套餐包的code 新增其他关联默认产品包下的角色与菜单的关系 + * + * @param packCode + * @param permission + */ + private void addDefaultPackPermission(String packCode, String permission) { + if (oConvertUtils.isEmpty(packCode)) { + return; + } + //查询当前匹配非默认套餐包的其他默认套餐包 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.ne(SysTenantPack::getPackType, CommonConstant.TENANT_PACK_DEFAULT); + query.eq(SysTenantPack::getPackCode, packCode); + List otherDefaultPacks = sysTenantPackMapper.selectList(query); + for (SysTenantPack pack : otherDefaultPacks) { + //新增套餐包用户菜单权限 + this.addPermission(pack.getId(), permission); + } + } + + /** + * 根据套餐包的code 删除其他关联默认套餐包下的角色与菜单的关系 + * + * @param packCode + * @param permissionId + */ + private void deleteDefaultPackPermission(String packCode, String permissionId) { + if (oConvertUtils.isEmpty(packCode)) { + return; + } + //查询当前匹配非默认套餐包的其他默认套餐包 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.ne(SysTenantPack::getPackType, CommonConstant.TENANT_PACK_DEFAULT); + query.eq(SysTenantPack::getPackCode, packCode); + List defaultPacks = sysTenantPackMapper.selectList(query); + for (SysTenantPack pack : defaultPacks) { + //删除套餐权限 + deletePackPermission(pack.getId(), permissionId); + } + } + + /** + * 同步同 packCode 下的相关套餐包数据 + * + * @param sysTenantPack + */ + private void syncRelatedPackDataByDefaultPack(SysTenantPack sysTenantPack) { + //查询与默认套餐相同code的套餐 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.ne(SysTenantPack::getPackType, CommonConstant.TENANT_PACK_DEFAULT); + query.eq(SysTenantPack::getPackCode, sysTenantPack.getPackCode()); + List relatedPacks = sysTenantPackMapper.selectList(query); + for (SysTenantPack pack : relatedPacks) { + //更新自定义套餐 + pack.setPackName(sysTenantPack.getPackName()); + pack.setStatus(sysTenantPack.getStatus()); + pack.setRemarks(sysTenantPack.getRemarks()); + pack.setIzSysn(sysTenantPack.getIzSysn()); + sysTenantPackMapper.updateById(pack); + //同步默认套餐报下的所有用户已 + if (oConvertUtils.isNotEmpty(sysTenantPack.getIzSysn()) && CommonConstant.STATUS_1.equals(sysTenantPack.getIzSysn())) { + this.addPackUserByPackTenantId(pack.getTenantId(), pack.getId()); + } + } + } + + + /** + * 是否为拥有管理用户权限【accountAdmin,superAdmin】 + * @param tenantId + */ + @Override + public void izHaveManageUserAuth(String tenantId) { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + long count = sysTenantPackMapper.izHaveManageUserAuth(tenantId,sysUser.getId()); + if(count == 0){ + throw new GhbBootBizTipException("你不是当前租户的组织账户管理员或超级管理员,无法进行此操作!"); + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTenantServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTenantServiceImpl.java new file mode 100644 index 0000000..d53000e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysTenantServiceImpl.java @@ -0,0 +1,1014 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.api.dto.message.BusMessageDTO; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.config.TenantContext; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbBootBizTipException; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.api.ISysBaseAPI; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.DateUtils; +import com.ghb.base.common.util.PasswordUtil; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.constant.enums.SysAnnmentTypeEnum; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.aop.TenantLog; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.mapper.*; +import com.ghb.base.modules.system.service.ISysTenantPackService; +import com.ghb.base.modules.system.service.ISysTenantService; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.vo.tenant.*; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Description: 租户实现类 + * @author: Ghb-boot + */ +@Service("sysTenantServiceImpl") +@Slf4j +public class SysTenantServiceImpl extends ServiceImpl implements ISysTenantService { + + @Autowired + ISysUserService userService; + @Autowired + private SysUserTenantMapper userTenantMapper; + @Autowired + private SysTenantMapper tenantMapper; + + @Autowired + private ISysTenantPackService sysTenantPackService; + + @Autowired + private SysTenantPackUserMapper sysTenantPackUserMapper; + + @Autowired + private ISysBaseAPI sysBaseApi; + + @Autowired + private SysUserDepartMapper sysUserDepartMapper; + + @Autowired + private SysTenantPackMapper sysTenantPackMapper; + + @Autowired + private SysPackPermissionMapper sysPackPermissionMapper; + + @Override + public List queryEffectiveTenant(Collection idList) { + if(oConvertUtils.listIsEmpty(idList)){ + return null; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysTenant::getId, idList); + queryWrapper.eq(SysTenant::getStatus, Integer.valueOf(CommonConstant.STATUS_1)); + //此处查询忽略时间条件 + return super.list(queryWrapper); + } + + @Override + public Long countUserLinkTenant(String id) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getTenantId,id); + query.eq(SysUserTenant::getStatus,CommonConstant.STATUS_1); + // 查找出已被关联的用户数量 + return userTenantMapper.selectCount(query); + } + + @Override + public boolean removeTenantById(String id) { + // 查找出已被关联的用户数量 + return super.removeById(Integer.parseInt(id)); + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public void invitationUserJoin(String ids, String phone,String username) { + String[] idArray = ids.split(SymbolConstant.COMMA); + String userId = null; + SysUser userByPhone = null; + // 代码逻辑说明: 【QQYUN-4605】后台的邀请谁加入租户,没办法选不是租户下的用户,通过手机号邀请------------ + if(oConvertUtils.isNotEmpty(phone)){ + userByPhone = userService.getUserByPhone(phone); + //说明用户不存在 + if(null == userByPhone){ + throw new GhbBootException("当前用户不存在,请核对手机号"); + } + userId = userByPhone.getId(); + }else{ + userByPhone = userService.getUserByName(username); + //说明用户不存在 + if(null == userByPhone){ + throw new GhbBootException("当前用户不存在,请核对手机号"); + } + userId = userByPhone.getId(); + } + + //循环租户id + for (String id:idArray) { + //获取被邀请人是否已存在 + SysUserTenant userTenant = userTenantMapper.getUserTenantByTenantId(userId, Integer.valueOf(id)); + if(null == userTenant){ + SysUserTenant relation = new SysUserTenant(); + relation.setUserId(userId); + relation.setTenantId(Integer.valueOf(id)); + relation.setStatus(CommonConstant.USER_TENANT_NORMAL); + userTenantMapper.insert(relation); + //给当前用户添加租户下的所有套餐 + this.addPackUser(userId,id); + //邀请用户加入租户,发送消息 + this.sendInvitationTenantMessage(userByPhone,id); + }else{ + // 代码逻辑说明: 【QQYUN-5885】邀请用户加入提示不准确------------ + String tenantErrorInfo = getTenantErrorInfo(userTenant.getStatus()); + String errMsg = "手机号用户:" + userByPhone.getPhone() + " 昵称:" + userByPhone.getRealname() + "," + tenantErrorInfo; + throw new GhbBootException(errMsg); + } + } + } + + /** + * 低代码下发送邀请加入租户消息 + * + * @param user + * @param id + */ + private void sendInvitationTenantMessage(SysUser user, String id) { + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + // 发消息 + SysTenant sysTenant = this.baseMapper.querySysTenant((Integer.valueOf(id))); + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setToAll(false); + messageDTO.setToUser(user.getUsername()); + messageDTO.setFromUser("system"); + String title = sysUser.getRealname() + " 邀请您加入了 "+sysTenant.getName()+"。"; + messageDTO.setTitle(title); + Map data = new HashMap<>(); + messageDTO.setData(data); + messageDTO.setContent(title); + messageDTO.setType("system"); + messageDTO.setCategory(CommonConstant.MSG_CATEGORY_1); + sysBaseApi.sendSysAnnouncement(messageDTO); + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public void leaveTenant(String userIds, String tenantId) { + String[] userIdArray = userIds.split(SymbolConstant.COMMA); + for (String userId:userIdArray) { + // 代码逻辑说明: [QQYUN-3371]租户逻辑改造,改成关系表------------ + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getTenantId,tenantId); + query.eq(SysUserTenant::getUserId,userId); + userTenantMapper.delete(query); + //代码逻辑说明: 【QQYUN-13720】移出用户当前租户,没有系统提醒--- + // 给移除人员发送消息 + SysTenantPackUser sysTenantPackUser = new SysTenantPackUser(); + sysTenantPackUser.setTenantId(Integer.valueOf(tenantId)); + sysTenantPackUser.setUserId(userId); + sendMsgForDelete(sysTenantPackUser); + } + //租户移除用户,直接删除用户租户产品包 + sysTenantPackUserMapper.deletePackUserByTenantId(Integer.valueOf(tenantId),Arrays.asList(userIds.split(SymbolConstant.COMMA))); + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public Integer saveTenantJoinUser(SysTenant sysTenant, String userId) { + //添加租户 + this.saveTenant(sysTenant); + + // 添加租户产品包 + Integer tenantId = sysTenant.getId(); + sysTenantPackService.addDefaultTenantPack(tenantId); + + //添加租户到关系表 + return tenantId; + } + + @Override + public void saveTenant(SysTenant sysTenant){ + //获取租户id + sysTenant.setId(this.tenantIdGenerate()); + sysTenant.setHouseNumber(RandomUtil.randomStringUpper(6)); + sysTenant.setDelFlag(CommonConstant.DEL_FLAG_0); + this.save(sysTenant); + //代码逻辑说明:【QQYUN-5723】1、把当前创建人加入到租户关系里面------------ + //当前登录人的id + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + this.saveTenantRelation(sysTenant.getId(),loginUser.getId()); + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public Integer joinTenantByHouseNumber(SysTenant sysTenant, String userId) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysTenant::getHouseNumber,sysTenant.getHouseNumber()); + SysTenant one = this.getOne(query); + //需要返回租户id(用于前台更新缓存),返回0则代表当前租户门牌号不存在 + if(null == one){ + return 0; + }else{ + LambdaQueryWrapper relationQuery = new LambdaQueryWrapper<>(); + relationQuery.eq(SysUserTenant::getTenantId,one.getId()); + relationQuery.eq(SysUserTenant::getUserId,userId); + SysUserTenant relation = userTenantMapper.selectOne(relationQuery); + if(relation != null){ + String msg = ""; + if(CommonConstant.USER_TENANT_UNDER_REVIEW.equals(relation.getStatus())){ + msg = ",状态:审核中"; + }else if(CommonConstant.USER_TENANT_REFUSE.equals(relation.getStatus())){ + throw new GhbBootBizTipException("管理员已拒绝您加入租户,请联系租户管理员"); + }else if(CommonConstant.USER_TENANT_QUIT.equals(relation.getStatus())){ + msg = ",状态:已离职"; + } + throw new GhbBootBizTipException("您已是该租户成员"+msg); + } + //用户加入门牌号审核中状态 + SysUserTenant tenant = new SysUserTenant(); + tenant.setTenantId(one.getId()); + tenant.setUserId(userId); + tenant.setStatus(CommonConstant.USER_TENANT_UNDER_REVIEW); + userTenantMapper.insert(tenant); + + // QQYUN-4526【应用】组织加入通知 + sendMsgForApplyJoinTenant(userId, one); + return tenant.getTenantId(); + } + } + + @Override + public Integer countCreateTenantNum(String userId) { + return this.userTenantMapper.countCreateTenantNum(userId); + } + + @Override + public IPage getRecycleBinPageList(Page page, SysTenant sysTenant) { + return page.setRecords(tenantMapper.getRecycleBinPageList(page,sysTenant)); + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public void deleteTenantLogic(String ids) { + String[] idArray = ids.split(SymbolConstant.COMMA); + List list = new ArrayList<>(); + //转成int类型 + for (String id:idArray) { + list.add(Integer.valueOf(id)); + } + //删除租户 + tenantMapper.deleteByTenantId(list); + //删除租户下的用户 + userTenantMapper.deleteUserByTenantId(list); + + //删除租户下的产品包 + this.deleteTenantPackByTenantId(list); + } + + @Override + public void revertTenantLogic(String ids) { + String[] idArray = ids.split(SymbolConstant.COMMA); + List list = new ArrayList<>(); + //转成int类型 + for (String id:idArray) { + list.add(Integer.valueOf(id)); + } + //还原租户 + tenantMapper.revertTenantLogic(list); + } + + /** + * 添加租户到关系表 + * @param tenantId + * @param userId + */ + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public Integer saveTenantRelation(Integer tenantId,String userId) { + SysUserTenant relation = new SysUserTenant(); + relation.setTenantId(tenantId); + relation.setUserId(userId); + relation.setStatus(CommonConstant.USER_TENANT_NORMAL); + userTenantMapper.insert(relation); + return relation.getTenantId(); + } + + /** + * 获取租户id + * @return + */ + public int tenantIdGenerate(){ + synchronized (this){ + //获取最大值id + // 代码逻辑说明: 数据库没有租户的时候,如果为空的话会报错sql返回类型不匹配------------ + int maxTenantId = oConvertUtils.getInt(tenantMapper.getMaxTenantId(),0); + if(maxTenantId >= 1000){ + return maxTenantId + 1; + }else{ + return 1000; + } + } + } + + + @Override + public void exitUserTenant(String userId, String username, String tenantId) { + int tId = Integer.parseInt(tenantId); + //获取所有租户信息 + List userIdsByTenantId = userTenantMapper.getUserIdsByTenantId(tId); + //查询当前租户是否为拥有者 + SysTenant sysTenant = tenantMapper.selectById(tId); + //如果是拥有着 + if (username.equals(sysTenant.getCreateBy())) { + //判断当前租户信息位数 + if (null != userIdsByTenantId && userIdsByTenantId.size() > 1) { + //需要指配拥有者 + throw new GhbBootException("assignedOwen"); + } else if (null != userIdsByTenantId && userIdsByTenantId.size() == 1) { + //只有拥有者的时候需要去注销租户 + throw new GhbBootException("cancelTenant"); + } else { + throw new GhbBootException("退出租户失败,租户信息已不存在"); + } + } else { + //不是拥有者直接删除 + this.leaveTenant(userId, tenantId); + this.leveUserProcess(userId, tenantId); + } + } + + @Override + public void changeOwenUserTenant(String userId, String tId) { + //查询当前用户是否存在该租户下 + // 代码逻辑说明: 租户id应该是传过来的,不应该是当前租户的------------ + int tenantId = oConvertUtils.getInt(tId, 0); + SysTenant sysTenant = tenantMapper.selectById(tenantId); + if(null == sysTenant){ + throw new GhbBootException("退出租户失败,不存在此租户"); + } + String createBy = sysTenant.getCreateBy(); + Integer count = userTenantMapper.userTenantIzExist(userId, tenantId); + if (count == 0) { + throw new GhbBootException("退出租户失败,此租户下没有该用户"); + } + //获取用户信息 + SysUser user = userService.getById(userId); + //变更拥有者 + SysTenant tenant = new SysTenant(); + tenant.setCreateBy(user.getUsername()); + tenant.setId(tenantId); + tenantMapper.updateById(tenant); + //删除当前登录用户的租户信息 + //update-begin---author:wangshuai ---date:20230705 for:旧拥有者退出后,需要将就拥有者的用户租户关系改成已离职------------ + //获取原创建人的用户id + SysUser userByName = userService.getUserByName(createBy); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getUserId,userByName.getId()); + query.eq(SysUserTenant::getTenantId,tenantId); + SysUserTenant userTenant = new SysUserTenant(); + userTenant.setStatus(CommonConstant.USER_TENANT_QUIT); + userTenantMapper.update(userTenant,query); + //离职流程 + this.leveUserProcess(userId, String.valueOf(tenantId)); + } + + /** + * 触发离职流程 + * + * @param userId + * @param tenantId + * @param tenantId + */ + private void leveUserProcess(String userId, String tenantId) { + LoginUser userInfo = new LoginUser(); + SysUser user = userService.getById(userId); + } + + @Override + public Result invitationUser(String phone, String departId) { + Result result = new Result<>(); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + //1、查询用户信息,判断用户是否存在 + SysUser userByPhone = userService.getUserByPhone(phone); + if(null == userByPhone){ + result.setSuccess(false); + result.setMessage("用户不存在"); + return result; + } + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + + //2.判断当前邀请人是否存在租户列表中 + Integer userCount = userTenantMapper.userTenantIzExist(sysUser.getId(), tenantId); + if(userCount == 0){ + result.setSuccess(false); + result.setMessage("当前管理员没有邀请权限"); + return result; + } + + //3.插入到租户信息,已存在的不予许插入 + //获取被邀请人是否已存在 + SysUserTenant sysUserTenant = userTenantMapper.getUserTenantByTenantId(userByPhone.getId(), tenantId); + //用户已存在 + if(null != sysUserTenant){ + result.setSuccess(false); + String tenantErrorInfo = getTenantErrorInfo(sysUserTenant.getStatus()); + String msg = "手机号用户:" + userByPhone.getPhone() + " 昵称:" + userByPhone.getRealname() + "," + tenantErrorInfo; + result.setMessage(msg); + return result; + } + + //4.需要用户手动同意加入 + String status = CommonConstant.USER_TENANT_INVITE; + + //5.当前用户不存在租户中,就需要将用户添加到租户中 + SysUserTenant tenant = new SysUserTenant(); + tenant.setTenantId(tenantId); + tenant.setUserId(userByPhone.getId()); + tenant.setStatus(status); + userTenantMapper.insert(tenant); + result.setSuccess(true); + result.setMessage("邀请成员成功,成员同意后方可加入"); + + //6.保存用户部门关系 + if(oConvertUtils.isNotEmpty(departId)){ + //保存用户部门关系 + this.saveUserDepart(userByPhone.getId(),departId); + } + + // QQYUN-4527【应用】邀请成员加入组织,发送消息提醒 + sendMsgForInvitation(userByPhone, tenantId, sysUser.getRealname()); + return result; + } + + @Override + public TenantDepartAuthInfo getTenantDepartAuthInfo(Integer tenantId) { + SysTenant sysTenant = this.getById(tenantId); + if(sysTenant==null) { + return null; + } + + TenantDepartAuthInfo info = new TenantDepartAuthInfo(); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + String userId = sysUser.getId(); + boolean superAdmin = false; + // 查询pack表 + List packCodeList = baseMapper.queryUserPackCode(tenantId, userId); + if(packCodeList==null || packCodeList.size()==0){ + //如果没有数据 判断租户创建人是不是当前用户 + if(sysTenant.getCreateBy().equals(sysUser.getUsername())){ + sysTenantPackService.addDefaultTenantPack(tenantId); + superAdmin = true; + }else{ + superAdmin = false; + } + } + if(superAdmin == false){ + List packCountList = baseMapper.queryTenantPackUserCount(tenantId); + info.setPackCountList(packCountList); + } + info.setSysTenant(sysTenant); + info.setSuperAdmin(superAdmin); + info.setPackCodes(packCodeList); + return info; + } + + @Override + public List queryTenantPackUserCount(Integer tenantId) { + return baseMapper.queryTenantPackUserCount(tenantId); + } + + @Override + public TenantPackModel queryTenantPack(TenantPackModel model) { + Integer tenantId = model.getTenantId(); + String packCode = model.getPackCode(); + + SysTenantPack sysTenantPack = sysTenantPackService.getSysTenantPack(tenantId, packCode); + if(sysTenantPack!=null){ + TenantPackModel tenantPackModel = new TenantPackModel(); + tenantPackModel.setPackName(sysTenantPack.getPackName()); + tenantPackModel.setPackId(sysTenantPack.getId()); + // 查询用户 + List userList = getTenantPackUserList(tenantId, sysTenantPack.getId(), 1); + tenantPackModel.setUserList(userList); + return tenantPackModel; + } + return null; + } + + @Override + public void addBatchTenantPackUser(SysTenantPackUser sysTenantPackUser) { + String userIds = sysTenantPackUser.getUserId(); + if(oConvertUtils.isNotEmpty(userIds)){ + ISysTenantService currentService = SpringContextUtils.getApplicationContext().getBean(ISysTenantService.class); + String realNames = sysTenantPackUser.getRealname(); + String[] userIdArray = userIds.split(","); + String[] realNameArray = realNames.split(","); + for(int i=0;i query = new LambdaQueryWrapper() + .eq(SysTenantPackUser::getTenantId, entity.getTenantId()) + .eq(SysTenantPackUser::getPackId, entity.getPackId()) + .eq(SysTenantPackUser::getUserId, entity.getUserId()); + SysTenantPackUser packUser = sysTenantPackUserMapper.selectOne(query); + if(packUser==null || packUser.getId()==null){ + currentService.addTenantPackUser(entity); + }else{ + if(packUser.getStatus()==0){ + packUser.setPackName(entity.getPackName()); + packUser.setRealname(realName); + currentService.addTenantPackUser(packUser); + } + } + } + } + } + + @TenantLog(2) + @Override + public void addTenantPackUser(SysTenantPackUser sysTenantPackUser) { + if(sysTenantPackUser.getId()==null){ + sysTenantPackUserMapper.insert(sysTenantPackUser); + }else{ + sysTenantPackUser.setStatus(1); + sysTenantPackUserMapper.updateById(sysTenantPackUser); + } + } + + @TenantLog(4) + @Override + public void deleteTenantPackUser(SysTenantPackUser sysTenantPackUser) { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysTenantPackUser::getUserId, sysTenantPackUser.getUserId()) + .eq(SysTenantPackUser::getPackId, sysTenantPackUser.getPackId()); + sysTenantPackUserMapper.delete(query); + + // QQYUN-4525【组织管理】管理员 2.管理员权限被移除时,给移除人员发送消息 + sendMsgForDelete(sysTenantPackUser); + } + + @Override + public List getTenantPackApplyUsers(Integer tenantId) { + return getTenantPackUserList(tenantId, null, 0); + } + + /** + * 获取租户下 某个产品包的用户 + * 或者是 租户下产品包的申请用户 + * @param tenantId + * @param packId + * @param packUserStatus + * @return + */ + private List getTenantPackUserList(Integer tenantId, String packId, Integer packUserStatus){ + // 查询用户 + List userList = baseMapper.queryPackUserList(tenantId, packId, packUserStatus); + if(userList!=null && userList.size()>0){ + List userIdList = userList.stream().map(i->i.getId()).collect(Collectors.toList()); + // 部门 + List depList = baseMapper.queryUserDepartList(userIdList); + // 职位 TODO + // 遍历用户 往用户中添加 部门信息和职位信息 + for(TenantPackUser user: userList){ + for(UserDepart dep: depList){ + if(user.getId().equals(dep.getUserId())){ + user.addDepart(dep.getDepartName()); + } + } + } + } + return userList; + } + + @Override + public void doApplyTenantPackUser(SysTenantPackUser sysTenantPackUser) { + LambdaQueryWrapper query1 = new LambdaQueryWrapper() + .eq(SysTenantPack::getTenantId, sysTenantPackUser.getTenantId()) + .eq(SysTenantPack::getPackCode, sysTenantPackUser.getPackCode()); + SysTenantPack pack = sysTenantPackService.getOne(query1); + if(pack!=null){ + sysTenantPackUser.setStatus(0); + sysTenantPackUser.setPackId(pack.getId()); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysTenantPackUser::getTenantId, sysTenantPackUser.getTenantId()) + .eq(SysTenantPackUser::getPackId, sysTenantPackUser.getPackId()) + .eq(SysTenantPackUser::getUserId, sysTenantPackUser.getUserId()); + Long count = sysTenantPackUserMapper.selectCount(query); + if(count==null || count==0){ + sysTenantPackUserMapper.insert(sysTenantPackUser); + } + // QQYUN-4524【组织关联】管理员 1.管理员权限申请-> 给相关管理员 发送通知消息 + sendMsgForApply(sysTenantPackUser.getUserId(), pack); + } + } + + /** + * 申请管理员权限发消息 + * @param userId + * @param pack + */ + private void sendMsgForApply(String userId, SysTenantPack pack){ + // 发消息 + SysUser user = userService.getById(userId); + Integer tenantId = pack.getTenantId(); + SysTenant sysTenant = this.baseMapper.querySysTenant(tenantId); + String packCode = pack.getPackCode(); + + List packCodeList = Arrays.asList(packCode.split(",")); + List userList = sysTenantPackUserMapper.queryTenantPackUserNameList(tenantId, packCodeList); + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setToAll(false); + messageDTO.setToUser(String.join(",", userList)); + messageDTO.setFromUser("system"); + String title = user.getRealname()+" 申请加入 "+sysTenant.getName()+" 的"+pack.getPackName()+"的成员。"; + messageDTO.setTitle(title); + Map data = new HashMap<>(); + messageDTO.setData(data); + messageDTO.setContent(title); + messageDTO.setType("system"); + sysBaseApi.sendTemplateMessage(messageDTO); + } + + /** + * 移除管理员权限发消息 + * @param sysTenantPackUser + */ + private void sendMsgForDelete(SysTenantPackUser sysTenantPackUser){ + // 发消息 + SysUser user = userService.getById(sysTenantPackUser.getUserId()); + SysTenant sysTenant = this.baseMapper.querySysTenant(sysTenantPackUser.getTenantId()); + LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setToAll(false); + messageDTO.setToUser(user.getUsername()); + //低代码下移出用户已做提醒,用户移出没有套餐包名称的概念 + String title = ""; + if(oConvertUtils.isNotEmpty(sysTenantPackUser.getPackName())){ + title = "您已被 "+loginUser.getRealname()+" 从 "+sysTenant.getName()+"的"+sysTenantPackUser.getPackName()+"中移除。"; + } else { + title = "您已被 "+loginUser.getRealname()+" 从 "+sysTenant.getName() + "中移除。"; + } + messageDTO.setTitle(title); + messageDTO.setFromUser("system"); + Map data = new HashMap<>(); + data.put("realname", loginUser.getRealname()); + data.put("tenantName", sysTenant.getName()); + data.put("packName", sysTenantPackUser.getPackName()); + messageDTO.setData(data); + messageDTO.setType("system"); + messageDTO.setContent(title); + sysBaseApi.sendTemplateMessage(messageDTO); + } + + /** + * 加入组织申请 发消息 + * @param userId + * @param sysTenant + */ + private void sendMsgForApplyJoinTenant(String userId, SysTenant sysTenant){ + // 发消息 + SysUser user = userService.getById(userId); + // 给超级管理员 和组织管理员发消息 + String codes = "superAdmin,accountAdmin"; + List packCodeList = Arrays.asList(codes.split(",")); + List userList = sysTenantPackUserMapper.queryTenantPackUserNameList(sysTenant.getId(), packCodeList); + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setToAll(false); + messageDTO.setToUser(String.join(",", userList)); + messageDTO.setFromUser("system"); + String title = user.getRealname()+" 申请加入 "+sysTenant.getName()+"。"; + messageDTO.setTitle(title); + Map data = new HashMap<>(); + messageDTO.setData(data); + messageDTO.setType("system"); + messageDTO.setContent(title); + sysBaseApi.sendTemplateMessage(messageDTO); + } + + /** + * 邀请成员 发消息 + * @param user + * @param tenantId + * @param realname + */ + private void sendMsgForInvitation(SysUser user, Integer tenantId, String realname){ + // 发消息 + SysTenant sysTenant = this.baseMapper.querySysTenant(tenantId); + BusMessageDTO messageDTO = new BusMessageDTO(); + messageDTO.setToAll(false); + messageDTO.setToUser(user.getUsername()); + messageDTO.setFromUser("system"); + // 代码逻辑说明: 【QQYUN-5730】租户邀请加入提示消息应该显示邀请人的名字------------ + String title = realname + " 邀请您加入 "+sysTenant.getName()+"。"; + messageDTO.setTitle(title); + Map data = new HashMap<>(); + messageDTO.setData(data); + messageDTO.setContent(title); + messageDTO.setType("system"); + // 代码逻辑说明: 【QQYUN-7168】邀请成员时,会报错,但实际已经邀请成功了--- + messageDTO.setCategory(CommonConstant.MSG_CATEGORY_1); + // 代码逻辑说明: 【QQYUN-5726】邀请加入租户加个按钮直接跳转过去------------ + messageDTO.setBusType(SysAnnmentTypeEnum.TENANT_INVITE.getType()); + sysBaseApi.sendBusAnnouncement(messageDTO); + } + + + @Override + public void passApply(SysTenantPackUser sysTenantPackUser) { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysTenantPackUser::getTenantId, sysTenantPackUser.getTenantId()) + .eq(SysTenantPackUser::getPackId, sysTenantPackUser.getPackId()) + .eq(SysTenantPackUser::getUserId, sysTenantPackUser.getUserId()); + SysTenantPackUser packUser = sysTenantPackUserMapper.selectOne(query); + if(packUser!=null && packUser.getId()!=null && packUser.getStatus()==0){ + ISysTenantService currentService = SpringContextUtils.getApplicationContext().getBean(ISysTenantService.class); + packUser.setPackName(sysTenantPackUser.getPackName()); + packUser.setRealname(sysTenantPackUser.getRealname()); + currentService.addTenantPackUser(packUser); + + //超级管理员成功加入发送系统消息 + SysTenant sysTenant = tenantMapper.selectById(sysTenantPackUser.getTenantId()); + String content = " 您已成功加入"+sysTenant.getName()+"的超级管理员的成员。"; + SysUser sysUser = userService.getById(sysTenantPackUser.getUserId()); + this.sendMsgForAgreeAndRefuseJoin(sysUser,content); + } + } + + @Override + public void deleteApply(SysTenantPackUser sysTenantPackUser) { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysTenantPackUser::getTenantId, sysTenantPackUser.getTenantId()) + .eq(SysTenantPackUser::getPackId, sysTenantPackUser.getPackId()) + .eq(SysTenantPackUser::getUserId, sysTenantPackUser.getUserId()); + SysTenantPackUser packUser = sysTenantPackUserMapper.selectOne(query); + if(packUser!=null && packUser.getId()!=null && packUser.getStatus()==0){ + sysTenantPackUserMapper.deleteById(packUser.getId()); + //超级管理员拒绝加入发送系统消息 + SysTenant sysTenant = tenantMapper.selectById(sysTenantPackUser.getTenantId()); + String content = " 管理员已拒绝您加入"+sysTenant.getName()+"的超级管理员的成员请求。"; + SysUser sysUser = userService.getById(sysTenantPackUser.getUserId()); + this.sendMsgForAgreeAndRefuseJoin(sysUser,content); + } + } + + @Override + public IPage queryTenantPackUserList(String tenantId, String packId,Integer status, Page page) { + // 查询用户 + List userList = baseMapper.queryTenantPackUserList(page,tenantId, packId,status); + // 获取产品包下用户部门和职位 + userList = getPackUserPositionAndDepart(userList); + return page.setRecords(userList); + } + + /** + * 获取用户职位和部门 + * @param userList + * @return + */ + private List getPackUserPositionAndDepart(List userList) { + if(userList!=null && userList.size()>0){ + List userIdList = userList.stream().map(i->i.getId()).collect(Collectors.toList()); + // 部门 + List depList = baseMapper.queryUserDepartList(userIdList); + // 职位 + List userPositions = baseMapper.queryUserPositionList(userIdList); + // 遍历用户 往用户中添加 部门信息和职位信息 + for (TenantPackUser user : userList) { + //添加部门 + for (UserDepart dep : depList) { + if (user.getId().equals(dep.getUserId())) { + user.addDepart(dep.getDepartName()); + } + } + //添加职位 + for (UserPosition userPosition : userPositions) { + if (user.getId().equals(userPosition.getUserId())) { + user.addPosition(userPosition.getPositionName()); + } + } + } + } + return userList; + } + + + /** + * 保存用户部门关系 + * @param userId + * @param departId + */ + private void saveUserDepart(String userId, String departId) { + //根据用户id和部门id获取数量,用于查看用户是否存在用户部门关系表中 + Long count = sysUserDepartMapper.getCountByDepartIdAndUserId(userId,departId); + if(count == 0){ + SysUserDepart sysUserDepart = new SysUserDepart(userId,departId); + sysUserDepartMapper.insert(sysUserDepart); + } + } + + @Override + public Long getApplySuperAdminCount() { + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + return baseMapper.getApplySuperAdminCount(sysUser.getId(),tenantId); + } + + /** + * 同意或拒绝加入超级管理员 发消息 + * @param user + * @param content + */ + public void sendMsgForAgreeAndRefuseJoin(SysUser user, String content){ + // 发消息 + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setToAll(false); + messageDTO.setToUser(user.getUsername()); + messageDTO.setFromUser("system"); + messageDTO.setTitle(content); + Map data = new HashMap<>(); + messageDTO.setData(data); + messageDTO.setContent(content); + messageDTO.setType("system"); + sysBaseApi.sendTemplateMessage(messageDTO); + } + + /** + * 获取租户错误提示信息 + * + * @param status + * @return + */ + private String getTenantErrorInfo(String status) { + String content = "已在租户中,无需邀请!"; + if (CommonConstant.USER_TENANT_QUIT.equals(status)) { + content = "已离职!"; + } else if (CommonConstant.USER_TENANT_UNDER_REVIEW.equals(status)) { + content = "租户管理员审核中!"; + } else if (CommonConstant.USER_TENANT_REFUSE.equals(status)) { + content = "租户管理员已拒绝!"; + } else if (CommonConstant.USER_TENANT_INVITE.equals(status)) { + content = "已被邀请,待用户同意!"; + } + return content; + } + + /** + * 删除租户下的产品包 + * + * @param tenantIdList + */ + private void deleteTenantPackByTenantId(List tenantIdList) { + //1.删除产品包下的用户 + sysTenantPackUserMapper.deletePackUserByTenantIds(tenantIdList); + //2.删除产品包对应的菜单权限 + sysPackPermissionMapper.deletePackPermByTenantIds(tenantIdList); + //3.删除产品包 + sysTenantPackMapper.deletePackByTenantIds(tenantIdList); + } + + @Override + public void deleteUserByPassword(SysUser sysUser, Integer tenantId) { + //被删除人的用户id + String userId = sysUser.getId(); + //被删除人的密码 + String password = sysUser.getPassword(); + //当前登录用户 + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //step1 判断当前用户是否为当前租户的管理员(只有超级管理员和账号管理员可以删除) + Long isHaveAdmin = sysTenantPackUserMapper.izHaveBuyAuth(user.getId(), tenantId); + if(null == isHaveAdmin || 0 == isHaveAdmin){ + throw new GhbBootException("您不是当前组织的管理员,无法删除用户!"); + } + //step2 离职状态下,并且无其他组织情况下,可以删除 + SysUserTenant sysUserTenant = userTenantMapper.getUserTenantByTenantId(userId, tenantId); + if(null == sysUserTenant || !CommonConstant.USER_TENANT_QUIT.equals(sysUserTenant.getStatus())){ + throw new GhbBootException("用户没有离职,不允许删除!"); + } + List tenantIdsByUserId = userTenantMapper.getTenantIdsByUserId(userId); + if(CollectionUtils.isNotEmpty(tenantIdsByUserId) && tenantIdsByUserId.size()>0){ + throw new GhbBootException("用户尚有未退出的组织,无法删除!"); + } + //step3 当天创建的用户和创建人可以删除 + SysUser sysUserData = userService.getById(userId); + if(!sysUserData.getCreateBy().equals(user.getUsername())){ + throw new GhbBootException("您不是该用户的创建人,无法删除!"); + } + + // 代码逻辑说明: 【QQYUN-11839】删除用户,需要输入被删除用户的密码,这逻辑对吗?不应该是管理员的密码吗--- + this.verifyCreateTimeAndPassword(sysUserData,password); + + //step5 逻辑删除用户 + userService.deleteUser(userId); + //step6 真实删除用户 + userService.removeLogicDeleted(Collections.singletonList(userId)); + } + + /** + * 验证创建时间和密码 + * + * @param sysUser + * @param password + */ + private void verifyCreateTimeAndPassword(SysUser sysUser,String password) { + if(null == sysUser){ + throw new GhbBootException("该用户不存在,无法删除!"); + } + //step1 验证创建时间 + //当前登录用户 + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + Date createTime = sysUser.getCreateTime(); + boolean sameDay = DateUtils.isSameDay(createTime, new Date()); + if(!sameDay){ + throw new GhbBootException("用户不是今天创建的,无法删除!"); + } + //step2 验证密码 + //获取admin的用户 + SysUser adminUser = userService.getById(user.getId()); + String passwordEncode = PasswordUtil.encrypt(adminUser.getUsername(), password, adminUser.getSalt()); + if(!passwordEncode.equals(adminUser.getPassword())){ + throw new GhbBootException("您输入的密码不正确,无法删除该用户!"); + } + } + + @Override + public List getTenantListByUserId(String userId) { + return tenantMapper.getTenantListByUserId(userId); + } + + @Override + public void deleteUser(SysUser sysUser, Integer tenantId) { + //被删除人的用户id + String userId = sysUser.getId(); + //被删除人的密码 + String password = sysUser.getPassword(); + //当前登录用户 + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //step1 判断当前用户是否为当前租户的创建者才可以删除 + SysTenant sysTenant = this.getById(tenantId); + if(null == sysTenant || !user.getUsername().equals(sysTenant.getCreateBy())){ + throw new GhbBootException("您不是当前组织的创建者,无法删除用户!"); + } + //step2 判断除了当前组织之外是否还有加入了其他组织 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getUserId,userId); + query.ne(SysUserTenant::getTenantId,tenantId); + List sysUserTenants = userTenantMapper.selectList(query); + if(CollectionUtils.isNotEmpty(sysUserTenants)){ + throw new GhbBootException("该用户还存在于其它组织中,无法删除用户!"); + } + //step3 验证创建时间和密码 + SysUser sysUserData = userService.getById(userId); + this.verifyCreateTimeAndPassword(sysUserData,password); + //step4 真实删除用户 + userService.deleteUser(userId); + userService.removeLogicDeleted(Collections.singletonList(userId)); + } + + /** + * 为用户添加租户下所有套餐 + * + * @param userId 用户id + * @param tenantId 租户id + */ + public void addPackUser(String userId, String tenantId) { + //根据租户id和产品包的code获取租户套餐id + List packIds = sysTenantPackMapper.getPackIdByPackCodeAndTenantId(oConvertUtils.getInt(tenantId)); + if (CollectionUtil.isNotEmpty(packIds)) { + for (String packId : packIds) { + SysTenantPackUser sysTenantPackUser = new SysTenantPackUser(); + sysTenantPackUser.setUserId(userId); + sysTenantPackUser.setTenantId(oConvertUtils.getInt(tenantId)); + sysTenantPackUser.setPackId(packId); + sysTenantPackUser.setStatus(CommonConstant.STATUS_1_INT); + try { + this.addTenantPackUser(sysTenantPackUser); + } catch (Exception e) { + log.warn("添加用户套餐包失败,原因:" + e.getMessage()); + } + } + } + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysThirdAccountServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysThirdAccountServiceImpl.java new file mode 100644 index 0000000..f91652f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysThirdAccountServiceImpl.java @@ -0,0 +1,241 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.jeecg.dingtalk.api.base.JdtBaseAPI; +import com.jeecg.dingtalk.api.core.response.Response; +import com.jeecg.dingtalk.api.core.vo.AccessToken; +import com.jeecg.dingtalk.api.user.JdtUserAPI; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.DateUtils; +import com.ghb.base.common.util.PasswordUtil; +import com.ghb.base.common.util.UUIDGenerator; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysRole; +import com.ghb.base.modules.system.entity.SysThirdAccount; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserRole; +import com.ghb.base.modules.system.mapper.SysRoleMapper; +import com.ghb.base.modules.system.mapper.SysThirdAccountMapper; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import com.ghb.base.modules.system.mapper.SysUserRoleMapper; +import com.ghb.base.modules.system.model.ThirdLoginModel; +import com.ghb.base.modules.system.service.ISysThirdAccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * @Description: 第三方登录账号表 + * @Author: Ghb-boot + * @Date: 2020-11-17 + * @Version: V1.0 + */ +@Service +@Slf4j +public class SysThirdAccountServiceImpl extends ServiceImpl implements ISysThirdAccountService { + + @Autowired + private SysThirdAccountMapper sysThirdAccountMapper; + + @Autowired + private SysUserMapper sysUserMapper; + @Autowired + private SysRoleMapper sysRoleMapper; + @Autowired + private SysUserRoleMapper sysUserRoleMapper; + + @Value("${justauth.type.DINGTALK.client-id:}") + private String dingTalkClientId; + @Value("${justauth.type.DINGTALK.client-secret:}") + private String dingTalkClientSecret; + + @Override + public void updateThirdUserId(SysUser sysUser,String thirdUserUuid) { + //修改第三方登录账户表使其进行添加用户id + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysThirdAccount::getThirdUserUuid,thirdUserUuid); + //扫码登录更新用户创建的时候存的是默认租户,更新的时候也需要根据默认租户来查询,同一个公司下UUID是一样的,不同应用需要区分租户。 + query.eq(SysThirdAccount::getTenantId,CommonConstant.TENANT_ID_DEFAULT_VALUE); + SysThirdAccount account = sysThirdAccountMapper.selectOne(query); + SysThirdAccount sysThirdAccount = new SysThirdAccount(); + sysThirdAccount.setSysUserId(sysUser.getId()); + //根据当前用户id和登录方式查询第三方登录表 + LambdaQueryWrapper thirdQuery = new LambdaQueryWrapper<>(); + thirdQuery.eq(SysThirdAccount::getSysUserId,sysUser.getId()); + thirdQuery.eq(SysThirdAccount::getThirdType,account.getThirdType()); + thirdQuery.eq(SysThirdAccount::getThirdUserUuid,thirdUserUuid); + thirdQuery.eq(SysThirdAccount::getTenantId,CommonConstant.TENANT_ID_DEFAULT_VALUE); + SysThirdAccount sysThirdAccounts = sysThirdAccountMapper.selectOne(thirdQuery); + if(sysThirdAccounts!=null){ + sysThirdAccount.setThirdUserId(sysThirdAccounts.getThirdUserId()); + sysThirdAccountMapper.deleteById(sysThirdAccounts.getId()); + } + //更新用户账户表sys_user_id + sysThirdAccountMapper.update(sysThirdAccount,query); + } + + @Override + public SysUser createUser(String phone, String thirdUserUuid, Integer tenantId) { + //先查询第三方,获取登录方式 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysThirdAccount::getThirdUserUuid,thirdUserUuid); + query.eq(SysThirdAccount::getTenantId,tenantId); + SysThirdAccount account = sysThirdAccountMapper.selectOne(query); + //通过用户名查询数据库是否已存在 + SysUser userByName = sysUserMapper.getUserByName(thirdUserUuid); + if(null!=userByName){ + //如果账号存在的话,则自动加上一个时间戳 + String format = DateUtils.yyyymmddhhmmss.get().format(new Date()); + thirdUserUuid = thirdUserUuid + format; + } + //添加用户 + SysUser user = new SysUser(); + user.setActivitiSync(CommonConstant.ACT_SYNC_1); + user.setDelFlag(CommonConstant.DEL_FLAG_0); + user.setStatus(1); + user.setUsername(thirdUserUuid); + user.setPhone(phone); + //设置初始密码 + String salt = oConvertUtils.randomGen(8); + user.setSalt(salt); + String passwordEncode = PasswordUtil.encrypt(user.getUsername(), "123456", salt); + user.setPassword(passwordEncode); + user.setRealname(account.getRealname()); + user.setAvatar(account.getAvatar()); + String s = this.saveThirdUser(user); + //更新用户第三方账户表的userId + SysThirdAccount sysThirdAccount = new SysThirdAccount(); + sysThirdAccount.setSysUserId(s); + sysThirdAccount.setTenantId(tenantId); + sysThirdAccountMapper.update(sysThirdAccount,query); + return user; + } + + public String saveThirdUser(SysUser sysUser) { + //保存用户 + String userid = UUIDGenerator.generate(); + sysUser.setId(userid); + sysUserMapper.insert(sysUser); + //获取第三方角色 + SysRole sysRole = sysRoleMapper.selectOne(new LambdaQueryWrapper().eq(SysRole::getRoleCode, "third_role")); + //保存用户角色 + SysUserRole userRole = new SysUserRole(); + userRole.setRoleId(sysRole.getId()); + userRole.setUserId(userid); + sysUserRoleMapper.insert(userRole); + return userid; + } + + @Override + public SysThirdAccount getOneBySysUserId(String sysUserId, String thirdType) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + log.info("getSysUserId: {} ,getThirdType: {}",sysUserId,thirdType); + queryWrapper.eq(SysThirdAccount::getSysUserId, sysUserId); + queryWrapper.eq(SysThirdAccount::getThirdType, thirdType); + return super.getOne(queryWrapper); + } + + @Override + public SysThirdAccount getOneByThirdUserId(String thirdUserId, String thirdType) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAccount::getThirdUserId, thirdUserId); + queryWrapper.eq(SysThirdAccount::getThirdType, thirdType); + return super.getOne(queryWrapper); + } + + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + @Override + public List listBySysUserIds(List sysUserIds, String thirdType) { + if (sysUserIds == null || sysUserIds.isEmpty()) { + return Collections.emptyList(); + } + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.in(SysThirdAccount::getSysUserId, sysUserIds); + qw.eq(SysThirdAccount::getThirdType, thirdType); + return list(qw); + } + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + + @Override + public List listThirdUserIdByUsername(String[] sysUsernameArr, String thirdType, Integer tenantId) { + return sysThirdAccountMapper.selectThirdIdsByUsername(sysUsernameArr, thirdType,tenantId); + } + + @Override + public SysThirdAccount saveThirdUser(ThirdLoginModel tlm, Integer tenantId) { + SysThirdAccount user = new SysThirdAccount(); + user.setDelFlag(CommonConstant.DEL_FLAG_0); + user.setStatus(1); + user.setThirdType(tlm.getSource()); + user.setAvatar(tlm.getAvatar()); + user.setRealname(tlm.getUsername()); + user.setThirdUserUuid(tlm.getUuid()); + user.setTenantId(tenantId); + //=============begin 判断如果是钉钉的情况下,需要将第三方的用户id查询出来,发送模板的时候有用========== + if(CommonConstant.DINGTALK.toLowerCase().equals(tlm.getSource())){ + AccessToken accessToken = JdtBaseAPI.getAccessToken(dingTalkClientId, dingTalkClientSecret); + Response getUserIdRes = JdtUserAPI.getUseridByUnionid(tlm.getUuid(), accessToken.getAccessToken()); + if (getUserIdRes.isSuccess()) { + user.setThirdUserId(getUserIdRes.getResult()); + }else{ + user.setThirdUserId(tlm.getUuid()); + } + //=============end 判断如果是钉钉的情况下,需要将第三方的用户id查询出来,发送模板的时候有用========== + }else{ + user.setThirdUserId(tlm.getUuid()); + } + super.save(user); + return user; + } + + @Override + public SysThirdAccount bindThirdAppAccountByUserId(SysThirdAccount sysThirdAccount) { + String thirdUserUuid = sysThirdAccount.getThirdUserUuid(); + String thirdType = sysThirdAccount.getThirdType(); + //获取当前登录用户 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + //当前第三方用户已被其他用户所绑定 + SysThirdAccount oneByThirdUserId = this.getOneByUuidAndThirdType(thirdUserUuid, thirdType,CommonConstant.TENANT_ID_DEFAULT_VALUE, null); + if(null != oneByThirdUserId){ + //如果不为空,并且第三方表和当前登录的用户一致,直接返回 + if(oConvertUtils.isNotEmpty(oneByThirdUserId.getSysUserId()) && oneByThirdUserId.getSysUserId().equals(sysUser.getId())){ + return oneByThirdUserId; + }else if(oConvertUtils.isNotEmpty(oneByThirdUserId.getSysUserId())){ + //如果第三方表的用户id不为空,那就说明已经绑定过了 + throw new GhbBootException("该敲敲云账号已被其它第三方账号绑定,请解绑或绑定其它敲敲云账号"); + }else{ + //更新第三方表信息用户id + oneByThirdUserId.setSysUserId(sysUser.getId()); + oneByThirdUserId.setThirdType(thirdType); + sysThirdAccountMapper.updateById(oneByThirdUserId); + return oneByThirdUserId; + } + }else{ + throw new GhbBootException("账号绑定失败,请稍后重试"); + } + } + + @Override + public SysThirdAccount getOneByUuidAndThirdType(String unionid, String thirdType,Integer tenantId,String thirdUserId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAccount::getThirdType, thirdType); + // 代码逻辑说明: 如果第三方用户id为空那么就不走第三方用户查询逻辑,因为扫码登录third_user_id是唯一的,没有重复的情况--- + if(oConvertUtils.isNotEmpty(thirdUserId)){ + queryWrapper.and((wrapper) ->wrapper.eq(SysThirdAccount::getThirdUserUuid,unionid).or().eq(SysThirdAccount::getThirdUserId,thirdUserId)); + }else{ + queryWrapper.eq(SysThirdAccount::getThirdUserUuid, unionid); + } + queryWrapper.eq(SysThirdAccount::getTenantId, tenantId); + return super.getOne(queryWrapper); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysThirdAppConfigServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysThirdAppConfigServiceImpl.java new file mode 100644 index 0000000..ac462b5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysThirdAppConfigServiceImpl.java @@ -0,0 +1,44 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.modules.system.entity.SysThirdAppConfig; +import com.ghb.base.modules.system.mapper.SysThirdAppConfigMapper; +import com.ghb.base.modules.system.service.ISysThirdAppConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @Description: 第三方配置表 + * @Author: Ghb-boot + * @Date: 2023-02-03 + * @Version: V1.0 + */ +@Service +@Slf4j +public class SysThirdAppConfigServiceImpl extends ServiceImpl implements ISysThirdAppConfigService { + + @Autowired + private SysThirdAppConfigMapper configMapper; + + @Override + public List getThirdConfigListByThirdType(int tenantId) { + return configMapper.getThirdConfigListByThirdType(tenantId); + } + + @Override + public SysThirdAppConfig getThirdConfigByThirdType(Integer tenantId, String thirdType) { + return configMapper.getThirdConfigByThirdType(tenantId,thirdType); + } + + @Override + public List getThirdAppConfigByClientId(String clientId) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysThirdAppConfig::getClientId,clientId); + List sysThirdAppConfigs = configMapper.selectList(query); + return sysThirdAppConfigs; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUgroupServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUgroupServiceImpl.java new file mode 100644 index 0000000..7e0d0f5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUgroupServiceImpl.java @@ -0,0 +1,41 @@ +package com.ghb.base.modules.system.service.impl; + +import com.ghb.base.modules.system.entity.SysUgroup; +import com.ghb.base.modules.system.entity.SysUgroupUser; +import com.ghb.base.modules.system.mapper.SysUgroupMapper; +import com.ghb.base.modules.system.service.ISysUgroupService; +import com.ghb.base.modules.system.service.ISysUgroupUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + +import java.util.List; + +/** + * @Description: 用户组表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +@Service("sysUgroupServiceImpl") +public class SysUgroupServiceImpl extends ServiceImpl implements ISysUgroupService { + + @Autowired + private ISysUgroupUserService sysUgroupUserService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteById(String id) { + this.baseMapper.deleteById(id); + sysUgroupUserService.remove(new QueryWrapper().eq("group_id", id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByIds(List list) { + this.baseMapper.deleteBatchIds(list); + sysUgroupUserService.remove(new QueryWrapper().in("group_id", list)); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUgroupUserServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUgroupUserServiceImpl.java new file mode 100644 index 0000000..0e517bd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUgroupUserServiceImpl.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.system.service.impl; + +import com.ghb.base.modules.system.entity.SysUgroupUser; +import com.ghb.base.modules.system.mapper.SysUgroupUserMapper; +import com.ghb.base.modules.system.service.ISysUgroupUserService; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: 用户组关系表 + * @Author: Ghb-boot + * @Date: 2026-02-27 + * @Version: V1.0 + */ +@Service("sysUgroupUserServiceImpl") +public class SysUgroupUserServiceImpl extends ServiceImpl implements ISysUgroupUserService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserDepPostServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserDepPostServiceImpl.java new file mode 100644 index 0000000..bc3924f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserDepPostServiceImpl.java @@ -0,0 +1,16 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.modules.system.entity.SysUserDepPost; +import com.ghb.base.modules.system.mapper.SysUserDepPostMapper; +import com.ghb.base.modules.system.service.ISysUserDepPostService; +import org.springframework.stereotype.Service; + +/** + * @Description: 部门岗位用户实现类 + * @author: wangshuai + * @date: 2025/9/5 11:46 + */ +@Service +public class SysUserDepPostServiceImpl extends ServiceImpl implements ISysUserDepPostService { +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserDepartServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserDepartServiceImpl.java new file mode 100644 index 0000000..b0b9e91 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserDepartServiceImpl.java @@ -0,0 +1,456 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.DepartCategoryEnum; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserDepart; +import com.ghb.base.modules.system.mapper.SysUserDepartMapper; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import com.ghb.base.modules.system.mapper.SysUserTenantMapper; +import com.ghb.base.modules.system.model.DepartIdModel; +import com.ghb.base.modules.system.service.ISysDepartService; +import com.ghb.base.modules.system.service.ISysUserDepartService; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.vo.SysUserDepVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 用户部门表实现类 + *

+ * @Author ZhiLin + *@since 2019-02-22 + */ +@Service +public class SysUserDepartServiceImpl extends ServiceImpl implements ISysUserDepartService { + @Autowired + private ISysDepartService sysDepartService; + @Lazy + @Autowired + private ISysUserService sysUserService; + @Autowired + private SysUserMapper sysUserMapper; + @Autowired + private SysUserTenantMapper userTenantMapper; + + + /** + * 根据用户id查询部门信息 + */ + @Override + public List queryDepartIdsOfUser(String userId) { + LambdaQueryWrapper queryUserDep = new LambdaQueryWrapper(); + LambdaQueryWrapper queryDep = new LambdaQueryWrapper(); + try { + queryUserDep.eq(SysUserDepart::getUserId, userId); + List depIdList = new ArrayList<>(); + List depIdModelList = new ArrayList<>(); + List userDepList = this.list(queryUserDep); + if(userDepList != null && userDepList.size() > 0) { + for(SysUserDepart userDepart : userDepList) { + depIdList.add(userDepart.getDepId()); + } + + // 代码逻辑说明: 判断是否开启租户saas模式,开启需要根据当前租户查询------------ + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + queryDep.eq(SysDepart::getTenantId,tenantId); + } + + queryDep.in(SysDepart::getId, depIdList); + List depList = sysDepartService.list(queryDep); + //Ghb-boot/issues/3906 + if(depList != null && depList.size() > 0) { + for(SysDepart depart : depList) { + depIdModelList.add(new DepartIdModel().convertByUserDepart(depart)); + } + } + return depIdModelList; + } + }catch(Exception e) { + e.fillInStackTrace(); + } + return null; + + + } + + + /** + * 根据部门id查询用户信息 + */ + @Override + public List queryUserByDepId(String depId) { + LambdaQueryWrapper queryUserDep = new LambdaQueryWrapper(); + queryUserDep.eq(SysUserDepart::getDepId, depId); + List userIdList = new ArrayList<>(); + List uDepList = this.list(queryUserDep); + if(uDepList != null && uDepList.size() > 0) { + for(SysUserDepart uDep : uDepList) { + userIdList.add(uDep.getUserId()); + } + List userList = (List) sysUserMapper.selectBatchIds(userIdList); + if(CollectionUtil.isNotEmpty(userList)){ + + // 代码逻辑说明: JHHB-812 人员按照排序展示 + userList.sort(Comparator.comparing(SysUser::getSort, + Comparator.nullsFirst(Comparator.naturalOrder()))); + + // 代码逻辑说明: 接口调用查询返回结果不能返回密码相关信息 + for (SysUser sysUser : userList) { + sysUser.setSalt(""); + sysUser.setPassword(""); + } + } + return userList; + } + return new ArrayList(); + } + + /** + * 根据部门code,查询当前部门和下级部门的 用户信息 + */ + @Override + public List queryUserByDepCode(String depCode,String realname) { + // 代码逻辑说明: 根据部门选择用户接口代码优化 + if(oConvertUtils.isNotEmpty(realname)){ + realname = realname.trim(); + } + List userList = this.baseMapper.queryDepartUserList(depCode, realname); + Map map = new LinkedHashMap(5); + for (SysUser sysUser : userList) { + // 返回的用户数据去掉密码信息 + sysUser.setSalt(""); + sysUser.setPassword(""); + map.put(sysUser.getId(), sysUser); + } + return new ArrayList(map.values()); + + } + + /** + * + * @param departId + * @param username + * @param realname + * @param pageSize + * @param pageNo + * @param id + * @param isMultiTranslate 是否多字段翻译 + * @return + */ + @Override + public IPage queryDepartUserPageList(String departId, String username, String realname, int pageSize, int pageNo,String id,String isMultiTranslate) { + IPage pageList = null; + // 部门ID不存在 直接查询用户表即可 + Page page = new Page(pageNo, pageSize); + if(oConvertUtils.isEmpty(departId)){ + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + // 代码逻辑说明: [JTC-297]已冻结用户仍可设置为代理人------------ + query.eq(SysUser::getStatus,Integer.parseInt(CommonConstant.STATUS_1)); + // 代码逻辑说明: 逗号分割多个用户翻译问题------------ + if(oConvertUtils.isNotEmpty(username)){ + String COMMA = ","; + if(oConvertUtils.isNotEmpty(isMultiTranslate) && username.contains(COMMA)){ + String[] usernameArr = username.split(COMMA); + query.in(SysUser::getUsername,usernameArr); + }else { + query.like(SysUser::getUsername, username); + } + } + + // 代码逻辑说明: JHHB-304 流程转办 人员选择时,加姓名搜索------------ + if(oConvertUtils.isNotEmpty(realname)){ + String COMMA = ","; + if(oConvertUtils.isNotEmpty(isMultiTranslate) && realname.contains(COMMA)){ + String[] realnameArr = realname.split(COMMA); + query.in(SysUser::getRealname,realnameArr); + }else { + query.like(SysUser::getRealname, realname); + } + } + + // 代码逻辑说明: [VUEN-1238]邮箱回复时,发送到显示的为用户id------------ + if(oConvertUtils.isNotEmpty(id)){ + // 代码逻辑说明: 【TV360X-1482】写信,选择用户后第一次回显没翻译------------ + String COMMA = ","; + if(oConvertUtils.isNotEmpty(isMultiTranslate) && id.contains(COMMA)){ + String[] idArr = id.split(COMMA); + query.in(SysUser::getId, Arrays.asList(idArr)); + }else { + query.eq(SysUser::getId, id); + } + } + // 代码逻辑说明: [VUEN-2121]临时用户不能直接显示------------ + query.ne(SysUser::getUsername,"_reserve_user_external"); + // 代码逻辑说明: 【JHHB-765】需要能设置排序--- + query.orderByAsc(SysUser::getSort); + query.orderByDesc(SysUser::getCreateTime); + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + String tenantId = oConvertUtils.getString(TenantContext.getTenant(), "0"); + // 代码逻辑说明: [QQYUN-3371]租户逻辑改造,改成关系表------------ + List userIdList = userTenantMapper.getUserIdsByTenantId(Integer.valueOf(tenantId)); + if(null!=userIdList && userIdList.size()>0){ + query.in(SysUser::getId,userIdList); + } + } + //------------------------------------------------------------------------------------------------ + pageList = sysUserMapper.selectPage(page, query); + }else{ + // 有部门ID 需要走自定义sql + SysDepart sysDepart = sysDepartService.getById(departId); + pageList = this.baseMapper.queryDepartUserPageList(page, sysDepart.getOrgCode(), username, realname); + } + List userList = pageList.getRecords(); + if(userList!=null && userList.size()>0){ + List userIds = userList.stream().map(SysUser::getId).collect(Collectors.toList()); + Map map = new LinkedHashMap(5); + if(userIds!=null && userIds.size()>0){ + // 查部门名称 + Map useDepNames = this.getDepNamesByUserIds(userIds); + userList.forEach(item->{ + //TODO 临时借用这个字段用于页面展示 + item.setOrgCodeTxt(useDepNames.get(item.getId())); + item.setSalt(""); + item.setPassword(""); + // 去重 + map.put(item.getId(), item); + }); + } + pageList.setRecords(new ArrayList(map.values())); + } + return pageList; + } + + @Override + public IPage getUserInformation(Integer tenantId, String departId, String keyword, Integer pageSize, Integer pageNo) { + IPage pageList = null; + // 部门ID不存在 直接查询用户表即可 + Page page = new Page<>(pageNo, pageSize); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + if(oConvertUtils.isEmpty(departId)){ + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUser::getStatus,Integer.parseInt(CommonConstant.STATUS_1)); + query.ne(SysUser::getUsername,"_reserve_user_external"); + + // 支持租户隔离 + if (tenantId != null) { + List userIds = userTenantMapper.getUserIdsByTenantId(tenantId); + if(oConvertUtils.listIsNotEmpty(userIds)){ + query.in(SysUser::getId, userIds); + }else{ + query.eq(SysUser::getId,"通过租户ID查不到用户"); + } + } + + //排除自己 + query.ne(SysUser::getId,sysUser.getId()); + if(StringUtils.isNotEmpty(keyword)){ + //这个语法可以将or用括号包起来,避免数据查不到 + query.and((wrapper) -> wrapper.like(SysUser::getUsername, keyword).or().like(SysUser::getRealname,keyword)); + } + pageList = sysUserMapper.selectPage(page, query); + }else{ + // 有部门ID 需要走自定义sql + SysDepart sysDepart = sysDepartService.getById(departId); + // 代码逻辑说明: 部门排除自己------------ + pageList = this.baseMapper.getUserInformation(page, sysDepart.getOrgCode(), keyword,sysUser.getId()); + } + return pageList; + } + + @Override + public IPage getUserInformation(Integer tenantId, String departId,String roleId, String keyword, Integer pageSize, Integer pageNo, String excludeUserIdList, String includeUsernameList) { + IPage pageList = null; + // 部门ID不存在 直接查询用户表即可 + Page page = new Page<>(pageNo, pageSize); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + List userIdList = new ArrayList<>(); + List inUsernameList = new ArrayList<>(); + if(oConvertUtils.isNotEmpty(excludeUserIdList)){ + userIdList = Arrays.asList(excludeUserIdList.split(SymbolConstant.COMMA)); + } + if(oConvertUtils.isNotEmpty(includeUsernameList)){ + inUsernameList = Arrays.asList(includeUsernameList.split(SymbolConstant.COMMA)); + } + if(oConvertUtils.isNotEmpty(departId)){ + // 有部门ID 需要走自定义sql + SysDepart sysDepart = sysDepartService.getById(departId); + // 代码逻辑说明: 【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页--- + pageList = this.baseMapper.getProcessUserList(page, sysDepart.getOrgCode(), keyword, tenantId, userIdList); + } else if (oConvertUtils.isNotEmpty(roleId)) { + // 代码逻辑说明: 【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页--- + pageList = this.sysUserMapper.selectUserListByRoleId(page, roleId, keyword, tenantId,userIdList); + } else{ + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUser::getStatus,Integer.parseInt(CommonConstant.STATUS_1)); + query.ne(SysUser::getUsername,"_reserve_user_external"); + if(inUsernameList!=null && inUsernameList.size()>0){ + query.in(SysUser::getUsername, inUsernameList); + } + + // 代码逻辑说明: 【QQYUN-8239】用户角色,添加用户 返回2页数据,实际只显示一页--- + if(oConvertUtils.isNotEmpty(excludeUserIdList)){ + query.notIn(SysUser::getId,Arrays.asList(excludeUserIdList.split(SymbolConstant.COMMA))); + } + // 支持租户隔离 + if (tenantId != null) { + List userIds = userTenantMapper.getUserIdsByTenantId(tenantId); + if(oConvertUtils.listIsNotEmpty(userIds)){ + query.in(SysUser::getId, userIds); + }else{ + query.eq(SysUser::getId,"通过租户ID查不到用户"); + } + } + + if(StringUtils.isNotEmpty(keyword)){ + //这个语法可以将or用括号包起来,避免数据查不到 + query.and((wrapper) -> wrapper.like(SysUser::getUsername, keyword).or().like(SysUser::getRealname,keyword)); + } + + // 【JHHB-811】添加排序 + query.orderByAsc(SysUser::getSort).orderByDesc(SysUser::getCreateTime); + + pageList = sysUserMapper.selectPage(page, query); + } + // 批量查询用户的所属部门 + // step.1 先拿到全部的 useids + // step.2 通过 useids,一次性查询用户的所属部门名字 + List userIds = pageList.getRecords().stream().map(SysUser::getId).collect(Collectors.toList()); + if (userIds.size() > 0) { + Map useDepNames = sysUserService.getDepNamesByUserIds(userIds); + pageList.getRecords().forEach(item -> item.setOrgCodeTxt(useDepNames.get(item.getId()))); + } + return pageList; + } + + @Override + public List getUsersByDepartTenantId(String departId, Integer tenantId) { + return baseMapper.getUsersByDepartTenantId(departId,tenantId); + } + + /** + * 升级SpringBoot2.6.6,不允许循环依赖 + * @param userIds + * @return + */ + private Map getDepNamesByUserIds(List userIds) { + List list = sysUserMapper.getDepNamesByUserIds(userIds); + + Map res = new HashMap(5); + list.forEach(item -> { + if (res.get(item.getUserId()) == null) { + res.put(item.getUserId(), item.getDepartName()); + } else { + res.put(item.getUserId(), res.get(item.getUserId()) + "," + item.getDepartName()); + } + } + ); + return res; + } + + + /** + * 查询部门岗位下的用户 + * @param departId + * @param username + * @param realname + * @param pageSize + * @param pageNo + * @param id + * @param isMultiTranslate + * @return + */ + @Override + public IPage queryDepartPostUserPageList(String departId, String username, String realname, Integer pageSize, Integer pageNo, String id, String isMultiTranslate) { + Page page = new Page(pageNo, pageSize); + if (oConvertUtils.isEmpty(departId)) { + // 部门ID不存在 直接查询用户表即可 + return getDepPostListByIdUserName(username,id,isMultiTranslate,page); + } else { + // 有部门ID 需要走部门岗位用户查询 + return getDepartPostListByIdUserRealName(departId,username,realname,page); + } + } + + /** + * 根据部门id和用户名获取部门岗位用户分页列表 + * + * @param id + * @param username + * @param isMultiTranslate + * @param page + * @return + */ + private IPage getDepPostListByIdUserName(String username, String id, String isMultiTranslate, Page page) { + //需要查询部门下的用户,故将写成自定义sql,非Lambda表达式的用法 + List userIdList = new ArrayList<>(); + List userNameList = new ArrayList<>(); + String userId = ""; + String userName = ""; + if (oConvertUtils.isNotEmpty(username)) { + String COMMA = ","; + if (oConvertUtils.isNotEmpty(isMultiTranslate) && username.contains(COMMA)) { + String[] usernameArr = username.split(COMMA); + userNameList.addAll(Arrays.asList(usernameArr)); + } else { + userName = username; + } + } + if (oConvertUtils.isNotEmpty(id)) { + String COMMA = ","; + if (oConvertUtils.isNotEmpty(isMultiTranslate) && id.contains(COMMA)) { + String[] idArr = id.split(COMMA); + userIdList.addAll(Arrays.asList(idArr)); + } else { + userId = ""; + } + } + //------------------------------------------------------------------------------------------------ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + String tenantId = oConvertUtils.getString(TenantContext.getTenant(), "0"); + List userIdsList = userTenantMapper.getUserIdsByTenantId(Integer.valueOf(tenantId)); + if (null != userIdsList && !userIdsList.isEmpty()) { + userIdList.addAll(userIdsList); + } + } + //------------------------------------------------------------------------------------------------ + return sysUserMapper.getDepPostListByIdUserName(page,userIdList,userId,userName,userNameList); + } + + /** + * 根据部门id、用户名和真实姓名获取部门岗位用户分页列表 + * + * @param departId + * @param username + * @param realname + * @param page + * @return + */ + private IPage getDepartPostListByIdUserRealName(String departId, String username, String realname, Page page) { + SysDepart sysDepart = sysDepartService.getById(departId); + return sysUserMapper.getDepartPostListByIdUserRealName(page, username, realname, sysDepart.getOrgCode()); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserPositionServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserPositionServiceImpl.java new file mode 100644 index 0000000..1c05732 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserPositionServiceImpl.java @@ -0,0 +1,81 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysPosition; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserPosition; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import com.ghb.base.modules.system.mapper.SysUserPositionMapper; +import com.ghb.base.modules.system.service.ISysUserPositionService; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: 用户职位关系表 + * @Author: Ghb-boot + * @Date: 2023-02-14 + * @Version: V1.0 + */ +@Service +public class SysUserPositionServiceImpl extends ServiceImpl implements ISysUserPositionService { + + @Autowired + private SysUserPositionMapper sysUserPositionMapper; + + @Autowired + private SysUserMapper userMapper; + + @Override + public IPage getPositionUserList(Page page, String positionId) { + return page.setRecords(sysUserPositionMapper.getPositionUserList(page, positionId)); + } + + @Override + public void saveUserPosition(String userIds, String positionId) { + String[] userIdArray = userIds.split(SymbolConstant.COMMA); + //存在的用户 + StringBuilder userBuilder = new StringBuilder(); + for (String userId : userIdArray) { + //获取成员是否存在于职位中 + Long count = sysUserPositionMapper.getUserPositionCount(userId, positionId); + if (count == 0) { + //插入到用户职位关系表里面 + SysUserPosition userPosition = new SysUserPosition(); + userPosition.setPositionId(positionId); + userPosition.setUserId(userId); + sysUserPositionMapper.insert(userPosition); + } else { + userBuilder.append(userId).append(SymbolConstant.COMMA); + } + } + //如果用户id存在,说明已存在用户职位关系表中,提示用户已存在 + String uIds = userBuilder.toString(); + if (oConvertUtils.isNotEmpty(uIds)) { + //查询用户列表 + List sysUsers = userMapper.selectBatchIds(Arrays.asList(uIds.split(SymbolConstant.COMMA))); + String realnames = sysUsers.stream().map(SysUser::getRealname).collect(Collectors.joining(SymbolConstant.COMMA)); + throw new GhbBootException(realnames + "已存在该职位中"); + } + } + + @Override + public void removeByPositionId(String positionId) { + sysUserPositionMapper.removeByPositionId(positionId); + } + + @Override + public void removePositionUser(String userIds, String positionId) { + String[] userIdArray = userIds.split(SymbolConstant.COMMA); + sysUserPositionMapper.removePositionUser(Arrays.asList(userIdArray),positionId); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserRoleServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserRoleServiceImpl.java new file mode 100644 index 0000000..dc0e6ab --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserRoleServiceImpl.java @@ -0,0 +1,30 @@ +package com.ghb.base.modules.system.service.impl; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import com.ghb.base.modules.system.entity.SysRole; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserRole; +import com.ghb.base.modules.system.mapper.SysUserRoleMapper; +import com.ghb.base.modules.system.service.ISysRoleService; +import com.ghb.base.modules.system.service.ISysUserRoleService; +import com.ghb.base.modules.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + *

+ * 用户角色表 服务实现类 + *

+ * + * @Author scott + * @since 2018-12-21 + */ +@Service +public class SysUserRoleServiceImpl extends ServiceImpl implements ISysUserRoleService { + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..8ba7fe0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,3135 @@ +package com.ghb.base.modules.system.service.impl; +import org.jeecg.common.util.RedisUtil; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.RandomUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.aliyuncs.exceptions.ClientException; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.UnavailableSecurityManagerException; +import com.ghb.base.common.api.dto.message.MessageDTO; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.config.TenantContext; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.FillRuleConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.PasswordConstant; +import com.ghb.base.common.constant.enums.*; +import com.ghb.base.common.desensitization.annotation.SensitiveEncode; +import com.ghb.base.common.exception.GhbBootBizTipException; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.system.vo.SysUserCacheInfo; +import com.ghb.base.common.util.*; +import com.ghb.base.common.util.encryption.AesEncryptUtil; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.base.service.BaseCommonService; +import com.ghb.base.modules.message.handle.impl.SystemSendMsgHandle; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.mapper.*; +import com.ghb.base.modules.system.model.SysLoginModel; +import com.ghb.base.modules.system.model.SysUserSysDepPostModel; +import com.ghb.base.modules.system.model.SysUserSysDepartModel; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.util.ImportSysUserCache; +import com.ghb.base.modules.system.vo.*; +import com.ghb.base.modules.system.vo.lowapp.AppExportUserVo; +import com.ghb.base.modules.system.vo.lowapp.DepartAndUserInfo; +import com.ghb.base.modules.system.vo.lowapp.DepartInfo; +import com.ghb.base.modules.system.vo.lowapp.UpdateDepartInfo; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 用户表 服务实现类 + *

+ * + * @Author: scott + * @Date: 2018-12-20 + */ +@Service +@Slf4j +public class SysUserServiceImpl extends ServiceImpl implements ISysUserService { + + @Autowired + private SysUserMapper userMapper; + @Autowired + private SysPermissionMapper sysPermissionMapper; + @Autowired + private SysUserRoleMapper sysUserRoleMapper; + @Autowired + private SysUserDepartMapper sysUserDepartMapper; + @Autowired + private SysDepartMapper sysDepartMapper; + @Autowired + private SysRoleMapper sysRoleMapper; + @Autowired + private SysDepartRoleUserMapper departRoleUserMapper; + @Autowired + private SysDepartRoleMapper sysDepartRoleMapper; + @Resource + private BaseCommonService baseCommonService; + @Autowired + private SysThirdAccountMapper sysThirdAccountMapper; + @Autowired + ThirdAppWechatEnterpriseServiceImpl wechatEnterpriseService; + @Autowired + ThirdAppDingtalkServiceImpl dingtalkService; + @Autowired + ISysRoleIndexService sysRoleIndexService; + @Autowired + SysTenantMapper sysTenantMapper; + @Autowired + private SysUserTenantMapper relationMapper; + @Autowired + private SysUserTenantMapper userTenantMapper; + @Autowired + private SysUserPositionMapper sysUserPositionMapper; + @Autowired + private SysPositionMapper sysPositionMapper; + @Autowired + private SystemSendMsgHandle systemSendMsgHandle; + + @Autowired + private ISysThirdAccountService sysThirdAccountService; + @Autowired + private RedisUtil redisUtil; + + @Autowired + private SysTenantPackUserMapper packUserMapper; + + @Autowired + private SysUserDepPostMapper depPostMapper; + + @Autowired + private GhbBaseConfig GhbBaseConfig; + + /** + * 管理员账号 + */ + public static final String[] ADMIN_ACCOUNT = new String[]{"admin"}; + + @Override + public Result> queryPageList(HttpServletRequest req, QueryWrapper queryWrapper, Integer pageSize, Integer pageNo) { + Result> result = new Result>(); + //部门ID + String departId = req.getParameter("departId"); + if (oConvertUtils.isNotEmpty(departId)) { + //代码逻辑说明:【JHHB-762】用户管理需要支持按组织架构查询用户(支持多选)--- + //兼容多个部门id + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + if(departId.contains(SymbolConstant.COMMA)) { + query.in(SysUserDepart::getDepId, Arrays.asList(departId.split(SymbolConstant.COMMA))); + } else { + query.eq(SysUserDepart::getDepId, departId); + } + List list = sysUserDepartMapper.selectList(query); + List userIds = list.stream().map(SysUserDepart::getUserId).collect(Collectors.toList()); + // 代码逻辑说明: [issues/I4XTYB]查询用户时,当部门id 下没有分配用户时接口报错------------ + if (oConvertUtils.listIsNotEmpty(userIds)) { + queryWrapper.in("id", userIds); + } else { + return Result.OK(); + } + } + //用户ID + String code = req.getParameter("code"); + if (oConvertUtils.isNotEmpty(code)) { + queryWrapper.in("id", Arrays.asList(code.split(","))); + pageSize = code.split(",").length; + } + + // 代码逻辑说明: JTC-372 【用户冻结问题】 online授权、用户组件,选择用户都能看到被冻结的用户 + String status = req.getParameter("status"); + if (oConvertUtils.isNotEmpty(status)) { + queryWrapper.eq("status", Integer.parseInt(status)); + } + + // 代码逻辑说明: 【QQYUN-8110】在线通讯录支持设置权限(只能看分配的技术支持)--- + String tenantId = TokenUtils.getTenantIdByRequest(req); + String lowAppId = TokenUtils.getLowAppIdByRequest(req); +// Object bean = ResourceUtil.getImplementationClass(DataEnhanceEnum.getClassPath(tenantId,lowAppId)); +// if(null != bean){ +// UserFilterEnhance userEnhanceService = (UserFilterEnhance) bean; +// LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); +// List userIds = userEnhanceService.getUserIds(sysUser.getId()); +// if(CollectionUtil.isNotEmpty(userIds)){ +// queryWrapper.in("id", userIds); +// } +// } + + //TODO 外部模拟登陆临时账号,列表不显示 + queryWrapper.ne("username", "_reserve_user_external"); + // 代码逻辑说明: 【JHHB-765】需要能设置排序--- + queryWrapper.orderByAsc("sort"); + queryWrapper.orderByDesc("create_time"); + Page page = new Page(pageNo, pageSize); + IPage pageList = this.page(page, queryWrapper); + + //批量查询用户的所属部门 + //step.1 先拿到全部的 useids + //step.2 通过 useids,一次性查询用户的所属部门名字 + List userIds = pageList.getRecords().stream().map(SysUser::getId).collect(Collectors.toList()); + if (userIds != null && userIds.size() > 0) { + Map useDepNames = this.getDepNamesByUserIds(userIds); + pageList.getRecords().forEach(item -> { + item.setOrgCodeTxt(useDepNames.get(item.getId())); + //增加所属部门id,前台需要展示 + List departs = sysDepartMapper.queryDepartsByUserId(item.getId()); + if(oConvertUtils.isNotEmpty(departs)){ + item.setBelongDepIds(String.join(SymbolConstant.COMMA, departs)); + } + //查询用户的租户ids + List list = userTenantMapper.getTenantIdsByUserId(item.getId()); + if (oConvertUtils.isNotEmpty(list)) { + item.setRelTenantIds(StringUtils.join(list.toArray(), SymbolConstant.COMMA)); + } else { + item.setRelTenantIds(""); + } + Integer posTenantId = null; + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + posTenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0);; + } + //查询用户职位关系表(获取租户下面的) + // 代码逻辑说明: 【QQYUN-7028】用户职务保存后未回显--- + List positionList = sysUserPositionMapper.getPositionIdByUserTenantId(item.getId(),posTenantId); + item.setPost(CommonUtils.getSplitText(positionList,SymbolConstant.COMMA)); + + //是否根据租户隔离(敲敲云用户列表专用,用于展示是否同步钉钉) + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + //查询账号表是否已同步钉钉 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysThirdAccount::getSysUserId,item.getId()); + query.eq(SysThirdAccount::getTenantId, tenantId); + //目前只有同步钉钉 + query.eq(SysThirdAccount::getThirdType, MessageTypeEnum.DD.getType()); + //不为空代表已同步钉钉 + List account = sysThirdAccountService.list(query); + if(CollectionUtil.isNotEmpty(account)){ + item.setIzBindThird(true); + } + } + //查询部门的兼职岗位 + List depPostList = depPostMapper.getDepPostByUserId(item.getId()); + if(CollectionUtil.isNotEmpty(depPostList)){ + item.setOtherDepPostId(StringUtils.join(depPostList.toArray(), SymbolConstant.COMMA)); + } + }); + } + + result.setSuccess(true); + result.setResult(pageList); + //log.info(pageList.toString()); + return result; + } + + + @Override + @CacheEvict(value = {CacheConstant.SYS_USERS_CACHE}, allEntries = true) + public Result resetPassword(String username, String oldpassword, String newpassword, String confirmpassword) { + SysUser user = userMapper.getUserByName(username); + String passwordEncode = PasswordUtil.encrypt(username, oldpassword, user.getSalt()); + if (!user.getPassword().equals(passwordEncode)) { + return Result.error("旧密码输入错误!"); + } + if (oConvertUtils.isEmpty(newpassword)) { + return Result.error("新密码不允许为空!"); + } + if (!newpassword.equals(confirmpassword)) { + return Result.error("两次输入密码不一致!"); + } + String password = PasswordUtil.encrypt(username, newpassword, user.getSalt()); + this.userMapper.update(new SysUser().setPassword(password), new LambdaQueryWrapper().eq(SysUser::getId, user.getId())); + return Result.ok("密码重置成功!"); + } + + @Override + @CacheEvict(value = {CacheConstant.SYS_USERS_CACHE}, allEntries = true) + public Result changePassword(SysUser sysUser) { + String salt = oConvertUtils.randomGen(8); + sysUser.setSalt(salt); + String password = sysUser.getPassword(); + String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(), password, salt); + sysUser.setPassword(passwordEncode); + sysUser.setLastPwdUpdateTime(new Date()); + this.userMapper.updateById(sysUser); + return Result.ok("密码修改成功!"); + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + @Transactional(rollbackFor = Exception.class) + public boolean deleteUser(String userId) { + //1.验证当前用户是管理员账号 admin + //验证用户是否为管理员 + this.checkUserAdminRejectDel(userId); + + //2.删除用户 + this.removeById(userId); + return false; + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + @Transactional(rollbackFor = Exception.class) + public boolean deleteBatchUsers(String userIds) { + //1.验证当前用户是管理员账号 admin + this.checkUserAdminRejectDel(userIds); + //2.删除用户 + this.removeByIds(Arrays.asList(userIds.split(","))); + return false; + } + + @Override + public SysUser getUserByName(String username) { + SysUser sysUser = userMapper.getUserByName(username); + //查询用户的租户ids + if(sysUser!=null){ + List list = userTenantMapper.getTenantIdsByUserId(sysUser.getId()); + if (oConvertUtils.isNotEmpty(list)) { + sysUser.setRelTenantIds(StringUtils.join(list.toArray(), SymbolConstant.COMMA)); + } else { + sysUser.setRelTenantIds(""); + } + } + return sysUser; + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void addUserWithRole(SysUser user, String roles) { + this.save(user); + if(oConvertUtils.isNotEmpty(roles)) { + String[] arr = roles.split(","); + for (String roleId : arr) { + SysUserRole userRole = new SysUserRole(user.getId(), roleId); + sysUserRoleMapper.insert(userRole); + } + } + } + + @Override + @CacheEvict(value= {CacheConstant.SYS_USERS_CACHE}, allEntries=true) + @Transactional(rollbackFor = Exception.class) + public void editUserWithRole(SysUser user, String roles) { + this.updateById(user); + //先删后加 + sysUserRoleMapper.delete(new QueryWrapper().lambda().eq(SysUserRole::getUserId, user.getId())); + if(oConvertUtils.isNotEmpty(roles)) { + String[] arr = roles.split(","); + for (String roleId : arr) { + SysUserRole userRole = new SysUserRole(user.getId(), roleId); + sysUserRoleMapper.insert(userRole); + } + } + } + + + @Override + public List getRole(String username) { + return sysUserRoleMapper.getRoleByUserName(username); + } + + /** + * 获取动态首页路由配置 + * + * @param username + * @param version + * @return + */ + @Override + public SysRoleIndex getDynamicIndexByUserRole(String username, String version) { + SysRoleIndex roleIndex = new SysRoleIndex(); + //只有 X-Version=v3 的时候,才读取sys_role_index表获取角色首页配置 + boolean isV3 = CommonConstant.VERSION_V3.equals(version); + if (isV3) { + //1.先查询 用户USER级别 的所有首页配置 + if(oConvertUtils.isNotEmpty(username)){ + LambdaQueryWrapper routeIndexUserQuery = new LambdaQueryWrapper<>(); + //角色首页状态0:未开启 1:开启 + routeIndexUserQuery.eq(SysRoleIndex::getStatus, CommonConstant.STATUS_1); + routeIndexUserQuery.eq(SysRoleIndex::getRelationType, CommonConstant.HOME_RELATION_USER); + routeIndexUserQuery.eq(SysRoleIndex::getRoleCode, username); + //优先级正序排序 + routeIndexUserQuery.orderByAsc(SysRoleIndex::getPriority); + List list = sysRoleIndexService.list(routeIndexUserQuery); + if (CollectionUtils.isNotEmpty(list)) { + roleIndex = list.get(0); + }else{ + //2.用户没有配置,再查询 角色ROLE级别 的所有首页配置 + LambdaQueryWrapper routeIndexQuery = new LambdaQueryWrapper<>(); + //角色首页状态0:未开启 1:开启 + routeIndexQuery.eq(SysRoleIndex::getStatus, CommonConstant.STATUS_1); + //角色所有首页配置 + routeIndexQuery.eq(SysRoleIndex::getRelationType, CommonConstant.HOME_RELATION_ROLE); + //当前用户角色 + List roles = sysUserRoleMapper.getRoleByUserName(username); + String componentUrl = RoleIndexConfigEnum.getIndexByRoles(roles); + roleIndex = new SysRoleIndex(componentUrl); + //用户所有角色 + // 代码逻辑说明: [QQYUN-13187]【新用户登录报错】没有添加角色时 报错 + if(CollectionUtil.isNotEmpty(roles)){ + routeIndexQuery.in(SysRoleIndex::getRoleCode, roles); + } + //优先级正序排序 + routeIndexQuery.orderByAsc(SysRoleIndex::getPriority); + list = sysRoleIndexService.list(routeIndexQuery); + if (CollectionUtils.isNotEmpty(list)) { + roleIndex = list.get(0); + } + } + } + } + + if (oConvertUtils.isEmpty(roleIndex.getComponent())) { + if (isV3) { + // 如果角色没有配置首页,则使用默认首页 + return sysRoleIndexService.queryDefaultIndex(); + } else { + // 非v3返回null + return null; + } + } + return roleIndex; + } + + /** + * 通过用户名获取用户角色集合 + * @param username 用户名 + * @return 角色集合 + */ + @Override + public Set getUserRolesSet(String username) { + // 查询用户拥有的角色集合 + List roles = sysUserRoleMapper.getRoleByUserName(username); + log.info("-------通过数据库读取用户拥有的角色Rules------username: " + username + ",Roles size: " + (roles == null ? 0 : roles.size())); + return new HashSet<>(roles); + } + + /** + * 通过用户名获取用户角色集合 + * @param userId 用户ID + * @return 角色集合 + */ + @Override + public Set getUserRoleSetById(String userId) { + // 查询用户拥有的角色集合 + List roles = sysUserRoleMapper.getRoleCodeByUserId(userId); + log.info("-------通过数据库读取用户拥有的角色Rules------userId: " + userId + ",Roles size: " + (roles == null ? 0 : roles.size())); + return new HashSet<>(roles); + } + + /** + * 通过用户名获取用户权限集合 + * + * @param userId 用户ID + * @return 权限集合 + */ + @Override + public Set getUserPermissionsSet(String userId) { + Set permissionSet = new HashSet<>(); + List permissionList = sysPermissionMapper.queryByUser(userId); + //================= begin 开启租户的时候 如果没有test角色,默认加入test角色================ + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + if (permissionList == null) { + permissionList = new ArrayList<>(); + } + List testRoleList = sysPermissionMapper.queryPermissionByTestRoleId(); + permissionList.addAll(testRoleList); + } + //================= end 开启租户的时候 如果没有test角色,默认加入test角色================ + for (SysPermission po : permissionList) { +// // TODO URL规则有问题? +// if (oConvertUtils.isNotEmpty(po.getUrl())) { +// permissionSet.add(po.getUrl()); +// } + if (oConvertUtils.isNotEmpty(po.getPerms())) { + permissionSet.add(po.getPerms()); + } + } + log.info("-------通过数据库读取用户拥有的权限Perms------userId: "+ userId+",Perms size: "+ (permissionSet==null?0:permissionSet.size()) ); + return permissionSet; + } + + /** + * 升级SpringBoot2.6.6,不允许循环依赖 + * @author:qinfeng + * @update: 2022-04-07 + * @param username + * @return + */ + @Override + public SysUserCacheInfo getCacheUser(String username) { + SysUserCacheInfo info = new SysUserCacheInfo(); + info.setOneDepart(true); + if(oConvertUtils.isEmpty(username)) { + return null; + } + + //查询用户信息 + SysUser sysUser = userMapper.getUserByName(username); + if(sysUser!=null) { + info.setSysUserCode(sysUser.getUsername()); + info.setSysUserName(sysUser.getRealname()); + info.setSysOrgCode(sysUser.getOrgCode()); + } + + //多部门支持in查询 + List list = sysDepartMapper.queryUserDeparts(sysUser.getId()); + List sysMultiOrgCode = new ArrayList(); + if(list==null || list.size()==0) { + //当前用户无部门 + //sysMultiOrgCode.add("0"); + }else if(list.size()==1) { + sysMultiOrgCode.add(list.get(0).getOrgCode()); + }else { + info.setOneDepart(false); + for (SysDepart dpt : list) { + sysMultiOrgCode.add(dpt.getOrgCode()); + } + } + info.setSysMultiOrgCode(sysMultiOrgCode); + + return info; + } + + /** + * 根据部门Id查询 + * @param page + * @param departId 部门id + * @param username 用户账户名称 + * @return + */ + @Override + public IPage getUserByDepId(Page page, String departId,String username) { + return userMapper.getUserByDepId(page, departId,username); + } + + @Override + public IPage getUserByDepIds(Page page, List departIds, String username) { + return userMapper.getUserByDepIds(page, departIds,username); + } + + @Override + public Map getDepNamesByUserIds(List userIds) { + List list = this.baseMapper.getDepNamesByUserIds(userIds); + + Map res = new HashMap(5); + list.forEach(item -> { + if (res.get(item.getUserId()) == null) { + res.put(item.getUserId(), item.getDepartName()); + } else { + res.put(item.getUserId(), res.get(item.getUserId()) + "," + item.getDepartName()); + } + } + ); + return res; + } + +/* @Override + public IPage getUserByDepartIdAndQueryWrapper(Page page, String departId, QueryWrapper queryWrapper) { + LambdaQueryWrapper lambdaQueryWrapper = queryWrapper.lambda(); + + lambdaQueryWrapper.eq(SysUser::getDelFlag, CommonConstant.DEL_FLAG_0); + lambdaQueryWrapper.inSql(SysUser::getId, "SELECT user_id FROM sys_user_depart WHERE dep_id = '" + departId + "'"); + + return userMapper.selectPage(page, lambdaQueryWrapper); + }*/ + + @Override + public IPage queryUserByOrgCode(String orgCode, SysUser userParams, IPage page) { + List list = baseMapper.getUserByOrgCode(page, orgCode, userParams); + //根据部门orgCode查询部门,需要将职位id进行传递 + for (SysUserSysDepartModel model:list) { + List positionList = sysUserPositionMapper.getPositionIdByUserId(model.getId()); + model.setPost(CommonUtils.getSplitText(positionList,SymbolConstant.COMMA)); + } + Integer total = baseMapper.getUserByOrgCodeTotal(orgCode, userParams); + + IPage result = new Page<>(page.getCurrent(), page.getSize(), total); + result.setRecords(list); + + return result; + } + + /** + * 根据角色Id查询 + * @param page + * @param roleId 角色id + * @param username 用户账户名称 + * @param realname 用户姓名 + * @return + */ + @Override + public IPage getUserByRoleId(Page page, String roleId, String username, String realname) { + // 代码逻辑说明: [QQYUN-3980]组织管理中 职位功能 职位表加租户id 加职位-用户关联表------------ + IPage userRoleList = userMapper.getUserByRoleId(page, roleId, username,realname); + List records = userRoleList.getRecords(); + if (null != records && records.size() > 0) { + List userIds = records.stream().map(SysUser::getId).collect(Collectors.toList()); + Map useDepNames = this.getDepNamesByUserIds(userIds); + for (SysUser sysUser : userRoleList.getRecords()) { + //设置部门 + sysUser.setOrgCodeTxt(useDepNames.get(sysUser.getId())); + //设置用户职位id + this.userPositionId(sysUser); + } + } + return userRoleList; + } + + + @Override + @CacheEvict(value= {CacheConstant.SYS_USERS_CACHE}, key="#username") + public void updateUserDepart(String username,String orgCode,Integer loginTenantId) { + baseMapper.updateUserDepart(username, orgCode,loginTenantId); + } + + + @Override + public SysUser getUserByPhone(String phone) { + return userMapper.getUserByPhone(phone); + } + + + @Override + public SysUser getUserByEmail(String email) { + return userMapper.getUserByEmail(email); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addUserWithDepart(SysUser user, String selectedParts) { +// this.save(user); //保存角色的时候已经添加过一次了 + if(oConvertUtils.isNotEmpty(selectedParts)) { + String[] arr = selectedParts.split(","); + for (String deaprtId : arr) { + SysUserDepart userDeaprt = new SysUserDepart(user.getId(), deaprtId); + sysUserDepartMapper.insert(userDeaprt); + } + } + } + + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public void editUserWithDepart(SysUser user, String departs) { + //更新角色的时候已经更新了一次了,可以再跟新一次 + this.updateById(user); + String[] arr = {}; + if(oConvertUtils.isNotEmpty(departs)){ + arr = departs.split(","); + } + //查询已关联部门 + List userDepartList = sysUserDepartMapper.selectList(new QueryWrapper().lambda().eq(SysUserDepart::getUserId, user.getId())); + if(userDepartList != null && userDepartList.size()>0){ + for(SysUserDepart depart : userDepartList ){ + //修改已关联部门删除部门用户角色关系 + if(!Arrays.asList(arr).contains(depart.getDepId())){ + List sysDepartRoleList = sysDepartRoleMapper.selectList( + new QueryWrapper().lambda().eq(SysDepartRole::getDepartId,depart.getDepId())); + List roleIds = sysDepartRoleList.stream().map(SysDepartRole::getId).collect(Collectors.toList()); + if(roleIds != null && roleIds.size()>0){ + departRoleUserMapper.delete(new QueryWrapper().lambda().eq(SysDepartRoleUser::getUserId, user.getId()) + .in(SysDepartRoleUser::getDroleId,roleIds)); + } + } + } + } + //先删后加 + sysUserDepartMapper.delete(new QueryWrapper().lambda().eq(SysUserDepart::getUserId, user.getId())); + if(oConvertUtils.isNotEmpty(departs)) { + for (String departId : arr) { + SysUserDepart userDepart = new SysUserDepart(user.getId(), departId); + sysUserDepartMapper.insert(userDepart); + } + } + } + + + /** + * 校验用户是否有效 + * @param sysUser + * @return + */ + @Override + public Result checkUserIsEffective(SysUser sysUser) { + Result result = new Result(); + //情况1:根据用户信息查询,该用户不存在 + if (sysUser == null) { + result.error500("该用户不存在,请注册"); + baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null); + return result; + } + //情况2:根据用户信息查询,该用户已注销 + // 代码逻辑说明: if条件永远为falsebug------------ + if (CommonConstant.DEL_FLAG_1.equals(sysUser.getDelFlag())) { + baseCommonService.addLog("用户登录失败,用户名:" + sysUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null); + result.error500("该用户已注销"); + return result; + } + //情况3:根据用户信息查询,该用户已冻结 + if (CommonConstant.USER_FREEZE.equals(sysUser.getStatus())) { + baseCommonService.addLog("用户登录失败,用户名:" + sysUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null); + result.error500("该用户已冻结"); + return result; + } + return result; + } + + @Override + public List queryLogicDeleted() { + // 代码逻辑说明: 回收站查询未离职的------------ + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.ne(SysUser::getStatus, CommonConstant.USER_QUIT); + return this.queryLogicDeleted(wrapper); + } + + @Override + public List queryLogicDeleted(LambdaQueryWrapper wrapper) { + if (wrapper == null) { + wrapper = new LambdaQueryWrapper<>(); + } + wrapper.eq(SysUser::getDelFlag, CommonConstant.DEL_FLAG_1); + return userMapper.selectLogicDeleted(wrapper); + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public boolean revertLogicDeleted(List userIds, SysUser updateEntity) { + return userMapper.revertLogicDeleted(userIds, updateEntity) > 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeLogicDeleted(List userIds) { + // 1. 删除用户 + int line = userMapper.deleteLogicDeleted(userIds); + // 2. 删除用户部门关系 + line += sysUserDepartMapper.delete(new LambdaQueryWrapper().in(SysUserDepart::getUserId, userIds)); + //3. 删除用户角色关系 + line += sysUserRoleMapper.delete(new LambdaQueryWrapper().in(SysUserRole::getUserId, userIds)); + //4.同步删除第三方App的用户 + try { + dingtalkService.removeThirdAppUser(userIds); + wechatEnterpriseService.removeThirdAppUser(userIds); + } catch (Exception e) { + log.error("同步删除第三方App的用户失败:", e); + } + //5. 删除第三方用户表(因为第4步需要用到第三方用户表,所以在他之后删) + line += sysThirdAccountMapper.delete(new LambdaQueryWrapper().in(SysThirdAccount::getSysUserId, userIds)); + + //6. 删除租户用户中间表的数据 + line += userTenantMapper.delete(new LambdaQueryWrapper().in(SysUserTenant::getUserId,userIds)); + + return line != 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateNullPhoneEmail() { + userMapper.updateNullByEmptyString("email"); + userMapper.updateNullByEmptyString("phone"); + return true; + } + + @Override + public void saveThirdUser(SysUser sysUser) { + //保存用户 + String userid = UUIDGenerator.generate(); + sysUser.setId(userid); + baseMapper.insert(sysUser); + //获取第三方角色 + SysRole sysRole = sysRoleMapper.selectOne(new LambdaQueryWrapper().eq(SysRole::getRoleCode, "third_role")); + //保存用户角色 + SysUserRole userRole = new SysUserRole(); + userRole.setRoleId(sysRole.getId()); + userRole.setUserId(userid); + sysUserRoleMapper.insert(userRole); + } + + @Override + public List queryByDepIds(List departIds, String username) { + return userMapper.queryByDepIds(departIds,username); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveUser(SysUser user, String selectedRoles, String selectedDeparts, String relTenantIds, boolean izSyncPack) { + if(null == user.getSort()){ + user.setSort(CommonConstant.DEFAULT_USER_SORT); + } + //step.1 保存用户 + this.save(user); + //获取用户保存前台传过来的租户id并添加到租户 + this.saveUserTenant(user.getId(),relTenantIds, izSyncPack); + //step.2 保存角色 + if(oConvertUtils.isNotEmpty(selectedRoles)) { + String[] arr = selectedRoles.split(","); + for (String roleId : arr) { + SysUserRole userRole = new SysUserRole(user.getId(), roleId); + sysUserRoleMapper.insert(userRole); + } + } + + //step.3 保存所属部门 + if(oConvertUtils.isNotEmpty(selectedDeparts)) { + String[] arr = selectedDeparts.split(","); + for (String deaprtId : arr) { + SysUserDepart userDeaprt = new SysUserDepart(user.getId(), deaprtId); + sysUserDepartMapper.insert(userDeaprt); + } + } + + //step.4 保存职位 + this.saveUserPosition(user.getId(),user.getPost()); + //step5 保存兼职岗位 + this.saveUserOtherDepPost(user.getId(),user.getOtherDepPostId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public void editUser(SysUser user, String roles, String departs, String relTenantIds, String updateFromPage) { + //获取用户编辑前台传过来的租户id + this.editUserTenants(user.getId(),relTenantIds); + if(null == user.getSort()){ + user.setSort(CommonConstant.DEFAULT_USER_SORT); + } + //step.1 修改用户基础信息 + this.updateById(user); + //step.2 修改角色 + if (oConvertUtils.isEmpty(updateFromPage) || !"deptUsers".equalsIgnoreCase(updateFromPage)) { + // 处理用户角色 先删后加 , 如果是在部门用户页面修改用户,不处理用户角色,因为该页面无法编辑用户角色. + sysUserRoleMapper.delete(new QueryWrapper().lambda().eq(SysUserRole::getUserId, user.getId())); + if (oConvertUtils.isNotEmpty(roles)) { + String[] arr = roles.split(","); + for (String roleId : arr) { + SysUserRole userRole = new SysUserRole(user.getId(), roleId); + sysUserRoleMapper.insert(userRole); + } + } + } + + //step.3 修改部门 + String[] arr = {}; + if(oConvertUtils.isNotEmpty(departs)){ + arr = departs.split(","); + } + //查询已关联部门 + List userDepartList = sysUserDepartMapper.selectList(new QueryWrapper().lambda().eq(SysUserDepart::getUserId, user.getId())); + if(userDepartList != null && userDepartList.size()>0){ + for(SysUserDepart depart : userDepartList ){ + //修改已关联部门删除部门用户角色关系 + if(!Arrays.asList(arr).contains(depart.getDepId())){ + List sysDepartRoleList = sysDepartRoleMapper.selectList( + new QueryWrapper().lambda().eq(SysDepartRole::getDepartId,depart.getDepId())); + List roleIds = sysDepartRoleList.stream().map(SysDepartRole::getId).collect(Collectors.toList()); + if(roleIds != null && roleIds.size()>0){ + departRoleUserMapper.delete(new QueryWrapper().lambda().eq(SysDepartRoleUser::getUserId, user.getId()) + .in(SysDepartRoleUser::getDroleId,roleIds)); + } + } + } + } + //先删后加 + sysUserDepartMapper.delete(new QueryWrapper().lambda().eq(SysUserDepart::getUserId, user.getId())); + if(oConvertUtils.isNotEmpty(departs)) { + for (String departId : arr) { + SysUserDepart userDepart = new SysUserDepart(user.getId(), departId); + sysUserDepartMapper.insert(userDepart); + } + } + //step.4 修改手机号和邮箱 + // 更新手机号、邮箱空字符串为 null + userMapper.updateNullByEmptyString("email"); + userMapper.updateNullByEmptyString("phone"); + + //step.5 修改职位 + this.editUserPosition(user.getId(),user.getPost()); + + //代码逻辑说明: 兼职岗位改造成中间表的方式--- + //step6 修改兼职岗位 + //先删后加 + depPostMapper.delete(new QueryWrapper().lambda().eq(SysUserDepPost::getUserId, user.getId())); + this.saveUserOtherDepPost(user.getId(),user.getOtherDepPostId()); + } + + + /** + * 保存兼职岗位 + * + * @param userId + * @param otherDepPostId + */ + private void saveUserOtherDepPost(String userId, String otherDepPostId) { + if (oConvertUtils.isNotEmpty(otherDepPostId)) { + String[] depPostId = otherDepPostId.split(SymbolConstant.COMMA); + for (String postId : depPostId) { + SysUserDepPost userPosition = new SysUserDepPost(userId, postId); + depPostMapper.insert(userPosition); + } + } + } + + @Override + public List userIdToUsername(Collection userIdList) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getId, userIdList); + List userList = super.list(queryWrapper); + return userList.stream().map(SysUser::getUsername).collect(Collectors.toList()); + } + + @Override + @Cacheable(cacheNames=CacheConstant.SYS_USERS_CACHE, key="#username") + @SensitiveEncode + public LoginUser getEncodeUserInfo(String username){ + if(oConvertUtils.isEmpty(username)) { + return null; + } + LoginUser loginUser = new LoginUser(); + SysUser sysUser = userMapper.getUserByName(username); + //查询用户的租户ids + this.setUserTenantIds(sysUser); + //设置职位id + this.userPositionId(sysUser); + if(sysUser==null) { + return null; + } + BeanUtils.copyProperties(sysUser, loginUser); + // 查询当前登录用户的部门id + loginUser.setOrgId(this.getDepartIdByOrCode(sysUser.getOrgCode())); + // 查询当前登录用户的角色code(多个逗号分割) + loginUser.setRoleCode(this.getJoinRoleCodeByUserId(sysUser.getId())); + return loginUser; + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + @Transactional(rollbackFor = Exception.class) + public void userQuit(String username) { + SysUser sysUser = userMapper.getUserByName(username); + if(null == sysUser){ + throw new GhbBootException("离职失败,该用户已不存在"); + } + // 代码逻辑说明: [QQYUN-3951]租户用户离职重构------------ + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + //更新用户租户表的状态为离职状态 + if(tenantId==0){ + throw new GhbBootException("离职失败,租户不存在"); + } + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getUserId,sysUser.getId()); + query.eq(SysUserTenant::getTenantId,tenantId); + SysUserTenant userTenant = new SysUserTenant(); + userTenant.setStatus(CommonConstant.USER_TENANT_QUIT); + userTenantMapper.update(userTenant,query); + } + + @Override + public List getQuitList(Integer tenantId) { + return userMapper.getTenantQuitList(tenantId); + } + + @Override + public void updateStatusAndFlag(List userIds, SysUser sysUser) { + userMapper.updateStatusAndFlag(userIds,sysUser); + } + + /** + * 设置登录租户 + * @param sysUser + * @return + */ + @Override + public Result setLoginTenant(SysUser sysUser, JSONObject obj, String username, Result result){ + //用户有哪些租户 +// List tenantList = null; + // 代码逻辑说明: [QQYUN-3371]租户逻辑改造,改成关系表 + List tenantList = relationMapper.getTenantNoCancel(sysUser.getId()); + obj.put("tenantList", tenantList); +// if (null!=tenantIdList && tenantIdList.size()>0) { +// //------------------------------------------------------------------------------------- +// //查询有效的租户集合 +// LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); +// queryWrapper.in(SysTenant::getId, tenantIdList); +// queryWrapper.eq(SysTenant::getStatus, Integer.valueOf(CommonConstant.STATUS_1)); +// tenantList = sysTenantMapper.selectList(queryWrapper); +// //------------------------------------------------------------------------------------- +// +// if (tenantList.size() == 0) { +// return result.error500("与该用户关联的租户均已被冻结,无法登录!"); +// } else { +// obj.put("tenantList", tenantList); +// } +// } + + + //登录会话租户ID,有效性重置 + if (tenantList != null && tenantList.size() > 0) { + if (tenantList.size() == 1) { + sysUser.setLoginTenantId(tenantList.get(0).getId()); + } else { + List listAfterFilter = tenantList.stream().filter(s -> s.getId().equals(sysUser.getLoginTenantId())).collect(Collectors.toList()); + if (listAfterFilter == null || listAfterFilter.size() == 0) { + //如果上次登录租户ID,在用户拥有的租户集合里面没有了,则随机取用户拥有的第一个租户ID + sysUser.setLoginTenantId(tenantList.get(0).getId()); + } + } + } else { + //无租户的时候,设置为 0 + sysUser.setLoginTenantId(0); + } + //设置用户登录缓存租户 + this.updateUserDepart(username, null,sysUser.getLoginTenantId()); + log.debug(" 登录接口用户的租户ID = {}", sysUser.getLoginTenantId()); + if(sysUser.getLoginTenantId()!=null){ + //登录的时候需要手工设置下会话中的租户ID,不然登录接口无法通过租户隔离查询到数据 + TenantContext.setTenant(sysUser.getLoginTenantId()+""); + } + return null; + } + + + /** + * 获取租户id + * @param sysUser + */ + private void setUserTenantIds(SysUser sysUser) { + if(ObjectUtils.isNotEmpty(sysUser)) { + List list = relationMapper.getTenantIdsNoStatus(sysUser.getId()); + if(null!=list && list.size()>0){ + sysUser.setRelTenantIds(StringUtils.join(list.toArray(), ",")); + }else{ + sysUser.setRelTenantIds(""); + } + } + } + + /** + * 保存租户 + * + * @param userId + * @param relTenantIds + * @param izSyncPack 是否需要将用户同步当前产品包下 + */ + private void saveUserTenant(String userId, String relTenantIds, boolean izSyncPack) { + if (oConvertUtils.isNotEmpty(relTenantIds)) { + String[] tenantIds = relTenantIds.split(SymbolConstant.COMMA); + for (String tenantId : tenantIds) { + SysUserTenant relation = new SysUserTenant(); + relation.setUserId(userId); + relation.setTenantId(Integer.valueOf(tenantId)); + relation.setStatus(CommonConstant.STATUS_1); + + LambdaQueryWrapper sysUserTenantQueryWrapper = new LambdaQueryWrapper() + .eq(SysUserTenant::getUserId, userId) + .eq(SysUserTenant::getTenantId,Integer.valueOf(tenantId)); + SysUserTenant tenantPresent = relationMapper.selectOne(sysUserTenantQueryWrapper); + if (tenantPresent != null) { + tenantPresent.setStatus(CommonConstant.STATUS_1); + relationMapper.updateById(tenantPresent); + }else{ + relationMapper.insert(relation); + ISysTenantService currentService = SpringContextUtils.getApplicationContext().getBean(ISysTenantService.class); + //默认添加当前用户到租户套餐中 + currentService.addPackUser(userId,tenantId); + } + } + }else{ + //是否开启系统管理模块的多租户数据隔离【SAAS多租户模式】 + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + // 代码逻辑说明: 判断当前用户是否在当前租户里面,如果不存在在新增------------ + String tenantId = TenantContext.getTenant(); + if(oConvertUtils.isNotEmpty(tenantId)){ + Integer count = relationMapper.userTenantIzExist(userId, Integer.parseInt(tenantId)); + if(count == 0){ + SysUserTenant relation = new SysUserTenant(); + relation.setUserId(userId); + relation.setTenantId(Integer.parseInt(tenantId)); + relation.setStatus(CommonConstant.STATUS_1); + relationMapper.insert(relation); + if(izSyncPack){ + ISysTenantService currentService = SpringContextUtils.getApplicationContext().getBean(ISysTenantService.class); + //自动为用户,添加租户下所有套餐 + currentService.addPackUser(userId,tenantId); + } + } + } + } + } + } + + /** + * 编辑租户 + * @param userId + * @param relTenantIds + */ + private void editUserTenants(String userId, String relTenantIds) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getUserId, userId); + //数据库的租户id + List oldTenantIds = relationMapper.getTenantIdsByUserId(userId); + //如果传过来的租户id为空,那么就删除租户 + if (oConvertUtils.isEmpty(relTenantIds) && CollectionUtils.isNotEmpty(oldTenantIds)) { + this.deleteTenantByUserId(userId, null); + } else if (oConvertUtils.isNotEmpty(relTenantIds) && CollectionUtils.isEmpty(oldTenantIds)) { + //如果传过来的租户id不为空但是数据库的租户id为空,那么就新增 + this.saveUserTenant(userId, relTenantIds, false); + } else { + //都不为空,需要比较,进行添加或删除 + if(oConvertUtils.isNotEmpty(relTenantIds) && CollectionUtils.isNotEmpty(oldTenantIds)){ + //找到新的租户id与原来的租户id不同之处,进行删除 + String[] relTenantIdArray = relTenantIds.split(SymbolConstant.COMMA); + List relTenantIdList = Arrays.asList(relTenantIdArray); + + List deleteTenantIdList = oldTenantIds.stream().filter(item -> !relTenantIdList.contains(item.toString())).collect(Collectors.toList()); + for (Integer tenantId : deleteTenantIdList) { + this.deleteTenantByUserId(userId, tenantId); + } + //找到原来租户的用户id与新的租户id不同之处,进行新增 + String tenantIds = relTenantIdList.stream().filter(item -> !oldTenantIds.contains(Integer.valueOf(item))).collect(Collectors.joining(",")); + this.saveUserTenant(userId, tenantIds, false); + } + } + } + + /** + * 删除租户通过用户id + * @param tenantId + * @param userId + */ + private void deleteTenantByUserId(String userId,Integer tenantId){ + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getUserId, userId); + if(oConvertUtils.isNotEmpty(tenantId)){ + query.eq(SysUserTenant::getTenantId, tenantId); + } + relationMapper.delete(query); + //删除产品包用户关联 + LambdaQueryWrapper packUserQuery = new LambdaQueryWrapper<>(); + packUserQuery.eq(SysTenantPackUser::getUserId, userId); + if(oConvertUtils.isNotEmpty(tenantId)){ + packUserQuery.eq(SysTenantPackUser::getTenantId, tenantId); + } + packUserMapper.delete(packUserQuery); + } + + + + @Override + public void batchEditUsers(JSONObject json) { + String userIds = json.getString("userIds"); + List idList = JSONArray.parseArray(userIds, String.class); + //部门 + String selecteddeparts = json.getString("selecteddeparts"); + //职位 + String post = json.getString("post"); + //工作地点? 没有这个字段 + String workAddress = json.getString("workAddress"); + //批量修改用户职位 + if(oConvertUtils.isNotEmpty(post)) { + //修改职位用户关系表 + for (String userId:idList) { + this.editUserPosition(userId,post); + } + } + if(oConvertUtils.isNotEmpty(selecteddeparts)) { + //查询当前租户的部门列表 + Integer currentTenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + LambdaQueryWrapper departQuery = new LambdaQueryWrapper() + .eq(SysDepart::getTenantId, currentTenantId); + List departList = sysDepartMapper.selectList(departQuery); + if(departList==null || departList.size()==0){ + log.error("batchEditUsers 根据租户ID没有找到部门>"+currentTenantId); + return; + } + List departIdList = new ArrayList(); + for(SysDepart depart: departList){ + if(depart!=null){ + String id = depart.getId(); + if(oConvertUtils.isNotEmpty(id)){ + departIdList.add(id); + } + } + } + //删除人员的部门关联 + LambdaQueryWrapper query = new LambdaQueryWrapper() + .in(SysUserDepart::getUserId, idList) + .in(SysUserDepart::getDepId, departIdList); + sysUserDepartMapper.delete(query); + + String[] arr = selecteddeparts.split(","); + + //再新增 + for (String deaprtId : arr) { + for(String userId: idList){ + SysUserDepart userDepart = new SysUserDepart(userId, deaprtId); + sysUserDepartMapper.insert(userDepart); + } + } + } + } + + @Override + public DepartAndUserInfo searchByKeyword(String keyword) { + DepartAndUserInfo departAndUserInfo = new DepartAndUserInfo(); + if(oConvertUtils.isNotEmpty(keyword)){ + LambdaQueryWrapper query1 = new LambdaQueryWrapper() + .like(SysUser::getRealname, keyword); + String str = oConvertUtils.getString(TenantContext.getTenant(), "0"); + Integer tenantId = Integer.valueOf(str); + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + List userIds = userTenantMapper.getUserIdsByTenantId(tenantId); + if (oConvertUtils.listIsNotEmpty(userIds)) { + query1.in(SysUser::getId, userIds); + }else{ + query1.eq(SysUser::getId, ""); + } + } + List list1 = this.baseMapper.selectList(query1); + if(list1!=null && list1.size()>0){ + List userList = list1.stream().map(v -> new UserAvatar(v)).collect(Collectors.toList()); + departAndUserInfo.setUserList(userList); + } + + LambdaQueryWrapper query2 = new LambdaQueryWrapper() + .like(SysDepart::getDepartName, keyword); + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + query2.eq(SysDepart::getTenantId, tenantId); + } + List list2 = sysDepartMapper.selectList(query2); + if(list2!=null && list2.size()>0){ + List departList = new ArrayList<>(); + for(SysDepart depart: list2){ + List orgName = new ArrayList<>(); + List orgId = new ArrayList<>(); + getParentDepart(depart, orgName, orgId); + DepartInfo departInfo = new DepartInfo(); + departInfo.setId(depart.getId()); + departInfo.setOrgId(orgId); + departInfo.setOrgName(orgName); + departList.add(departInfo); + } + departAndUserInfo.setDepartList(departList); + } + } + return departAndUserInfo; + } + + @Override + public UpdateDepartInfo getUpdateDepartInfo(String departId) { + SysDepart depart = sysDepartMapper.selectById(departId); + if(depart!=null){ + UpdateDepartInfo info = new UpdateDepartInfo(depart); + List subList = sysDepartMapper.queryDeptByPid(departId); + if(subList!=null && subList.size()>0){ + info.setHasSub(true); + } + //获取部门负责人信息 + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(SysUser::getUserIdentity, 2) + .like(SysUser::getDepartIds, depart.getId()); + List userList = this.baseMapper.selectList(query); + if(userList!=null && userList.size()>0){ + List idList = userList.stream().map(i -> i.getId()).collect(Collectors.toList()); + info.setChargePersonList(idList); + } + return info; + } + return null; + } + + @Override + public void doUpdateDepartInfo(UpdateDepartInfo info) { + String departId = info.getDepartId(); + SysDepart depart = sysDepartMapper.selectById(departId); + if(depart!=null){ + //修改部门信息-上级和部门名称 + if(!depart.getParentId().equals(info.getParentId())){ + String pid = info.getParentId(); + SysDepart parentDepart = sysDepartMapper.selectById(pid); + if(parentDepart!=null){ + String orgCode = getNextOrgCode(pid); + depart.setOrgCode(orgCode); + depart.setParentId(pid); + } + } + depart.setDepartName(info.getDepartName()); + sysDepartMapper.updateById(depart); + //先查询这个部门的负责人 + List departChargeUsers = queryDepartChargePersons(departId); + List departChargeUserIdList = departChargeUsers.stream().map(i -> i.getId()).collect(Collectors.toList()); + //修改部门负责人 + List userIdList = info.getChargePersonList(); + if(userIdList!=null && userIdList.size()>0){ + for(String userId: userIdList){ + SysUser user = this.baseMapper.selectById(userId); + if(user!=null){ + departChargeUserIdList.remove(user.getId()); + user.setUserIdentity(2); + String departIds = user.getDepartIds(); + if(oConvertUtils.isEmpty(departIds)){ + user.setDepartIds(departId); + }else{ + List list = new ArrayList(Arrays.asList(departIds.split(","))); + if(list.indexOf(departId)>=0){ + continue; + }else{ + list.add(departId); + String newDepartIds = String.join(",", list); + user.setDepartIds(newDepartIds); + } + } + this.baseMapper.updateById(user); + } + } + // 代码逻辑说明: 部门负责人不能被删除------------ + this.removeDepartmentManager(departChargeUserIdList,departChargeUsers,departId); + }else{ + if(CollectionUtil.isNotEmpty(departChargeUsers)){ + //前端传过来用户列表id为空,说明数据库的负责部门人员均需要删除 + this.removeDepartmentManager(departChargeUserIdList,departChargeUsers,departId); + } + + } + } + } + + private List queryDepartChargePersons(String departId){ + List result = new ArrayList<>(); + // 代码逻辑说明: 部门负责人不能被删除------------ + LambdaQueryWrapper userQuery = new LambdaQueryWrapper<>(); + userQuery.like(SysUser::getDepartIds,departId); + List userList = userMapper.selectList(userQuery); + if(userList!=null && userList.size()>0){ + for(SysUser user: userList){ + Integer identity = user.getUserIdentity(); + String deps = user.getDepartIds(); + if(identity!=null && identity==2){ + if(oConvertUtils.isNotEmpty(deps)){ + if(deps.indexOf(departId)>=0){ + result.add(user); + } + } + } + } + } + return result; + } + + /** + * 变更父级部门 修改编码 + * @param parentId + * @return + */ + private String getNextOrgCode(String parentId){ + JSONObject formData = new JSONObject(); + formData.put("parentId",parentId); + String[] codeArray = (String[]) FillRuleUtil.executeRule(FillRuleConstant.DEPART, formData); + return codeArray[0]; + } + + @Override + public void changeDepartChargePerson(JSONObject json) { + String userId = json.getString("userId"); + String departId = json.getString("departId"); + boolean status = json.getBoolean("status"); + SysUser user = this.getById(userId); + if(user!=null){ + String ids = user.getDepartIds(); + if(status==true){ + //设置部门负责人 + if(oConvertUtils.isEmpty(ids)){ + //设置为上级 + user.setUserIdentity(CommonConstant.USER_IDENTITY_2); + user.setDepartIds(departId); + }else{ + List list = new ArrayList(Arrays.asList(ids.split(","))); + if(list.indexOf(departId)>=0){ + //啥也不干 + }else{ + list.add(departId); + String newIds = String.join(",", list); + //设置为上级 + user.setUserIdentity(CommonConstant.USER_IDENTITY_2); + user.setDepartIds(newIds); + } + } + }else{ + // 取消负责人 + if(oConvertUtils.isNotEmpty(ids)){ + List list = new ArrayList(); + for(String temp: ids.split(",")){ + if(oConvertUtils.isEmpty(temp)){ + continue; + } + if(!temp.equals(departId)){ + list.add(temp); + } + } + String newIds = ""; + if(list.size()>0){ + newIds = String.join(",", list); + }else{ + //负责部门为空时,说明已经是普通用户 + user.setUserIdentity(CommonConstant.USER_IDENTITY_1); + } + user.setDepartIds(newIds); + } + } + this.updateById(user); + } + } + + /** + * 找上级部门 + * @param depart + * @param orgName + * @param orgId + */ + private void getParentDepart(SysDepart depart,List orgName,List orgId){ + String pid = depart.getParentId(); + orgName.add(0, depart.getDepartName()); + orgId.add(0, depart.getId()); + if(oConvertUtils.isNotEmpty(pid)){ + SysDepart temp = sysDepartMapper.selectById(pid); + getParentDepart(temp, orgName, orgId); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public void editTenantUser(SysUser sysUser, String tenantId, String departs, String roles) { + SysUser user = new SysUser(); + user.setWorkNo(sysUser.getWorkNo()); + user.setId(sysUser.getId()); + this.updateById(user); + // 代码逻辑说明: 【QQYUN-5251】人员与部门:部门删除不掉------------ + if(oConvertUtils.isEmpty(departs)){ + //直接删除用户下的的租户部门 + sysUserDepartMapper.deleteUserDepart(user.getId(),tenantId); + }else{ + //修改租户用户下的部门 + this.updateTenantDepart(user, tenantId, departs); + } + //修改用户下的职位 + this.editUserPosition(sysUser.getId(),sysUser.getPost()); + } + + /** + * 修改账号状态 + * @param id 账号id + * @param status 账号状态 + */ + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + public void updateStatus(String id, String status) { + userMapper.update(new SysUser().setStatus(Integer.parseInt(status)), + new UpdateWrapper().lambda().eq(SysUser::getId,id)); + } + + /** + * 修改租户下的部门 + * @param departs + */ + public void updateTenantDepart(SysUser user, String tenantId, String departs) { + List departList = new ArrayList<>(); + long startTime = System.currentTimeMillis(); + if (oConvertUtils.isNotEmpty(departs)) { + //获取当前租户下的部门id,根据前台 + departList = sysUserDepartMapper.getTenantDepart(Arrays.asList(departs.split(SymbolConstant.COMMA)), tenantId); + } + long endTime = System.currentTimeMillis(); + System.out.println("查询用户部门用时:" + (endTime - startTime) + "ms"); + //查询当前租户下部门和用户已关联的部门 + List userDepartList = sysUserDepartMapper.getTenantUserDepart(user.getId(), tenantId); + if (userDepartList != null && userDepartList.size() > 0 && departList.size() > 0) { + for (SysUserDepart depart : userDepartList) { + //修改已关联部门删除部门用户角色关系 + if (!departList.contains(depart.getDepId())) { + List sysDepartRoleList = sysDepartRoleMapper.selectList( + new QueryWrapper().lambda().eq(SysDepartRole::getDepartId, depart.getDepId())); + List roleIds = sysDepartRoleList.stream().map(SysDepartRole::getId).collect(Collectors.toList()); + if (roleIds.size() > 0) { + departRoleUserMapper.delete(new QueryWrapper().lambda().eq(SysDepartRoleUser::getUserId, user.getId()) + .in(SysDepartRoleUser::getDroleId, roleIds)); + } + } + } + } + long endTime1 = System.currentTimeMillis(); + System.out.println("修改部门角色用时:" + (endTime1 - startTime) + "ms"); + + if (departList.size() > 0) { + //删除用户下的部门 + sysUserDepartMapper.deleteUserDepart(user.getId(), tenantId); + for (String departId : departList) { + //添加部门 + SysUserDepart userDepart = new SysUserDepart(user.getId(), departId); + sysUserDepartMapper.insert(userDepart); + } + } + long endTime2 = System.currentTimeMillis(); + System.out.println("修改用户部门用时:" + (endTime2 - startTime) + "ms"); + } + + /** + * 保存用户职位 + * + * @param userId + * @param positionIds + */ + private void saveUserPosition(String userId, String positionIds) { + if (oConvertUtils.isNotEmpty(positionIds)) { + String[] positionIdArray = positionIds.split(SymbolConstant.COMMA); + for (String postId : positionIdArray) { + SysUserPosition userPosition = new SysUserPosition(); + userPosition.setUserId(userId); + userPosition.setPositionId(postId); + sysUserPositionMapper.insert(userPosition); + } + } + } + + /** + * 编辑用户职位 + * + * @param userId + * @param positionIds + */ + private void editUserPosition(String userId, String positionIds) { + //先删除 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserPosition::getUserId, userId); + sysUserPositionMapper.delete(query); + //后新增数据 + this.saveUserPosition(userId, positionIds); + } + + /** + * 设置用户职位id(已逗号拼接起来) + * @param sysUser + */ + private void userPositionId(SysUser sysUser) { + if(null != sysUser){ + List positionList = sysUserPositionMapper.getPositionIdByUserId(sysUser.getId()); + sysUser.setPost(CommonUtils.getSplitText(positionList,SymbolConstant.COMMA)); + } + } + + /** + * 查询用户当前登录部门的id + * + * @param orgCode + */ + private @Nullable String getDepartIdByOrCode(String orgCode) { + if (oConvertUtils.isEmpty(orgCode)) { + return null; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDepart::getOrgCode, orgCode); + queryWrapper.select(SysDepart::getId); + SysDepart depart = sysDepartMapper.selectOne(queryWrapper); + if (depart == null || oConvertUtils.isEmpty(depart.getId())) { + return null; + } + return depart.getId(); + } + + /** + * 查询用户的角色code(多个逗号分割) + * + * @param userId + */ + private @Nullable String getJoinRoleCodeByUserId(String userId) { + if (oConvertUtils.isEmpty(userId)) { + return null; + } + // 判断是否开启saas模式,根据租户id过滤 + Integer tenantId = null; + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + // 开启了但是没有租户ID,默认-1,使其查询不到任何数据 + tenantId = oConvertUtils.getInt(TenantContext.getTenant(), -1); + } + List roleList = sysRoleMapper.getRoleCodeListByUserId(userId, tenantId); + if (CollectionUtils.isEmpty(roleList)) { + return null; + } + return roleList.stream().map(SysRole::getRoleCode).collect(Collectors.joining(SymbolConstant.COMMA)); + } + + /** + * 移除部门负责人 + * @param departChargeUserIdList + * @param departChargeUsers + * @param departId + */ + private void removeDepartmentManager(List departChargeUserIdList,List departChargeUsers,String departId){ + //移除部门负责人 + for(String chargeUserId: departChargeUserIdList){ + for(SysUser chargeUser: departChargeUsers){ + if(chargeUser.getId().equals(chargeUserId)){ + String departIds = chargeUser.getDepartIds(); + List list = new ArrayList(Arrays.asList(departIds.split(","))); + list.remove(departId); + String newDepartIds = String.join(",", list); + chargeUser.setDepartIds(newDepartIds); + this.baseMapper.updateById(chargeUser); + break; + } + } + } + } + + //======================================= begin 用户与部门 用户列表导出 ========================================= + @Override + public ModelAndView exportAppUser(HttpServletRequest request) { + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant()); + // Step.1 组装查询条件,导出选中的部门id数据 + String departIds = request.getParameter("departIds"); + List list = new ArrayList<>(); + if(oConvertUtils.isNotEmpty(departIds)){ + list = Arrays.asList(departIds.split(SymbolConstant.COMMA)); + } + //查询用户数据 + List userList = userMapper.getUserByDepartsTenantId(list, tenantId); + //获取部门名称 + List userDepVos = new ArrayList<>(); + if(CollectionUtil.isNotEmpty(userList)){ + userDepVos = sysDepartMapper.getUserDepartByTenantUserId(userList, tenantId); + } + //获取职位 + List positionVos = sysUserPositionMapper.getPositionIdByUsersTenantId(userList, tenantId); + // step2 根据用户id进行分类 + //循环用户数据将数据整合导出 + List exportUserVoList = new ArrayList<>(); + for (SysUser sysUser : userList) { + AppExportUserVo exportUserVo = new AppExportUserVo(); + BeanUtils.copyProperties(sysUser, exportUserVo); + // 代码逻辑说明: 【QQYUN-10926】组织管理——用户导出时,部门没有导出上下级关系--- + Map departMap = this.getDepartNamesAndCategory(userDepVos, sysUser); + String departNames = departMap.get("departNames"); + exportUserVo.setDepart(departNames.toString()); + String posNames = positionVos.stream().filter(item -> item.getUserId().equals(sysUser.getId())).map(SysUserPositionVo::getName).collect(Collectors.joining(SymbolConstant.SEMICOLON)); + exportUserVo.setPosition(posNames); + exportUserVoList.add(exportUserVo); + } + //step3 封装导出excel参数 + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + //导出文件名称 + mv.addObject(NormalExcelConstants.FILE_NAME, "用户列表"); + mv.addObject(NormalExcelConstants.CLASS, AppExportUserVo.class); + LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + ExportParams exportParams = new ExportParams("导入规则:\n" + + "1、存在用户编号时,数据会根据用户编号进行匹配,匹配成功后只会更新职位和工号;\n" + + "2、不存在用户编号时,支持手机号、邮箱、姓名、部们、职位、工号导入,其中手机号必填;\n" + + "3、上下级部门用英文字符 / 连接,如 财务部/财务一部,多个部门或者职位用英文字符 ; 进行连接,如 财务部;研发部", "导出人:" + user.getRealname(), "导出信息"); + mv.addObject(NormalExcelConstants.PARAMS, exportParams); + mv.addObject(NormalExcelConstants.DATA_LIST, exportUserVoList); + return mv; + } + + /** + * 获取部门名称和部门类型 + * for:【QQYUN-10926】组织管理——用户导出时,部门没有导出上下级关系 + * + * @param userDepVos + * @param sysUser + * @return + */ + private Map getDepartNamesAndCategory(List userDepVos, SysUser sysUser) { + List SysUserDepVoList = userDepVos.stream().filter(item -> item.getUserId().equals(sysUser.getId())) + .map(item -> { + SysUserDepVo userDepVo = new SysUserDepVo(); + userDepVo.setUserId(item.getUserId()); + userDepVo.setDeptId(item.getDeptId()); + userDepVo.setDepartName(item.getDepartName()); + userDepVo.setParentId(item.getParentId()); + userDepVo.setOrgCategory(DepartCategoryEnum.getNameByValue(item.getOrgCategory())); + return userDepVo; + }).collect(Collectors.toList()); + //循环SysUserDepVoList,如果存在父级id的情况下,需要将父级id的部门名称查询出来 + StringBuilder departNames = new StringBuilder(); + StringBuilder departOrgCategorys = new StringBuilder(); + for (SysUserDepVo sysUserDepVo : SysUserDepVoList) { + if(oConvertUtils.isEmpty(sysUserDepVo.getDepartName())){ + continue; + } + //用于查询父级的部门名称 + List departNameList = new LinkedList<>(); + //用于查询父级的部门类型 + List departOrgCategoryList = new LinkedList<>(); + departNameList.add(sysUserDepVo.getDepartName()); + departOrgCategoryList.add(sysUserDepVo.getOrgCategory()); + if (StringUtils.isNotEmpty(sysUserDepVo.getParentId())) { + //递归查询部门名称 + this.getDepartNameByParentId(sysUserDepVo.getParentId(), departNameList, departOrgCategoryList); + } + Collections.reverse(departNameList); + Collections.reverse(departOrgCategoryList); + String departName = departNameList.stream().collect(Collectors.joining(SymbolConstant.SINGLE_SLASH)); + if (StringUtils.isNotEmpty(departNames.toString())) { + departNames.append(SymbolConstant.SEMICOLON); + } + departNames.append(departName); + String orgCatrgory = departOrgCategoryList.stream().collect(Collectors.joining(SymbolConstant.SINGLE_SLASH)); + if (StringUtils.isNotEmpty(departOrgCategorys.toString())) { + departOrgCategorys.append(SymbolConstant.SEMICOLON); + } + departOrgCategorys.append(orgCatrgory); + } + // 代码逻辑说明: 【QQYUN-13617】导入时 部门添加层级不对了--- + Map map = new HashMap<>(); + map.put("departNames", departNames.toString()); + map.put("departOrgCategorys",departOrgCategorys.toString()); + return map; + } + + /** + * 根据父级id查询父级的部门名称和部门类型 + * for:【QQYUN-10926】组织管理——用户导出时,部门没有导出上下级关系 + * + * @param parentId + * @param departNameList + * @param departOrgCategoryList + */ + private void getDepartNameByParentId(String parentId, List departNameList, List departOrgCategoryList) { + SysDepart parentDepartId = sysDepartMapper.getDepartById(parentId); + if (null != parentDepartId) { + departNameList.add(parentDepartId.getDepartName()); + departOrgCategoryList.add(DepartCategoryEnum.getNameByValue(parentDepartId.getOrgCategory())); + if (StringUtils.isNotEmpty(parentDepartId.getParentId())) { + this.getDepartNameByParentId(parentDepartId.getParentId(), departNameList, departOrgCategoryList); + } + } + } + + //======================================= end 用户与部门 用户列表导出 ========================================= + + //======================================= begin 用户与部门 用户列表导入 ========================================= + @Override + public Result importAppUser(HttpServletRequest request) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + Integer tenantId = oConvertUtils.getInt(TenantContext.getTenant()); + SysTenant sysTenant = sysTenantMapper.selectById(tenantId); + // 错误信息 + List errorMessage = new ArrayList<>(); + int successLines = 0, errorLines = 0; + for (Map.Entry entity : fileMap.entrySet()) { + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + //存放职位的map;key为名称 value为职位id。避免多次导入和查询 + Map positionMap = new HashMap<>(); + //存放部门的map;key为名称 value为SysDepart对象。避免多次导入和查询 + Map departMap = new HashMap<>(); + try { + List listSysUsers = ExcelImportUtil.importExcel(file.getInputStream(), AppExportUserVo.class, params); + for (int i = 0; i < listSysUsers.size(); i++) { + //记录现在是多少行 + int lineNumber = i + 1; + //记录是编辑还是添加 + boolean isEdit = false; + AppExportUserVo sysUserExcel = listSysUsers.get(i); + String id = sysUserExcel.getId(); + String workNo = sysUserExcel.getWorkNo(); + String email = sysUserExcel.getEmail(); + String phone = sysUserExcel.getPhone(); + String realname = sysUserExcel.getRealname(); + String depart = sysUserExcel.getDepart(); + String position = sysUserExcel.getPosition(); + SysUser sysUser = new SysUser(); + //判断id是否存在,如果存在的话就是更新 + if (oConvertUtils.isNotEmpty(id)) { + SysUser user = userMapper.selectById(id); + if (null == user) { + errorLines++; + errorMessage.add("第 " + lineNumber + " 行:用户不存在,请查看编号是否已修改,忽略导入。"); + continue; + } + isEdit = true; + sysUser.setId(id); + } else { + //处理租户中是否已存在,用户是否已存在,已存在的用户直接更新 + isEdit = false; + } + if (oConvertUtils.isNotEmpty(workNo)) { + sysUser.setWorkNo(workNo); + } + try { + if (isEdit) { + userMapper.updateById(sysUser); + } else { + if (oConvertUtils.isEmpty(phone)) { + errorMessage.add("第 " + lineNumber + " 行:手机号为空,忽略导入。"); + errorLines++; + continue; + } + SysUser userByPhone = userMapper.getUserByPhone(phone); + if (null != userByPhone) { + //查看看是否已经存在此租户中,存在禁止导入,否则直接更新即可 + Integer tenantCount = userTenantMapper.userTenantIzExist(userByPhone.getId(), tenantId); + if (tenantCount > 0) { + errorMessage.add("第 " + lineNumber + " 行:成员已存在该组织中,如果列表中不存在,请确认该成员是否在审核中或者已离职,忽略导入。"); + errorLines++; + continue; + } + sysUser.setId(userByPhone.getId()); + userMapper.updateById(sysUser); + this.addUserTenant(sysUser.getId(), tenantId, userByPhone.getUsername(),sysTenant.getName()); + } else { + // 密码默认为 “租户门牌号+手机号” + String password = sysTenant.getHouseNumber()+phone; + String salt = oConvertUtils.randomGen(8); + sysUser.setSalt(salt); + // 密码加密加盐 + String passwordEncode = PasswordUtil.encrypt(phone, password, salt); + sysUser.setPassword(passwordEncode); + sysUser.setUsername(phone); + sysUser.setRealname(oConvertUtils.getString(realname,phone)); + sysUser.setEmail(email); + sysUser.setPhone(phone); + sysUser.setStatus(CommonConstant.DEL_FLAG_1); + sysUser.setDelFlag(CommonConstant.DEL_FLAG_0); + sysUser.setCreateTime(new Date()); + userMapper.insert(sysUser); + this.addUserTenant(sysUser.getId(), tenantId, sysUser.getUsername(),sysTenant.getName()); + } + } + //新增或编辑职位 + if (oConvertUtils.isNotEmpty(position)) { + this.addOrEditPosition(sysUser.getId(), position, isEdit, tenantId, positionMap); + } + //新增的时候才可以添加部门 + if (!isEdit) { + //新增或编辑部门 + this.addOrEditDepart(sysUser.getId(), depart, tenantId, departMap); + } + successLines++; + } catch (Exception e) { + errorLines++; + String message = e.getMessage().toLowerCase(); + + // 通过索引名判断出错信息 + if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_USERNAME)) { + errorMessage.add("第 " + lineNumber + " 行:用户名已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_WORK_NO)) { + errorMessage.add("第 " + lineNumber + " 行:工号已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_PHONE)) { + errorMessage.add("第 " + lineNumber + " 行:手机号已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_EMAIL)) { + errorMessage.add("第 " + lineNumber + " 行:电子邮件已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER)) { + errorMessage.add("第 " + lineNumber + " 行:违反表唯一性约束。"); + } else { + errorMessage.add("第 " + lineNumber + " 行:未知错误,忽略导入"); + log.error(e.getMessage(), e); + } + } + } + } catch (Exception e) { + errorMessage.add("发生异常:" + e.getMessage()); + log.error(e.getMessage(), e); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + try { + return ImportExcelUtil.imporReturnRes(errorLines, successLines, errorMessage); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 新增或者编辑职位 + * + * @param userId 用户id + * @param position 职位名称 已/拼接 + * @param isEdit 新增或编辑 + * @param positionMap 职位map key为name,value为职位id + */ + private void addOrEditPosition(String userId, String position, Boolean isEdit, Integer tenantId, Map positionMap) { + Page page = new Page<>(1, 1); + String[] positions = position.split(SymbolConstant.SEMICOLON); + List positionList = Arrays.asList(positions); + positionList = positionList.stream().distinct().collect(Collectors.toList()); + //删除当前租户下的职位,根据职位名称、租户id、用户id + sysUserPositionMapper.deleteUserPosByNameAndTenantId(positionList, tenantId, userId); + //循环需要添加或修改的数据 + for (String pos : positionList) { + String posId = ""; + if (positionMap.containsKey(pos)) { + posId = positionMap.get(pos); + } else { + List namePage = sysPositionMapper.getPositionIdByName(pos, tenantId, page); + if (CollectionUtil.isNotEmpty(namePage)) { + posId = namePage.get(0); + positionMap.put(pos, posId); + } + } + + //职位id不为空直接新增 + if (oConvertUtils.isNotEmpty(posId)) { + this.addSysUserPosition(userId, posId); + continue; + } + + //不是编辑的情况下职位才会新增 + if (!isEdit) { + //新增职位和用户职位关系 + SysPosition sysPosition = new SysPosition(); + sysPosition.setName(pos); + sysPosition.setCode(RandomUtil.randomString(10)); + sysPosition.setTenantId(tenantId); + sysPositionMapper.insert(sysPosition); + positionMap.put(pos, sysPosition.getId()); + this.addSysUserPosition(userId, sysPosition.getId()); + } + } + } + + /** + * 添加用户职位 + */ + private void addSysUserPosition(String userId, String positionId) { + Long count = sysUserPositionMapper.getUserPositionCount(userId, positionId); + if(count == 0){ + SysUserPosition userPosition = new SysUserPosition(); + userPosition.setUserId(userId); + userPosition.setPositionId(positionId); + sysUserPositionMapper.insert(userPosition); + } + } + + /** + * 新增或编辑部门 + * + * @param userId 用户id + * @param depart 部门名称 + * @param tenantId 租户id + * @param departMap 存放部门的map;key为名称 value为SysDepart对象。 + */ + private void addOrEditDepart(String userId, String depart, Integer tenantId, Map departMap) { + //批量将部门和用户信息建立关联关系 + if (StringUtils.isNotEmpty(depart)) { + Page page = new Page<>(1, 1); + //多个部门分离开 + String[] departNames = depart.split(SymbolConstant.SEMICOLON); + List departNameList = Arrays.asList(departNames); + departNameList = departNameList.stream().distinct().collect(Collectors.toList()); + for (String departName : departNameList) { + //部门id + String parentId = ""; + String[] names = departName.split(SymbolConstant.SINGLE_SLASH); + //部门名称拼接 + String nameStr = ""; + for (int i = 0; i < names.length; i++) { + String name = names[i]; + //拼接name + if (oConvertUtils.isNotEmpty(nameStr)) { + nameStr = nameStr + SymbolConstant.SINGLE_SLASH + name; + } else { + nameStr = name; + } + SysDepart sysDepart = null; + //判断map中是否存在该部门名称 + if (departMap.containsKey(nameStr)) { + sysDepart = departMap.get(nameStr); + } else { + //不存在需要去查询 + List departPageByName = sysDepartMapper.getDepartPageByName(page, name, tenantId, parentId); + //部门为空需要新增部门 + if (CollectionUtil.isEmpty(departPageByName)) { + JSONObject formData = new JSONObject(); + formData.put("parentId", parentId); + String[] codeArray = (String[]) FillRuleUtil.executeRule(FillRuleConstant.DEPART, formData); + sysDepart = new SysDepart(); + sysDepart.setParentId(parentId); + sysDepart.setOrgCode(codeArray[0]); + sysDepart.setOrgType(codeArray[1]); + sysDepart.setTenantId(tenantId); + sysDepart.setDepartName(name); + sysDepart.setIzLeaf(CommonConstant.IS_LEAF); + sysDepart.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0)); + sysDepart.setStatus(CommonConstant.STATUS_1); + sysDepartMapper.insert(sysDepart); + } else { + sysDepart = departPageByName.get(0); + } + //父级id不为空那么就将父级部门改成不是叶子节点 + if (oConvertUtils.isNotEmpty(parentId)) { + sysDepartMapper.setMainLeaf(parentId, CommonConstant.NOT_LEAF); + } + parentId = sysDepart.getId(); + departMap.put(nameStr, sysDepart); + } + //最后一位新增部门用户关系表 + if (i == names.length - 1) { + Long count = sysUserDepartMapper.getCountByDepartIdAndUserId(userId, sysDepart.getId()); + if(count == 0){ + SysUserDepart userDepart = new SysUserDepart(userId, sysDepart.getId()); + sysUserDepartMapper.insert(userDepart); + } + } + } + } + } + + } + + /** + * 添加用户租户 + * + * @param userId + * @param tenantId + * @param invitedUsername 被邀请人的账号 + * @param tenantName 租户名称 + */ + private void addUserTenant(String userId, Integer tenantId, String invitedUsername, String tenantName) { + SysUserTenant userTenant = new SysUserTenant(); + userTenant.setTenantId(tenantId); + userTenant.setUserId(userId); + userTenant.setStatus(CommonConstant.USER_TENANT_INVITE); + userTenantMapper.insert(userTenant); + //发送系统消息通知 + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + MessageDTO messageDTO = new MessageDTO(); + String title = sysUser.getRealname() + " 邀请您加入 " + tenantName + "。"; + messageDTO.setTitle(title); + Map data = new HashMap<>(); + // 代码逻辑说明: 【QQYUN-8425】用户导入成功后 消息提醒 跳转至同意页面--- + data.put(CommonConstant.NOTICE_MSG_BUS_TYPE,SysAnnmentTypeEnum.TENANT_INVITE.getType()); + messageDTO.setData(data); + messageDTO.setContent(title); + messageDTO.setToUser(invitedUsername); + messageDTO.setFromUser("system"); + systemSendMsgHandle.sendMessage(messageDTO); + } + //======================================= end 用户与部门 用户列表导入 ========================================= + + @Override + public void checkUserAdminRejectDel(String userIds) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.in(SysUser::getId,Arrays.asList(userIds.split(SymbolConstant.COMMA))); + query.eq(SysUser::getUsername,"admin"); + Long adminRoleCount = this.baseMapper.selectCount(query); + //大于0说明存在管理员用户,不允许删除 + if(adminRoleCount>0){ + throw new GhbBootException("admin用户,不允许删除!"); + } + } + + @Override + public void changePhone(JSONObject json, String username) { + String smscode = json.getString("smscode"); + String phone = json.getString("phone"); + String type = json.getString("type"); + if(oConvertUtils.isEmpty(phone)){ + throw new GhbBootException("请填写原手机号!"); + } + if(oConvertUtils.isEmpty(smscode)){ + throw new GhbBootException("请填写验证码!"); + } + //step1 验证原手机号是否和当前用户匹配 + SysUser sysUser = userMapper.getUserByNameAndPhone(phone,username); + if (null == sysUser){ + throw new GhbBootException("原手机号不匹配,无法修改密码!"); + } + //step2 根据类型判断是验证原手机号的验证码还是新手机号的验证码 + //验证原手机号 + if(CommonConstant.VERIFY_ORIGINAL_PHONE.equals(type)){ + this.verifyPhone(phone, smscode); + }else if(CommonConstant.UPDATE_PHONE.equals(type)){ + //修改手机号 + String newPhone = json.getString("newPhone"); + //需要验证新手机号和原手机号是否一致,一致不让修改 + if(newPhone.equals(phone)){ + throw new GhbBootException("新手机号与原手机号一致,无法修改!"); + } + this.verifyPhone(newPhone, smscode); + //step3 新手机号验证码验证成功之后即可修改手机号 + sysUser.setPhone(newPhone); + userMapper.updateById(sysUser); + } + } + + /** + * 验证手机号 + * + * @param phone + * @param smsCode + * @return + */ + public void verifyPhone(String phone, String smsCode){ + String phoneKey = CommonConstant.CHANGE_PHONE_REDIS_KEY_PRE + phone; + Object phoneCode = redisUtil.get(phoneKey); + if(null == phoneCode){ + throw new GhbBootException("验证码失效,请重新发送验证码!"); + } + if(!smsCode.equals(phoneCode.toString())) { + throw new GhbBootException("短信验证码不匹配!"); + } + //验证完成之后清空手机验证码 + redisUtil.removeAll(phoneKey); + } + + @Override + public void sendChangePhoneSms(JSONObject jsonObject, String username, String ipAddress) { + String type = jsonObject.getString("type"); + String phone = jsonObject.getString("phone"); + if(oConvertUtils.isEmpty(phone)){ + throw new GhbBootException("请填写手机号!"); + } + //step1 根据类型判断是发送旧手机号验证码还是新的手机号验证码 + if(CommonConstant.VERIFY_ORIGINAL_PHONE.equals(type)){ + //step2 旧手机号验证码需要验证手机号是否匹配 + SysUser sysUser = userMapper.getUserByNameAndPhone(phone, username); + if(null == sysUser){ + throw new GhbBootException("旧手机号不匹配,无法修改手机号!"); + } + }else if(CommonConstant.UPDATE_PHONE.equals(type)){ + //step3 新手机号需要验证手机号码是否已注册过 + SysUser userByPhone = userMapper.getUserByPhone(phone); + if(null != userByPhone){ + throw new GhbBootException("手机号已被注册,请尝试其他手机号!"); + } + } + //step4 发送短信验证码 + String redisKey = CommonConstant.CHANGE_PHONE_REDIS_KEY_PRE+phone; + this.sendPhoneSms(phone, ipAddress,redisKey); + } + + @Override + public void sendLogOffPhoneSms(JSONObject jsonObject, String username, String ipAddress) { + String phone = jsonObject.getString("phone"); + //通过用户名查询数据库中的手机号 + SysUser userByNameAndPhone = userMapper.getUserByNameAndPhone(phone, username); + if (null == userByNameAndPhone) { + throw new GhbBootException("当前用户手机号不匹配,无法修改!"); + } + String code = CommonConstant.LOG_OFF_PHONE_REDIS_KEY_PRE + phone; + this.sendPhoneSms(phone, ipAddress, code); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void userLogOff(JSONObject jsonObject, String username) { + String phone = jsonObject.getString("phone"); + String smsCode = jsonObject.getString("smscode"); + //通过用户名查询数据库中的手机号 + SysUser userByNameAndPhone = userMapper.getUserByNameAndPhone(phone, username); + if (null == userByNameAndPhone) { + throw new GhbBootException("当前用户手机号不匹配,无法注销!"); + } + String code = CommonConstant.LOG_OFF_PHONE_REDIS_KEY_PRE + phone; + Object redisSmdCode = redisUtil.get(code); + if (null == redisSmdCode) { + throw new GhbBootException("验证码失效,无法注销!"); + } + if (!redisSmdCode.toString().equals(smsCode)) { + throw new GhbBootException("验证码不匹配,无法注销!"); + } + this.deleteUser(userByNameAndPhone.getId()); + redisUtil.removeAll(code); + redisUtil.removeAll(CacheConstant.SYS_USERS_CACHE + phone); + } + + /** + * 发送短信验证码 + * @param phone + */ + private void sendPhoneSms(String phone, String clientIp,String redisKey) { + Object object = redisUtil.get(redisKey); + + if (object != null) { + throw new GhbBootException("验证码10分钟内,仍然有效!"); + } + + //增加 check防止恶意刷短信接口 + if(!DySmsLimit.canSendSms(clientIp)){ + log.warn("--------[警告] IP地址:{}, 短信接口请求太多-------", clientIp); + throw new GhbBootException("短信接口请求太多,请稍后再试!", CommonConstant.PHONE_SMS_FAIL_CODE); + } + + //随机数 + String captcha = RandomUtil.randomNumbers(6); + JSONObject obj = new JSONObject(); + obj.put("code", captcha); + try { + boolean sendSmsSuccess = DySmsHelper.sendSms(phone, obj, DySmsEnum.LOGIN_TEMPLATE_CODE); + if(!sendSmsSuccess){ + throw new GhbBootException("短信验证码发送失败,请稍后重试!"); + } + //验证码10分钟内有效 + redisUtil.set(redisKey, captcha, 600); + } catch (ClientException e) { + log.error(e.getMessage(),e); + throw new GhbBootException("短信接口未配置,请联系管理员!"); + } + } + + //================================================= begin 低代码部门导入导出 ================================================================ + @Override + public List getDepartAndRoleExportMsg(List userList) { + List list = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(userList)) { + //获取部门 + List userDepVos = sysDepartMapper.getUserDepartByUserId(userList); + //获取角色 + List sysRoles = sysRoleMapper.getUserRoleByUserId(userList); + //存放职位名称的map,key:主岗位的id value: 职级的名称 + Map postNameMap = new HashMap<>(); + //组装数据并返回 + for (SysUser sysUser : userList) { + SysUserExportVo userExportVo = new SysUserExportVo(); + BeanUtils.copyProperties(sysUser, userExportVo); + // 代码逻辑说明: 【QQYUN-13617】导入时 部门添加层级不对了--- + Map departMap = this.getDepartNamesAndCategory(userDepVos, sysUser); + String departNames = departMap.get("departNames"); + userExportVo.setDepartNames(departNames); + userExportVo.setOrgCategorys(departMap.get("departOrgCategorys")); + String departIds = sysUser.getDepartIds(); + if (oConvertUtils.isNotEmpty(departIds)) { + List depVoList = sysDepartMapper.getDepartByIds(Arrays.asList(departIds.split(","))); + Map departMaps = this.getDepartNamesAndCategory(userDepVos, sysUser); + userExportVo.setDepartIds(departMaps.get("departNames")); + } + String posNames = sysRoles.stream().filter(item -> item.getUserId().equals(sysUser.getId())).map(SysUserPositionVo::getName).collect(Collectors.joining(SymbolConstant.SEMICOLON)); + userExportVo.setRoleNames(posNames); + if (null != sysUser.getMainDepPostId()) { + String postName = ""; + if (null != postNameMap && postNameMap.containsKey(sysUser.getMainDepPostId())) { + postName = postNameMap.get(sysUser.getMainDepPostId()); + } else { + postName = sysDepartMapper.getPostNameByPostId(sysUser.getMainDepPostId()); + } + userExportVo.setPostName(postName); + postNameMap.put(sysUser.getMainDepPostId(), postName); + } + // 代码逻辑说明: 兼职岗位改造成中间表的方式--- + List depPost = depPostMapper.getDepPostByUserId(sysUser.getId()); + if(CollectionUtil.isNotEmpty(depPost)){ + userExportVo.setOtherDepPostId(String.join(SymbolConstant.COMMA, depPost)); + } + list.add(userExportVo); + } + } + return list; + } + + @Override + public Result importSysUser(HttpServletRequest request) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + String fileKey = multipartRequest.getParameter("fileKey"); + Map fileMap = multipartRequest.getFileMap(); + // 错误信息 + List errorMessage = new ArrayList<>(); + int successLines = 0, errorLines = 0; + //存放部门的map;key为名称 value为SysDepart对象。避免多次导入和查询 + Map departMap = new HashMap<>(); + //职级map key: 职级名称 value: 职级id + Map positionMap = new HashMap<>(); + //岗位map key:岗位名称 + 部门id value:岗位(部门id) + Map postMap = new HashMap<>(); + String tenantId = TokenUtils.getTenantIdByRequest(request); + for (Map.Entry entity : fileMap.entrySet()) { + MultipartFile file = entity.getValue();// 获取上传文件对象 + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List listSysUsers = ExcelImportUtil.importExcel(file.getInputStream(), SysUserImportVo.class, params); + ImportSysUserCache.setImportSysUserMap(fileKey,0,listSysUsers.size(),"user"); + for (int i = 0; i < listSysUsers.size(); i++) { + SysUserImportVo sysUserExcel = listSysUsers.get(i); + SysUser sysUser = new SysUser(); + BeanUtils.copyProperties(sysUserExcel, sysUser); + if (oConvertUtils.isEmpty(sysUser.getUsername())) { + errorLines += 1; + int lineNumber = i + 1; + errorMessage.add("第 " + lineNumber + " 行:用户账号为空,忽略导入。"); + continue; + } + try { + String username = sysUser.getUsername(); + //根据用户名程序,为空则添加用户 + SysUser userByName = userMapper.getUserByName(username); + if (null != userByName) { + errorLines += 1; + int lineNumber = i + 1; + errorMessage.add("第 " + lineNumber + " 行:用户名已经存在,忽略导入。"); + continue; + } else { + // 密码默认为 “123456” + sysUser.setPassword(PasswordConstant.DEFAULT_PASSWORD); + // 密码加密加盐 + String salt = oConvertUtils.randomGen(8); + sysUser.setSalt(salt); + String passwordEncode = PasswordUtil.encrypt(sysUserExcel.getUsername(), sysUser.getPassword(), salt); + sysUser.setPassword(passwordEncode); + sysUser.setActivitiSync(CommonConstant.ACT_SYNC_1); + if(null == sysUser.getDelFlag()){ + sysUser.setDelFlag(CommonConstant.DEL_FLAG_0); + } + if(null == sysUser.getStatus()){ + sysUser.setStatus(CommonConstant.STATUS_1_INT); + } + this.save(sysUser); + } + //添加部门 + String departNames = sysUserExcel.getDepartNames(); + String orgCategorys = sysUserExcel.getOrgCategorys(); + //新增或编辑部门 + Integer tenantIdInt = 0; + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + tenantIdInt = oConvertUtils.getInt(tenantId, 0); + } + this.lowAddOrEditDepart(sysUser.getId(), departNames, tenantIdInt, departMap, orgCategorys, sysUserExcel.getPostName(), sysUserExcel.getMainDepPostId(),postMap,positionMap, sysUserExcel.getOtherDepPostId()); + //新增或编辑角色 + String roleNames = sysUserExcel.getRoleNames(); + this.saveOrEditRole(sysUser.getId(), roleNames, tenantIdInt); + //新增或编辑职位 + /* String position = sysUserExcel.getPost(); + if (oConvertUtils.isNotEmpty(position)) { + this.addOrEditPosition(sysUser.getId(), position, false, tenantIdInt, positionMap); + }*/ + //添加负责部门 + this.saveChargeDepart(sysUser, sysUserExcel.getDepartIds(), departMap); + successLines++; + } catch (Exception e) { + errorLines++; + String message = e.getMessage().toLowerCase(); + int lineNumber = i + 1; + // 通过索引名判断出错信息 + if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_USERNAME)) { + errorMessage.add("第 " + lineNumber + " 行:用户名已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_WORK_NO)) { + errorMessage.add("第 " + lineNumber + " 行:工号已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_PHONE)) { + errorMessage.add("第 " + lineNumber + " 行:手机号已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_EMAIL)) { + errorMessage.add("第 " + lineNumber + " 行:电子邮件已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER)) { + errorMessage.add("第 " + lineNumber + " 行:违反表唯一性约束。"); + } else { + errorMessage.add("第 " + lineNumber + " 行:未知错误,忽略导入"); + log.error(e.getMessage(), e); + } + } + ImportSysUserCache.setImportSysUserMap(fileKey,i,listSysUsers.size(),"user"); + } + } catch (Exception e) { + ImportSysUserCache.removeImportLowAppMap(fileKey); + errorMessage.add("发生异常:" + e.getMessage()); + log.error(e.getMessage(), e); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + ImportSysUserCache.removeImportLowAppMap(fileKey); + log.error(e.getMessage(), e); + } + } + } + try { + departMap.clear(); + departMap = null; + //最终导入完成 + ImportSysUserCache.setImportSysUserMap(fileKey,1,1,"user"); + return ImportExcelUtil.imporReturnRes(errorLines, successLines, errorMessage); + } catch (IOException e) { + ImportSysUserCache.removeImportLowAppMap(fileKey); + throw new RuntimeException(e); + } + } + + //================================================================ begin 【用户导入】导入时 部门添加层级不对了====================================================================== + /** + * 低代码下添加部门和用户 + * + * @param userId 用户id + * @param depart 部门名称 + * @param tenantId 租户id + * @param departMap 存放部门的map;key为名称 value为SysDepart对象。 + * @param orgCategorys 部门类型 + * @param postName 职级名称 + * @param mainDepPostName 主岗位名称 + * @param postMap key: 岗位名称 + 部门id value:岗位(部门id) + * @param positionMap key: 职级名称 value: 职级id + * @param otherDepPostName 兼职岗位名称 + * @Description 和敲敲云分割处理,原因:因低代码岗位等改造,有级别,故添加部门分开处理 + */ + private void lowAddOrEditDepart(String userId, String depart, Integer tenantId, Map departMap, String orgCategorys, String postName, String mainDepPostName, Map postMap, Map positionMap, String otherDepPostName) { + //批量将部门和用户信息建立关联关系 + if (StringUtils.isNotEmpty(depart)) { + Page page = new Page<>(1, 1); + //多个部门分离开 + String[] departNames = depart.split(SymbolConstant.SEMICOLON); + List departNameList = Arrays.asList(departNames); + //部门类型 + List categoryList = new ArrayList<>(); + if (oConvertUtils.isNotEmpty(orgCategorys)) { + categoryList = Arrays.asList(orgCategorys.split(SymbolConstant.SEMICOLON)); + } + departNameList = departNameList.stream().distinct().collect(Collectors.toList()); + //当下部门循环下标 + int index = 0; + //是否已导入岗位,岗位只导入第一个部门下 + boolean izImportPost = false; + for (String departName : departNameList) { + //部门id + String parentId = ""; + String[] names = departName.split(SymbolConstant.SINGLE_SLASH); + //部门名称拼接 + String nameStr = ""; + //部门类型 + String[] orgCategory = null; + if (categoryList != null && categoryList.size() > index) { + orgCategory = categoryList.get(index).split(SymbolConstant.SINGLE_SLASH); + } + for (int i = 0; i < names.length; i++) { + String name = names[i]; + //拼接name + if (oConvertUtils.isNotEmpty(nameStr)) { + nameStr = nameStr + SymbolConstant.SINGLE_SLASH + name; + } else { + nameStr = name; + } + SysDepart sysDepart = null; + //默认部门 + String category = DepartCategoryEnum.DEPART_CATEGORY_DEPART.getValue(); + if (null != orgCategory && orgCategory.length > i) { + category = orgCategory[i]; + } + //判断map中是否存在该部门名称 + if (departMap.containsKey(nameStr)) { + sysDepart = departMap.get(nameStr); + parentId = sysDepart.getId(); + } else { + //不存在需要去查询 + List departPageByName = sysDepartMapper.getDepartPageByName(page, name, tenantId, parentId); + //部门为空需要新增部门 + if (CollectionUtil.isEmpty(departPageByName)) { + JSONObject formData = new JSONObject(); + formData.put("parentId", parentId); + String[] codeArray = (String[]) FillRuleUtil.executeRule(FillRuleConstant.DEPART, formData); + sysDepart = new SysDepart(); + sysDepart.setParentId(parentId); + sysDepart.setOrgCode(codeArray[0]); + sysDepart.setOrgType(codeArray[1]); + sysDepart.setTenantId(tenantId); + sysDepart.setDepartName(name); + sysDepart.setIzLeaf(CommonConstant.IS_LEAF); + sysDepart.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0)); + sysDepart.setStatus(CommonConstant.STATUS_1); + sysDepart.setOrgCategory(DepartCategoryEnum.getValueByName(category)); + sysDepartMapper.insert(sysDepart); + } else { + sysDepart = departPageByName.get(0); + } + //父级id不为空那么就将父级部门改成不是叶子节点 + if (oConvertUtils.isNotEmpty(parentId)) { + sysDepartMapper.setMainLeaf(parentId, CommonConstant.NOT_LEAF); + } + parentId = sysDepart.getId(); + departMap.put(nameStr, sysDepart); + } + //最后一位新增部门用户关系表 + if (i == names.length - 1 && !izImportPost) { + Long count = sysUserDepartMapper.getCountByDepartIdAndUserId(userId, sysDepart.getId()); + if (count == 0) { + SysUserDepart userDepart = new SysUserDepart(userId, sysDepart.getId()); + sysUserDepartMapper.insert(userDepart); + } + //添加岗位 + if (oConvertUtils.isNotEmpty(mainDepPostName)) { + this.insertDepartPost(userId, parentId ,postName, mainDepPostName, postMap, tenantId, positionMap); + } + //添加兼职岗位 + if(oConvertUtils.isNotEmpty(otherDepPostName)){ + this.insertOtherDepartPost(userId,parentId,postName, otherDepPostName, postMap, tenantId, positionMap); + } + izImportPost = true; + } + } + index++; + } + } + } + + /** + * 添加部门岗位 + * + * @param mainDepPost 岗位名称 + * @param userId 用户id + * @param departId 部门id【上级部门id】 + * @param postName 职级名称 + * @param mainDepPostName 岗位名称 + * @param postMap 岗位map key:岗位名称 + 部门id value:岗位(部门id) + * @param tenantId 租户id + * @param postionMap 职级map key: 职级名称 value: 职级id + */ + private void insertDepartPost(String userId, String depId, String postName, String mainDepPostName, Map postMap, Integer tenantId, Map postionMap) { + if(mainDepPostName.contains(SymbolConstant.COMMA)){ + mainDepPostName = mainDepPostName.split(SymbolConstant.COMMA)[0]; + } + //当前部门下已经存在岗位就不需要再次添加岗位了 + if (null == postMap || !postMap.containsKey(mainDepPostName + depId)) { + //根据父级部门id和职务名称查找岗位id + String departId = sysDepartMapper.getDepIdByDepIdAndPostName(depId, postName); + //不存在新增岗位 + if (oConvertUtils.isEmpty(departId) ) { + //添加部门岗位信息 + departId = this.addCommontDepartPost(depId, tenantId, mainDepPostName, postionMap, postName, postMap); + } + if(oConvertUtils.isNotEmpty(departId)){ + //更新用户主岗位 + SysUser user = new SysUser(); + user.setId(userId); + user.setMainDepPostId(departId); + userMapper.updateById(user); + } + } + } + + /** + * 导入通用添加部门岗位方法 + * + * @param depId + * @param tenantId + * @param mainDepPostName + * @param postionMap + * @param postName + * @param map + * @param postMap + * @return + */ + private String addCommontDepartPost(String depId, Integer tenantId, String mainDepPostName, Map postionMap, String postName, Map postMap) { + //新增岗位 + SysDepart sysDepart = new SysDepart(); + JSONObject formData = new JSONObject(); + formData.put("parentId", depId); + String[] codeArray = (String[]) FillRuleUtil.executeRule(FillRuleConstant.DEPART, formData); + sysDepart.setParentId(depId); + sysDepart.setOrgCode(codeArray[0]); + sysDepart.setOrgType(codeArray[1]); + sysDepart.setTenantId(tenantId); + sysDepart.setDepartName(mainDepPostName); + sysDepart.setIzLeaf(CommonConstant.IS_LEAF); + sysDepart.setDelFlag(String.valueOf(CommonConstant.DEL_FLAG_0)); + sysDepart.setStatus(CommonConstant.STATUS_1); + sysDepart.setOrgCategory(DepartCategoryEnum.DEPART_CATEGORY_POST.getValue()); + //获取职级id + String positionId = ""; + if (postionMap.containsKey(postName)) { + positionId = postionMap.get(postName); + } else { + //根据租户id和职级名称获取职级id + positionId = this.getSysPosition(tenantId, postName); + } + sysDepart.setPositionId(positionId); + postionMap.put(postName, positionId); + sysDepartMapper.insert(sysDepart); + sysDepartMapper.setMainLeaf(depId, CommonConstant.NOT_LEAF); + postMap.put(mainDepPostName + depId, sysDepart.getId()); + //需要将用户表的主岗位进行关联 + return sysDepart.getId(); + } + + /** + * 添加兼职岗位 + * + * @param userId 用户id + * @param departId 部门id【上级部门id】 + * @param postName 职级名称 + * @param otherDepPostName 兼职岗位名称 + * @param postMap 岗位map key:岗位名称 + 部门id value:岗位(部门id) + * @param tenantId 租户id + * @param postionMap 职级map key: 职级名称 value: 职级id + */ + private void insertOtherDepartPost(String userId, String depId, String postName, String otherDepPostName, Map postMap, Integer tenantId, Map positionMap) { + String[] otherDepPostNames = otherDepPostName.split(SymbolConstant.SEMICOLON); + for (int i = 0; i < otherDepPostNames.length; i++) { + //当前部门下已经存在岗位就不需要再次添加岗位了 + String departId = ""; + if (null == postMap || !postMap.containsKey(otherDepPostNames[i] + depId)) { + //不存在时新增部门岗位 + departId = this.addCommontDepartPost(depId, tenantId, otherDepPostNames[i], positionMap, postName, postMap); + } else { + departId = postMap.get(otherDepPostNames[i] + depId); + } + //插入用岗位第三方中间表 + if (oConvertUtils.isNotEmpty(departId)) { + try { + SysUserDepPost depPost = new SysUserDepPost(userId, departId); + depPostMapper.insert(depPost); + } catch (Exception e) { + log.error("当前岗位插入失败:" + e.getMessage(), e); + } + } + } + } + + /** + * 获取职务信息 + * + * @param tenantId + * @param postName + * @return + */ + private String getSysPosition(Integer tenantId, String postName) { + tenantId = oConvertUtils.getInt(tenantId,0); + Page page = new Page<>(1, 1); + List namePage = sysPositionMapper.getPositionIdByName(postName, tenantId, page); + if (CollectionUtil.isNotEmpty(namePage)) { + return namePage.get(0); + } + return ""; + } + //================================================================ end 【用户导入】导入时 部门添加层级不对了====================================================================== + + private void saveChargeDepart(SysUser sysUser, String departIds, Map departMap) { + //判断那些部门没有,即没有加入到部门,则不能成为负责部门人员 + if (oConvertUtils.isEmpty(departIds)) { + return; + } + //多个部门用;分隔开 + String[] split = departIds.split(SymbolConstant.SEMICOLON); + //负责部门id + StringBuilder departIdBulider = new StringBuilder(); + for (String name : split) { + if (departMap.containsKey(name)) { + SysDepart sysDepart = departMap.get(name); + departIdBulider.append(sysDepart.getId()).append(","); + } + } + // 检查并删除最后一个逗号 + if (departIdBulider.length() > 0 && departIdBulider.charAt(departIdBulider.length() - 1) == ',') { + departIdBulider.deleteCharAt(departIdBulider.length() - 1); + } + SysUser user = new SysUser(); + user.setId(sysUser.getId()); + user.setDepartIds(departIdBulider.toString()); + this.updateById(user); + } + + /** + * 保存或编辑角色 + * + * @param userId + * @param roleNames + * @param tenantIdInt + */ + private void saveOrEditRole(String userId, String roleNames, Integer tenantIdInt) { + if (oConvertUtils.isEmpty(roleNames)) { + return; + } + String[] roleNameArray = roleNames.split(SymbolConstant.SEMICOLON); + //删除用户下的角色 + LambdaQueryWrapper deleteQuery = new LambdaQueryWrapper<>(); + deleteQuery.eq(SysUserRole::getUserId, userId); + sysUserRoleMapper.delete(deleteQuery); + //通过名字获取角色 + LambdaQueryWrapper roleQuery = new LambdaQueryWrapper<>(); + roleQuery.orderByDesc(SysRole::getCreateTime); + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + roleQuery.eq(SysRole::getTenantId, tenantIdInt); + } + for (String roleName : roleNameArray) { + roleQuery.eq(SysRole::getRoleName, roleName); + List sysRoles = sysRoleMapper.selectList(roleQuery); + String roleId = ""; + if (CollectionUtil.isNotEmpty(sysRoles)) { + roleId = sysRoles.get(0).getId(); + } else { + SysRole sysRole = new SysRole(); + sysRole.setRoleName(roleName); + sysRole.setRoleCode(RandomUtil.randomString(10)); + sysRoleMapper.insert(sysRole); + roleId = sysRole.getId(); + } + SysUserRole sysUserRole = new SysUserRole(); + sysUserRole.setUserId(userId); + sysUserRole.setRoleId(roleId); + sysUserRoleMapper.insert(sysUserRole); + } + } + //================================================= end 低代码部门导入导出 ================================================================ + + @Override + public void updatePasswordNotBindPhone(String oldPassword, String password, String username) { + LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal(); + //step1 只能修改自己的密码 + if(!sysUser.getUsername().equals(username)){ + throw new GhbBootBizTipException("只允许修改自己的密码!"); + } + //step2 用户不存在禁止修改密码 + SysUser user = this.getUserByName(username); + if(null == user){ + throw new GhbBootBizTipException("用户不存在,无法修改密码!"); + } + //setp3 如果手机号存在需要用手机号修改密码的方式 + if(oConvertUtils.isNotEmpty(user.getPhone())){ + throw new GhbBootBizTipException("手机号不为空,请根据手机号进行修改密码操作!"); + } + //step4 判断旧密码是否正确 + String passwordEncode = PasswordUtil.encrypt(username, oldPassword, user.getSalt()); + if (!user.getPassword().equals(passwordEncode)) { + throw new GhbBootBizTipException("旧密码输入错误!"); + } + if (oConvertUtils.isEmpty(password)) { + throw new GhbBootBizTipException("新密码不允许为空!"); + } + //step5 修改密码 + String newPassWord = PasswordUtil.encrypt(username, password, user.getSalt()); + this.userMapper.update(new SysUser().setPassword(newPassWord).setLastPwdUpdateTime(new Date()), new LambdaQueryWrapper().eq(SysUser::getId, user.getId())); + } + + /** + * + * @param userName + * @return + */ + @Override + public Map queryUserAndDeptByName(String userName) { + // 返回用户和部门信息(根据需求调整) + Map result = new HashMap<>(); + SysUser user = this.getUserByName(userName); + result.put("userId", user.getId()); + result.put("username", user.getUsername()); + //用户的部门信息 + String orgCode = user.getOrgCode(); + if (oConvertUtils.isEmpty(orgCode)) { + return result; + } + + // 查询公司部门 + String companyName = Optional.ofNullable(sysDepartMapper.queryCompByOrgCode(orgCode)) + .map(SysDepart::getDepartName) + .orElse(""); + + // 查询用户部门并匹配 + String userDeptName = sysDepartMapper.queryDepartsByUsername(userName).stream() + .filter(depart -> orgCode.equals(depart.getOrgCode())) + .findFirst() + .map(SysDepart::getDepartName) + .orElse(""); + + // 设置部门显示文本 + String compDepart; + if (StringUtils.isNotEmpty(companyName) && StringUtils.isNotEmpty(userDeptName)) { + compDepart = companyName.equals(userDeptName) + ? companyName + : companyName + "-" + userDeptName; + } else { + compDepart = StringUtils.isNotEmpty(companyName) ? companyName : userDeptName; + } + result.put("compDepart", compDepart); + return result; + } + + /** + * 查询部门、岗位下的用户 包括子部门下的用户 + * + * @param orgCode + * @param userParams + * @param page + * @return + */ + @Override + public IPage queryDepartPostUserByOrgCode(String orgCode, SysUser userParams, IPage page) { + List sysDepartModels = baseMapper.queryDepartPostUserByOrgCode(page, orgCode, userParams); + if(CollectionUtil.isNotEmpty(sysDepartModels)){ + List userIds = sysDepartModels.stream().map(SysUserSysDepPostModel::getId).toList(); + //获取部门名称 + Map useDepNames = this.getDepNamesByUserIds(userIds); + sysDepartModels.forEach(item -> { + List positionList = sysUserPositionMapper.getPositionIdByUserId(item.getId()); + item.setPost(CommonUtils.getSplitText(positionList,SymbolConstant.COMMA)); + item.setOrgCodeTxt(useDepNames.get(item.getId())); + //查询用户的租户ids + List list = userTenantMapper.getTenantIdsByUserId(item.getId()); + if (oConvertUtils.isNotEmpty(list)) { + item.setRelTenantIds(StringUtils.join(list.toArray(), SymbolConstant.COMMA)); + } else { + item.setRelTenantIds(""); + } + //兼职岗位 + List depPostList = depPostMapper.getDepPostByUserId(item.getId()); + if(CollectionUtil.isNotEmpty(depPostList)){ + item.setOtherDepPostId(StringUtils.join(depPostList.toArray(), SymbolConstant.COMMA)); + } + }); + } + return page.setRecords(sysDepartModels); + } + + /** + * 据 orgCode 查询用户信息(部门全路径,主岗位和兼职岗位的信息),包括公司、子公司、部门 + * + * @param orgCode + * @param userParams + * @param page + * @return + */ + @Override + public IPage queryDepartUserByOrgCode(String orgCode, SysUser userParams, IPage page) { + List sysDepartModels = baseMapper.queryDepartUserByOrgCode(page, orgCode, userParams); + //用户id + List userIdList = sysDepartModels.stream().map(SysUserSysDepPostModel::getId).toList(); + if (CollectionUtil.isNotEmpty(userIdList)) { + //根据用户ids获取部门名称 key 用户id value 部门名称 + Map departNameMap = this.getDepartNamesByUserIds(userIdList, SymbolConstant.COMMA); + //获取兼职岗位 + Map departPostMap = this.getDepartOtherPostByUserIds(userIdList, SymbolConstant.COMMA); + ISysDepartService service = SpringContextUtils.getBean(SysDepartServiceImpl.class); + sysDepartModels.forEach(item -> { + item.setDepartName(departNameMap.get(item.getId())); + item.setOtherPostName(departPostMap.get(item.getId())); + //获取主岗位全路径 + if (oConvertUtils.isNotEmpty(item.getMainDepPostId())) { + SysDepart departById = sysDepartMapper.getDepartById(item.getMainDepPostId()); + if (null != departById) { + String departPathName = service.getDepartPathNameByOrgCode(departById.getOrgCode(), ""); + item.setPostName(departPathName); + } + } + }); + } + return page.setRecords(sysDepartModels); + } + + /** + * 通讯录点击用户获取用户详情(包含用户基本信息、部门全路径、主岗位兼职岗位全路径) + * + * @param userId + * @return + */ + @Override + public SysUserSysDepPostModel getUserDetailByUserId(String userId) { + SysUser sysUser = baseMapper.selectById(userId); + if (null != sysUser) { + SysUserSysDepPostModel userModel = new SysUserSysDepPostModel(); + BeanUtils.copyProperties(sysUser, userModel); + //获取部门名称 + List userIds = new ArrayList<>(); + userIds.add(userId); + Map departNameMap = this.getDepartNamesByUserIds(userIds, "__"); + userModel.setDepartName(departNameMap.get(userId)); + ISysDepartService service = SpringContextUtils.getBean(SysDepartServiceImpl.class); + //获取主岗位全路径 + if (oConvertUtils.isNotEmpty(sysUser.getMainDepPostId())) { + SysDepart departById = sysDepartMapper.getDepartById(sysUser.getMainDepPostId()); + if (null != departById) { + String departPathName = service.getDepartPathNameByOrgCode(departById.getOrgCode(), ""); + userModel.setPostName(departPathName); + } + } + //获取兼职岗位全路径 + Map departPostMap = this.getDepartOtherPostByUserIds(userIds, "__"); + userModel.setOtherPostName(departPostMap.get(userId)); + return userModel; + } + return null; + } + + /** + * 登录获取用户部门信息 + * @param jsonObject + * @return + */ + @Override + public Result loginGetUserDeparts(JSONObject jsonObject) { + Result result = new Result<>(); + //返回内容 + JSONObject obj = new JSONObject(new LinkedHashMap<>()); + // 登录方式 phone:手机 account:账号密码 + String loginType = jsonObject.getString("loginType"); + String username = jsonObject.getString("username"); + String source = oConvertUtils.getString(jsonObject.getString("source"),"PC"); + // 手机号登录校验 + if("phone".equalsIgnoreCase(loginType)){ + String phone = jsonObject.getString("mobile"); + //1.校验用户有效性 + SysUser sysUser = baseMapper.getUserByPhone(phone); + result = this.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + return result; + } + //2.校验验证码 + String smscode = jsonObject.getString("smscode"); + String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; + Object code = redisUtil.get(redisKey); + if (!smscode.equals(code)) { + return Result.error("手机验证码错误"); + } + //3.当前登录账号 + username = sysUser.getUsername(); + String orgCode = sysUser.getOrgCode(); + obj.put("currentOrgCode", orgCode); + }else{ + String password = AesEncryptUtil.resolvePassword(jsonObject.getString("password")); + log.debug("登录密码,原始密码:{},解密密码:{}" , jsonObject.getString("password"), password); + // 手机端没有验证码,不做校验 + if(!"APP".equalsIgnoreCase(source)){ + // step.1 验证码check + SysLoginModel sysLoginModel = new SysLoginModel(); + String inputCode = jsonObject.getString("inputCode"); + String checkKey = jsonObject.getString("checkKey"); + sysLoginModel.setCaptcha(inputCode); + sysLoginModel.setCheckKey(checkKey); + if (inputCode == null) { + result.error500("验证码无效"); + return result; + } + String lowerCaseCaptcha = inputCode.toLowerCase(); + String keyPrefix = Md5Util.md5Encode(sysLoginModel.getCheckKey() + GhbBaseConfig.getSignatureSecret(), "utf-8"); + String realKey = keyPrefix + lowerCaseCaptcha; + Object checkCode = redisUtil.get(realKey); + if (checkCode == null || !checkCode.toString().equals(lowerCaseCaptcha)) { + log.warn("验证码错误,key= {} , Ui checkCode= {}, Redis checkCode = {}", sysLoginModel.getCheckKey(), lowerCaseCaptcha, checkCode); + result.error500("验证码错误"); + result.setCode(HttpStatus.PRECONDITION_FAILED.value()); + return result; + } + } + + // step.2 校验用户是否存在且有效 + SysUser sysUser = baseMapper.getUserByName(username); + result = this.checkUserIsEffective(sysUser); + if(!result.isSuccess()) { + return result; + } + + // step.3 校验用户名或密码是否正确 + String userpassword = PasswordUtil.encrypt(username, password, sysUser.getSalt()); + String syspassword = sysUser.getPassword(); + if (!syspassword.equals(userpassword)) { + result.error500("用户名或密码错误"); + return result; + } + String orgCode = sysUser.getOrgCode(); + obj.put("currentOrgCode", orgCode); + } + // step.4 获取用户部门信息,仅限部门 + List departList = sysDepartMapper.queryDeptByUserAndCategory(username,DepartCategoryEnum.DEPART_CATEGORY_DEPART.getValue()); + if (CollectionUtil.isNotEmpty(departList)) { + ISysDepartService service = SpringContextUtils.getBean(SysDepartServiceImpl.class); + List> departs = departList.stream() + .filter(depart -> oConvertUtils.isNotEmpty(depart) && + oConvertUtils.isNotEmpty(depart.getOrgCode())) + .map(depart -> { + String departName = depart.getDepartNameAbbr(); + //简称是空的情况下,查询全路径名称 + if(oConvertUtils.isEmpty(departName)){ + departName = service.getDepartPathNameByOrgCode(depart.getOrgCode(), ""); + } + Map map = new HashMap<>(); + map.put("orgCode", depart.getOrgCode()); + map.put("departName", departName); + return map; + }) + .collect(Collectors.toList()); + obj.put("departs", departs); + } + result.setResult(obj); + return result; + } + + /** + * 批量重置密码为系统密码 + * @param usernames + */ + @Override + public void resetToSysPassword(String usernames) { + //1.判断是否存在admin账户 + if(hasAdminIntersection(usernames)){ + throw new GhbBootException("所选用户中包含管理员,管理员账号不允许重置密码!!"); + } + List userArr = Arrays.asList(usernames.split(",")); + userArr.stream().forEach(username -> { + if(oConvertUtils.isNotEmpty(username)){ + String salt = oConvertUtils.randomGen(8); + String passwordEncode = PasswordUtil.encrypt(username, PasswordConstant.DEFAULT_PASSWORD, salt); + //重置密码 + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper + .eq("username", username) + .set("last_pwd_update_time", new Date()) + .set("password", passwordEncode) + .set("salt", salt); + this.baseMapper.update(null, updateWrapper); + } + }); + } + + /** + * 更新设备信息 + * @param clientId + * @param userId + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateClientId(String clientId,String userId) { + //解绑之前的设备账户 + if(oConvertUtils.isNotEmpty(clientId)){ + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper + .eq("client_id", clientId) + .set("client_id", null); + this.baseMapper.update(null, updateWrapper); + } + //设置新的绑定 + SysUser sysUser = new SysUser(); + sysUser.setClientId(clientId); + sysUser.setId(userId); + this.baseMapper.updateById(sysUser); + } + + /** + * 根据用户组查询用户列表 + * @param page + * @param groupId + * @param username + * @param realname + * @return + */ + @Override + public IPage getUserByUgroupId(Page page, String groupId, String username, String realname) { + IPage userGroupList = userMapper.getUserByUgroupId(page, groupId, username,realname); + List records = userGroupList.getRecords(); + if (null != records && records.size() > 0) { + List userIds = records.stream().map(SysUser::getId).collect(Collectors.toList()); + Map useDepNames = this.getDepNamesByUserIds(userIds); + for (SysUser sysUser : userGroupList.getRecords()) { + //设置部门 + sysUser.setOrgCodeTxt(useDepNames.get(sysUser.getId())); + //设置用户职位id + this.userPositionId(sysUser); + } + } + return userGroupList; + } + + /** + * 是否有交集 + */ + public static boolean hasAdminIntersection(String usernames) { + if (oConvertUtils.isEmpty(usernames)) { + return false; + } + // 使用HashSet提高查找效率 + Set adminSet = Arrays.stream(ADMIN_ACCOUNT) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + + return Arrays.stream(usernames.split(SymbolConstant.COMMA)) + .map(String::trim) + .map(String::toLowerCase) + .anyMatch(adminSet::contains); + } + /** + * 根据用户ids获取部门名称 + * + * @param userIdList + * @param symbol + * @return + */ + private Map getDepartOtherPostByUserIds(List userIdList, String symbol) { + Map departPostMap = new HashMap<>(); + List departPost = sysDepartMapper.getDepartOtherPostByUserIds(userIdList); + if (CollectionUtil.isNotEmpty(departPost)) { + ISysDepartService service = SpringContextUtils.getBean(SysDepartServiceImpl.class); + departPost.forEach(item -> { + if (oConvertUtils.isNotEmpty(item.getId()) && oConvertUtils.isNotEmpty(item.getOtherDepPostId())) { + String postName = service.getDepartPathNameByOrgCode(item.getOrgCode(), ""); + if (departPostMap.containsKey(item.getId())) { + departPostMap.put(item.getId(), departPostMap.get(item.getId()) + symbol + postName); + } else { + departPostMap.put(item.getId(), postName); + } + } + }); + } + return departPostMap; + } + + /** + * 根据用户ids获取部门名称 + * + * @param userIdList + * @param symbol + * @return + */ + private Map getDepartNamesByUserIds(List userIdList, String symbol) { + Map userOrgCodeMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(userIdList)) { + List userDepPosts = sysUserDepartMapper.getUserDepPostByUserIds(userIdList); + if (CollectionUtil.isNotEmpty(userDepPosts)) { + ISysDepartService service = SpringContextUtils.getBean(SysDepartServiceImpl.class); + userDepPosts.forEach(item -> { + if (oConvertUtils.isNotEmpty(item.getId()) && oConvertUtils.isNotEmpty(item.getOrgCode())) { + String departNamePath = service.getDepartPathNameByOrgCode(item.getOrgCode(), ""); + if (userOrgCodeMap.containsKey(item.getId())) { + userOrgCodeMap.put(item.getId(), userOrgCodeMap.get(item.getId()) + symbol + departNamePath); + } else { + userOrgCodeMap.put(item.getId(), departNamePath); + } + } + }); + } + } + return userOrgCodeMap; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserTenantServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserTenantServiceImpl.java new file mode 100644 index 0000000..f787768 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/SysUserTenantServiceImpl.java @@ -0,0 +1,206 @@ +package com.ghb.base.modules.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.lang.StringUtils; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.constant.CacheConstant; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.vo.LoginUser; +import com.ghb.base.common.util.CommonUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysTenant; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserTenant; +import com.ghb.base.modules.system.mapper.SysTenantPackUserMapper; +import com.ghb.base.modules.system.mapper.SysUserMapper; +import com.ghb.base.modules.system.mapper.SysUserPositionMapper; +import com.ghb.base.modules.system.mapper.SysUserTenantMapper; +import com.ghb.base.modules.system.service.ISysUserTenantService; +import com.ghb.base.modules.system.vo.SysUserDepVo; +import com.ghb.base.modules.system.vo.SysUserTenantVo; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Description: sys_user_tenant_relation + * @Author: Ghb-boot + * @Date: 2022-12-23 + * @Version: V1.0 + */ +@Service +public class SysUserTenantServiceImpl extends ServiceImpl implements ISysUserTenantService { + + @Autowired + private SysUserTenantMapper userTenantMapper; + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysUserPositionMapper userPositionMapper; + + @Autowired + private SysTenantPackUserMapper packUserMapper; + + @Override + public Page getPageUserList(Page page, Integer userTenantId, SysUser user) { + return page.setRecords(userTenantMapper.getPageUserList(page,userTenantId,user)); + } + + @Override + public List setUserTenantIds(List records) { + if(null == records || records.size() == 0){ + return records; + } + for (SysUser sysUser:records) { + //查询租户id + List list = userTenantMapper.getTenantIdsByUserId(sysUser.getId()); + if(oConvertUtils.isNotEmpty(list)){ + sysUser.setRelTenantIds(StringUtils.join(list.toArray(), SymbolConstant.COMMA)); + }else{ + sysUser.setRelTenantIds(""); + } + } + return records; + } + + @Override + public List getUserIdsByTenantId(Integer tenantId) { + return userTenantMapper.getUserIdsByTenantId(tenantId); + } + + @Override + public List getTenantIdsByUserId(String userId) { + return userTenantMapper.getTenantIdsByUserId(userId); + } + + @Override + public List getTenantListByUserId(String userId, List userTenantStatus) { + List tenantListByUserId = userTenantMapper.getTenantListByUserId(userId, userTenantStatus); + // 代码逻辑说明: 【QQYUN-7283】1.已经是会员的租户,不是管理员时,没有购买按钮--- + String noVip = "default"; + tenantListByUserId.forEach((item) ->{ + if(oConvertUtils.isNotEmpty(item.getMemberType()) && !noVip.equals(item.getMemberType())){ + //查询是不是管理员 + Long count = packUserMapper.izHaveBuyAuth(item.getId(), Integer.valueOf(item.getTenantUserId())); + if(count!=0){ + item.setTenantAdmin(true); + } + } + }); + return tenantListByUserId; + } + + @Override + public void updateUserTenantStatus(String id, String tenantId, String userTenantStatus) { + if (oConvertUtils.isEmpty(tenantId)) { + throw new GhbBootException("租户数据为空"); + } + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserTenant::getUserId, id); + query.eq(SysUserTenant::getTenantId, Integer.valueOf(tenantId)); + SysUserTenant userTenant = userTenantMapper.selectOne(query); + if (null == userTenant) { + throw new GhbBootException("租户数据为空"); + } + SysUserTenant tenant = new SysUserTenant(); + tenant.setStatus(userTenantStatus); + this.update(tenant, query); + } + + @Override + public IPage getUserTenantPageList(Page page, List status, SysUser user, Integer tenantId) { + List tenantPageList = userTenantMapper.getUserTenantPageList(page, status, user, tenantId); + List userIds = tenantPageList.stream().map(SysUserTenantVo::getId).collect(Collectors.toList()); + if (userIds != null && userIds.size() > 0) { + Map useDepNames = this.getDepNamesByUserIds(userIds); + tenantPageList.forEach(item -> { + item.setOrgCodeTxt(useDepNames.get(item.getId())); + //查询用户的租户ids + List list = userTenantMapper.getTenantIdsNoStatus(item.getId()); + if (oConvertUtils.isNotEmpty(list)) { + item.setRelTenantIds(StringUtils.join(list.toArray(), SymbolConstant.COMMA)); + } else { + item.setRelTenantIds(""); + } + //查询用户职位,将租户id传到前台 + List positionList = userPositionMapper.getPositionIdByUserId(item.getId()); + item.setPost(CommonUtils.getSplitText(positionList,SymbolConstant.COMMA)); + }); + } + return page.setRecords(tenantPageList); + } + + /** + * 根据用户id获取部门名称 + * + * @param userIds + * @return + */ + public Map getDepNamesByUserIds(List userIds) { + List list = userMapper.getDepNamesByUserIds(userIds); + Map res = new HashMap(5); + list.forEach(item -> { + if (res.get(item.getUserId()) == null) { + res.put(item.getUserId(), item.getDepartName()); + } else { + res.put(item.getUserId(), res.get(item.getUserId()) + "," + item.getDepartName()); + } + } + ); + return res; + } + + @Override + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + @Transactional(rollbackFor = Exception.class) + public void putCancelQuit(List userIds, Integer tenantId) { + userTenantMapper.putCancelQuit(userIds, tenantId); + } + + @Override + public Integer userTenantIzExist(String userId, Integer tenantId) { + return userTenantMapper.userTenantIzExist(userId,tenantId); + } + + @Override + public IPage getTenantPageListByUserId(Page page, String userId, List userTenantStatus,SysUserTenantVo sysUserTenantVo) { + return page.setRecords(userTenantMapper.getTenantPageListByUserId(page,userId,userTenantStatus,sysUserTenantVo)); + } + + @CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true) + @Override + public void agreeJoinTenant(String userId, Integer tenantId) { + userTenantMapper.agreeJoinTenant(userId,tenantId); + } + + @Override + public void refuseJoinTenant(String userId, Integer tenantId) { + userTenantMapper.refuseJoinTenant(userId,tenantId); + } + + @Override + public SysUserTenant getUserTenantByTenantId(String userId, Integer tenantId) { + return userTenantMapper.getUserTenantByTenantId(userId,tenantId); + } + + @Override + public Long getUserCount(Integer tenantId, String tenantStatus) { + return userTenantMapper.getUserCount(tenantId,tenantStatus); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java new file mode 100644 index 0000000..2317a47 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java @@ -0,0 +1,1407 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.jeecg.dingtalk.api.base.JdtBaseAPI; +import com.jeecg.dingtalk.api.core.response.Response; +import com.jeecg.dingtalk.api.core.util.HttpUtil; +import com.jeecg.dingtalk.api.core.vo.AccessToken; +import com.jeecg.dingtalk.api.core.vo.PageResult; +import com.jeecg.dingtalk.api.department.JdtDepartmentAPI; +import com.jeecg.dingtalk.api.department.vo.Department; +import com.jeecg.dingtalk.api.message.JdtMessageAPI; +import com.jeecg.dingtalk.api.message.vo.ActionCardMessage; +import com.jeecg.dingtalk.api.message.vo.MarkdownMessage; +import com.jeecg.dingtalk.api.message.vo.Message; +import com.jeecg.dingtalk.api.message.vo.TextMessage; +import com.jeecg.dingtalk.api.oauth2.JdtOauth2API; +import com.jeecg.dingtalk.api.oauth2.vo.ContactUser; +import com.jeecg.dingtalk.api.user.JdtUserAPI; +import com.jeecg.dingtalk.api.user.body.GetUserListBody; +import com.jeecg.dingtalk.api.user.vo.User; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import com.ghb.base.common.api.dto.message.MessageDTO; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.MessageTypeEnum; +import com.ghb.base.common.exception.GhbBootBizTipException; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.util.*; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.mapper.*; +import com.ghb.base.modules.system.model.SysDepartTreeModel; +import com.ghb.base.modules.system.model.ThirdLoginModel; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.vo.SysPositionVO; +import com.ghb.base.modules.system.vo.thirdapp.JdtDepartmentTreeVo; +import com.ghb.base.modules.system.vo.thirdapp.SyncInfoVo; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + + +/** + * 第三方App对接:钉钉实现类 + * @author: Ghb-boot + */ +@Slf4j +@Service +public class ThirdAppDingtalkServiceImpl implements IThirdAppService { + + @Autowired + GhbBaseConfig GhbBaseConfig; + @Autowired + private ISysDepartService sysDepartService; + @Autowired + private SysUserMapper userMapper; + @Autowired + private ISysThirdAccountService sysThirdAccountService; + @Autowired + private ISysUserDepartService sysUserDepartService; + @Autowired + private ISysPositionService sysPositionService; + @Autowired + private SysAnnouncementSendMapper sysAnnouncementSendMapper; + @Autowired + private SysThirdAppConfigMapper configMapper; + @Autowired + private SysUserTenantMapper userTenantMapper; + @Autowired + private SysTenantMapper tenantMapper; + + /** + * 第三方APP类型,当前固定为 dingtalk + */ + public final String THIRD_TYPE = "dingtalk"; + + @Override + public String getAccessToken() { + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + SysThirdAppConfig config = getDingThirdAppConfig(); + if(null != config){ + return getTenantAccessToken(config); + } + log.warn("租户下未配置钉钉"); + return null; + } + + // update:2022-1-21,updateBy:sunjianlei; for 【JTC-704】【钉钉】部门同步成功,实际没成,后台提示ip白名单 + @Override + public SyncInfoVo syncLocalDepartmentToThirdApp(String ids) { + SyncInfoVo syncInfo = new SyncInfoVo(); + String accessToken = this.getAccessToken(); + if (accessToken == null) { + syncInfo.addFailInfo("accessToken获取失败!"); + return syncInfo; + } + // 获取【钉钉】所有的部门 + List> departments = JdtDepartmentAPI.listAllResponse(accessToken); + // 删除钉钉有但本地没有的部门(以本地部门数据为主)(钉钉不能创建同名部门,只能先删除) + List sysDepartList = sysDepartService.list(); + for1: + for (Response departmentRes : departments) { + // 判断部门是否查询成功 + if (!departmentRes.isSuccess()) { + syncInfo.addFailInfo(departmentRes.getErrmsg()); + // 88 是 ip 不在白名单的错误码,如果遇到此错误码,后面的操作都可以不用进行了,因为肯定都是失败的 + if (new Integer(88).equals(departmentRes.getErrcode())) { + return syncInfo; + } + continue; + } + Department department = departmentRes.getResult(); + for (SysDepart depart : sysDepartList) { + // id相同,代表已存在,不删除 + String sourceIdentifier = department.getSource_identifier(); + if (sourceIdentifier != null && sourceIdentifier.equals(depart.getId())) { + continue for1; + } + } + // 循环到此说明本地没有,删除 + int deptId = department.getDept_id(); + // 钉钉不允许删除带有用户的部门,所以需要判断下,将有用户的部门的用户移动至根部门 + Response> userIdRes = JdtUserAPI.getUserListIdByDeptId(deptId, accessToken); + if (userIdRes.isSuccess() && userIdRes.getResult().size() > 0) { + for (String userId : userIdRes.getResult()) { + User updateUser = new User(); + updateUser.setUserid(userId); + updateUser.setDept_id_list(1); + JdtUserAPI.update(updateUser, accessToken); + } + } + JdtDepartmentAPI.delete(deptId, accessToken); + } + // 获取本地所有部门树结构 + List sysDepartsTree = sysDepartService.queryTreeList(); + // -- 钉钉不能创建新的顶级部门,所以新的顶级部门的parentId就为1 + Department parent = new Department(); + parent.setDept_id(1); + // 递归同步部门 + departments = JdtDepartmentAPI.listAllResponse(accessToken); + this.syncDepartmentRecursion(sysDepartsTree, departments, parent, accessToken, syncInfo); + return syncInfo; + } + + /** + * 递归同步部门到本地 + * @param sysDepartsTree + * @param departments + * @param parent + * @param accessToken + * @param syncInfo + */ + public void syncDepartmentRecursion(List sysDepartsTree, List> departments, Department parent, String accessToken, SyncInfoVo syncInfo) { + if (sysDepartsTree != null && sysDepartsTree.size() != 0) { + for1: + for (SysDepartTreeModel depart : sysDepartsTree) { + for (Response departmentRes : departments) { + // 判断部门是否查询成功 + if (!departmentRes.isSuccess()) { + syncInfo.addFailInfo(departmentRes.getErrmsg()); + continue; + } + Department department = departmentRes.getResult(); + // id相同,代表已存在,执行修改操作 + String sourceIdentifier = department.getSource_identifier(); + if (sourceIdentifier != null && sourceIdentifier.equals(depart.getId())) { + this.sysDepartToDtDepartment(depart, department, parent.getDept_id()); + Response response = JdtDepartmentAPI.update(department, accessToken); + if (response.isSuccess()) { + // 紧接着同步子级 + this.syncDepartmentRecursion(depart.getChildren(), departments, department, accessToken, syncInfo); + } + // 收集错误信息 + this.syncDepartCollectErrInfo(response, depart, syncInfo); + // 跳出外部循环 + continue for1; + } + } + // 循环到此说明是新部门,直接调接口创建 + Department newDepartment = this.sysDepartToDtDepartment(depart, parent.getDept_id()); + Response response = JdtDepartmentAPI.create(newDepartment, accessToken); + // 创建成功,将返回的id绑定到本地 + if (response.getResult() != null) { + Department newParent = new Department(); + newParent.setDept_id(response.getResult()); + // 紧接着同步子级 + this.syncDepartmentRecursion(depart.getChildren(), departments, newParent, accessToken, syncInfo); + } + // 收集错误信息 + this.syncDepartCollectErrInfo(response, depart, syncInfo); + } + } + } + +// @Override +// public SyncInfoVo syncThirdAppDepartmentToLocal(String ids) { +// SyncInfoVo syncInfo = new SyncInfoVo(); +// String accessToken = this.getAccessToken(); +// if (accessToken == null) { +// syncInfo.addFailInfo("accessToken获取失败!"); +// return syncInfo; +// } +// // 获取【钉钉】所有的部门 +// List departments = JdtDepartmentAPI.listAll(accessToken); +// String username = JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()); +// List departmentTreeList = JdtDepartmentTreeVo.listToTree(departments); +// // 递归同步部门 +// this.syncDepartmentToLocalRecursion(departmentTreeList, null, username, syncInfo, accessToken,false); +// return syncInfo; +// } + + public void syncDepartmentToLocalRecursion(List departmentTreeList, String sysParentId, String username, SyncInfoVo syncInfo, String accessToken,Boolean syncUser,Integer tenantId) { + + if (departmentTreeList != null && departmentTreeList.size() != 0) { + // 记录已经同步过的用户id,当有多个部门的情况时,只同步一次 + Set syncedUserIdSet = new HashSet<>(); + for (JdtDepartmentTreeVo departmentTree : departmentTreeList) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + // 根据 source_identifier 字段查询 + // 代码逻辑说明: 【issues/6017】钉钉同步部门时没有最顶层的部门名,同步用户时,用户没有部门信息--- + queryWrapper.and(item -> item.eq(SysDepart::getId, departmentTree.getSource_identifier()).or().eq(SysDepart::getDingIdentifier,oConvertUtils.getString(departmentTree.getDept_id()))); + SysDepart sysDepart = sysDepartService.getOne(queryWrapper); + if (sysDepart != null) { + // 执行更新操作 + SysDepart updateSysDepart = this.dtDepartmentToSysDepart(departmentTree, sysDepart); + if (sysParentId != null) { + updateSysDepart.setParentId(sysParentId); + //更新父级部门不是叶子结点 + sysDepartService.updateIzLeaf(sysParentId,CommonConstant.NOT_LEAF); + } + try { + sysDepartService.updateDepartDataById(updateSysDepart, username); + String str = String.format("部门 %s 更新成功!", updateSysDepart.getDepartName()); + syncInfo.addSuccessInfo(str); + } catch (Exception e) { + this.syncDepartCollectErrInfo(e, departmentTree, syncInfo); + } + if (departmentTree.hasChildren()) { + // 紧接着同步子级 + this.syncDepartmentToLocalRecursion(departmentTree.getChildren(), updateSysDepart.getId(), username, syncInfo, accessToken,syncUser,tenantId); + } + //判断是否需要同步用户 + if(syncUser){ + this.addDepartUser(updateSysDepart.getId(),departmentTree.getDept_id(), accessToken, syncInfo, syncedUserIdSet,tenantId); + } + } else { + // 执行新增操作 + SysDepart newSysDepart = this.dtDepartmentToSysDepart(departmentTree, null); + if (sysParentId != null) { + newSysDepart.setParentId(sysParentId); + // 2 = 组织机构 + newSysDepart.setOrgCategory("2"); + } else { + // 1 = 公司 + newSysDepart.setOrgCategory("1"); + } + try { + if(oConvertUtils.isEmpty(departmentTree.getParent_id())){ + newSysDepart.setDingIdentifier(departmentTree.getDept_id().toString()); + } + newSysDepart.setTenantId(tenantId); + sysDepartService.saveDepartData(newSysDepart, username); + // 更新钉钉 source_identifier + Department updateDtDepart = new Department(); + updateDtDepart.setDept_id(departmentTree.getDept_id()); + updateDtDepart.setSource_identifier(newSysDepart.getId()); + //为空说明是最顶级部门,最顶级部门不允许修改操作 + if(oConvertUtils.isNotEmpty(newSysDepart.getParentId())){ + Response response = JdtDepartmentAPI.update(updateDtDepart, accessToken); + if (!response.isSuccess()) { + throw new RuntimeException(response.getErrmsg()); + } + } + String str = String.format("部门 %s 创建成功!", newSysDepart.getDepartName()); + syncInfo.addSuccessInfo(str); + //判断是否需要同步用户 + if(syncUser){ + this.addDepartUser(newSysDepart.getId(),departmentTree.getDept_id(), accessToken, syncInfo, syncedUserIdSet,tenantId); + } + } catch (Exception e) { + this.syncDepartCollectErrInfo(e, departmentTree, syncInfo); + } + // 紧接着同步子级 + if (departmentTree.hasChildren()) { + this.syncDepartmentToLocalRecursion(departmentTree.getChildren(), newSysDepart.getId(), username, syncInfo, accessToken,syncUser,tenantId); + } + } + } + } + } + + private boolean syncDepartCollectErrInfo(Exception e, Department department, SyncInfoVo syncInfo) { + String msg; + if (e instanceof DuplicateKeyException) { + msg = e.getCause().getMessage(); + } else { + msg = e.getMessage(); + } + String str = String.format("部门 %s(%s) 同步失败!错误信息:%s", department.getName(), department.getDept_id(), msg); + syncInfo.addFailInfo(str); + return false; + } + + /** + * 【同步部门】收集同步过程中的错误信息 + */ + private boolean syncDepartCollectErrInfo(Response response, SysDepartTreeModel depart, SyncInfoVo syncInfo) { + if (!response.isSuccess()) { + String str = String.format("部门 %s(%s) 同步失败!错误码:%s——%s", depart.getDepartName(), depart.getOrgCode(), response.getErrcode(), response.getErrmsg()); + syncInfo.addFailInfo(str); + return false; + } else { + String str = String.format("部门户 %s(%s) 同步成功!", depart.getDepartName(), depart.getOrgCode()); + syncInfo.addSuccessInfo(str); + return true; + } + } + + @Override + public SyncInfoVo syncLocalUserToThirdApp(String ids) { + SyncInfoVo syncInfo = new SyncInfoVo(); + String accessToken = this.getAccessToken(); + if (accessToken == null) { + syncInfo.addFailInfo("accessToken获取失败!"); + return syncInfo; + } + List sysUsers; + if (StringUtils.isNotBlank(ids)) { + String[] idList = ids.split(","); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getId, (Object[]) idList); + // 获取本地指定用户 + sysUsers = userMapper.selectList(queryWrapper); + } else { + // 获取本地所有用户 + sysUsers = userMapper.selectList(Wrappers.emptyWrapper()); + } + if (CollectionUtils.isEmpty(sysUsers)) { + return syncInfo; + } + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + List userIds = sysUsers.stream().map(SysUser::getId).collect(Collectors.toList()); + // ① 批量预加载 sys_third_account → Map + Map thirdAccountMap = sysThirdAccountService + .listBySysUserIds(userIds, THIRD_TYPE) + .stream() + .collect(Collectors.toMap(SysThirdAccount::getSysUserId, a -> a, (a, b) -> a)); + // ② 批量预加载用户-部门关系 → Map> + LambdaQueryWrapper udQw = new LambdaQueryWrapper<>(); + udQw.in(SysUserDepart::getUserId, userIds); + Map> userDepartIdsMap = sysUserDepartService.list(udQw) + .stream() + .collect(Collectors.groupingBy( + SysUserDepart::getUserId, + Collectors.mapping(SysUserDepart::getDepId, Collectors.toList()) + )); + // ③ 批量预加载所有涉及的部门 → Map + Set allDepartIds = userDepartIdsMap.values().stream() + .flatMap(Collection::stream).collect(Collectors.toSet()); + Map departMap = Collections.emptyMap(); + if (!allDepartIds.isEmpty()) { + departMap = sysDepartService.listByIds(allDepartIds) + .stream() + .collect(Collectors.toMap(SysDepart::getId, d -> d, (a, b) -> a)); + } + // ④ 批量预加载职位 → Map> + Map> positionMap = sysPositionService + .getPositionListByUserIds(userIds) + .stream() + .collect(Collectors.groupingBy(SysPositionVO::getUserId)); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + + // 查询钉钉所有的部门,用于同步用户和部门的关系 + List allDepartment = JdtDepartmentAPI.listAll(accessToken); + + for (SysUser sysUser : sysUsers) { + // 外部模拟登陆临时账号,不同步 + if ("_reserve_user_external".equals(sysUser.getUsername())) { + continue; + } + // 钉钉用户信息,不为null代表已同步过 + Response dtUserInfo; + /* + * 判断是否同步过的逻辑: + * 1. 查询 sys_third_account(第三方账号表)是否有数据,如果有代表已同步 + * 2. 本地表里没有,就先用手机号判断,不通过再用username(用户账号)判断。 + */ + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + SysThirdAccount sysThirdAccount = thirdAccountMap.get(sysUser.getId()); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + if (sysThirdAccount != null && oConvertUtils.isNotEmpty(sysThirdAccount.getThirdUserId())) { + // sys_third_account 表匹配成功,通过第三方userId查询出第三方userInfo + dtUserInfo = JdtUserAPI.getUserById(sysThirdAccount.getThirdUserId(), accessToken); + } else { + // 手机号匹配 + Response thirdUserId = JdtUserAPI.getUseridByMobile(sysUser.getPhone(), accessToken); + // 手机号匹配成功 + if (thirdUserId.isSuccess() && oConvertUtils.isNotEmpty(thirdUserId.getResult())) { + // 通过查询到的userId查询用户详情 + dtUserInfo = JdtUserAPI.getUserById(thirdUserId.getResult(), accessToken); + } else { + // 手机号匹配失败,尝试使用username匹配 + dtUserInfo = JdtUserAPI.getUserById(sysUser.getUsername(), accessToken); + } + } + String dtUserId; + // api 接口是否执行成功 + boolean apiSuccess; + // 已同步就更新,否则就创建 + if (dtUserInfo != null && dtUserInfo.isSuccess() && dtUserInfo.getResult() != null) { + User dtUser = dtUserInfo.getResult(); + dtUserId = dtUser.getUserid(); + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + User updateQwUser = this.sysUserToDtUser(sysUser, dtUser, allDepartment, userDepartIdsMap, departMap, positionMap); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + Response updateRes = JdtUserAPI.update(updateQwUser, accessToken); + // 收集成功/失败信息 + apiSuccess = this.syncUserCollectErrInfo(updateRes, sysUser, syncInfo); + } else { + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + User newQwUser = this.sysUserToDtUser(sysUser, allDepartment, userDepartIdsMap, departMap, positionMap); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + Response createRes = JdtUserAPI.create(newQwUser, accessToken); + dtUserId = createRes.getResult(); + // 收集成功/失败信息 + apiSuccess = this.syncUserCollectErrInfo(createRes, sysUser, syncInfo); + } + + // api 接口执行成功,并且 sys_third_account 表匹配失败,就向 sys_third_account 里插入一条数据 + boolean flag = (sysThirdAccount == null || oConvertUtils.isEmpty(sysThirdAccount.getThirdUserId())); + if (apiSuccess && flag) { + if (sysThirdAccount == null) { + sysThirdAccount = new SysThirdAccount(); + sysThirdAccount.setSysUserId(sysUser.getId()); + sysThirdAccount.setStatus(1); + sysThirdAccount.setDelFlag(0); + sysThirdAccount.setThirdType(THIRD_TYPE); + } + // 设置第三方app用户ID + sysThirdAccount.setThirdUserId(dtUserId); + sysThirdAccountService.saveOrUpdate(sysThirdAccount); + } + } + return syncInfo; + } + +// @Override +// public SyncInfoVo syncThirdAppUserToLocal() { +// SyncInfoVo syncInfo = new SyncInfoVo(); +// String accessToken = this.getAccessToken(); +// if (accessToken == null) { +// syncInfo.addFailInfo("accessToken获取失败!"); +// return syncInfo; +// } +// +// // 获取本地用户 +// List sysUsersList = userMapper.selectList(Wrappers.emptyWrapper()); +// +// // 查询钉钉所有的部门,用于同步用户和部门的关系 +// List allDepartment = JdtDepartmentAPI.listAll(accessToken); +// // 根据钉钉部门查询所有钉钉用户,用于反向同步到本地 +// List ddUserList = this.getDtAllUserByDepartment(allDepartment, accessToken); +// // 记录已经同步过的用户id,当有多个部门的情况时,只同步一次 +// Set syncedUserIdSet = new HashSet<>(); +// +// for (User dtUserInfo : ddUserList) { +// if (syncedUserIdSet.contains(dtUserInfo.getUserid())) { +// continue; +// } +// syncedUserIdSet.add(dtUserInfo.getUserid()); +// SysThirdAccount sysThirdAccount = sysThirdAccountService.getOneByThirdUserId(dtUserInfo.getUserid(), THIRD_TYPE); +// List collect = sysUsersList.stream().filter(user -> (dtUserInfo.getMobile().equals(user.getPhone()) || dtUserInfo.getUserid().equals(user.getUsername())) +// ).collect(Collectors.toList()); +// if (collect != null && collect.size() > 0) { +// SysUser sysUserTemp = collect.get(0); +// // 循环到此说明用户匹配成功,进行更新操作 +// SysUser updateSysUser = this.dtUserToSysUser(dtUserInfo, sysUserTemp); +// try { +// userMapper.updateById(updateSysUser); +// String str = String.format("用户 %s(%s) 更新成功!", updateSysUser.getRealname(), updateSysUser.getUsername()); +// syncInfo.addSuccessInfo(str); +// } catch (Exception e) { +// this.syncUserCollectErrInfo(e, dtUserInfo, syncInfo); +// } +// //第三方账号关系表 +// this.thirdAccountSaveOrUpdate(sysThirdAccount, updateSysUser.getId(), dtUserInfo.getUserid()); +// }else{ +// // 如果没有匹配到用户,则走创建逻辑 +// SysUser newSysUser = this.dtUserToSysUser(dtUserInfo); +// try { +// userMapper.insert(newSysUser); +// String str = String.format("用户 %s(%s) 创建成功!", newSysUser.getRealname(), newSysUser.getUsername()); +// syncInfo.addSuccessInfo(str); +// } catch (Exception e) { +// this.syncUserCollectErrInfo(e, dtUserInfo, syncInfo); +// } +// //第三方账号关系表 +// this.thirdAccountSaveOrUpdate(null, newSysUser.getId(), dtUserInfo.getUserid()); +// } +// } +// return syncInfo; +// } + +// private List getDtAllUserByDepartment(List allDepartment, String accessToken) { +// // 根据钉钉部门查询所有钉钉用户,用于反向同步到本地 +// List userList = new ArrayList<>(); +// for (Department department : allDepartment) { +// this.getUserListByDeptIdRecursion(department.getDept_id(), 0, userList, accessToken); +// } +// return userList; +// } + + /** + * 递归查询所有用户 + */ + private void getUserListByDeptIdRecursion(int deptId, int cursor, List userList, String accessToken) { + // 根据钉钉部门查询所有钉钉用户,用于反向同步到本地 + GetUserListBody getUserListBody = new GetUserListBody(deptId, cursor, 100); + Response> response = JdtUserAPI.getUserListByDeptId(getUserListBody, accessToken); + if (response.isSuccess()) { + PageResult page = response.getResult(); + userList.addAll(page.getList()); + if (page.getHas_more()) { + this.getUserListByDeptIdRecursion(deptId, page.getNext_cursor(), userList, accessToken); + } + } + } + + /** + * 保存或修改第三方登录表 + * + * @param sysThirdAccount 第三方账户表对象,为null就新增数据,否则就修改 + * @param sysUserId 本地系统用户ID + * @param user 钉钉用户 + */ + private void thirdAccountSaveOrUpdate(SysThirdAccount sysThirdAccount, String sysUserId, User user, Integer tenantId) { + if (sysThirdAccount == null) { + sysThirdAccount = new SysThirdAccount(); + sysThirdAccount.setSysUserId(sysUserId); + sysThirdAccount.setThirdUserUuid(user.getUnionid()); + sysThirdAccount.setStatus(1); + sysThirdAccount.setTenantId(tenantId); + sysThirdAccount.setDelFlag(0); + sysThirdAccount.setThirdType(THIRD_TYPE); + } + sysThirdAccount.setThirdUserId(user.getUserid()); + if(oConvertUtils.isEmpty(sysThirdAccount.getRealname())){ + sysThirdAccount.setRealname(user.getName()); + } + sysThirdAccountService.saveOrUpdate(sysThirdAccount); + } + + /** + * 【同步用户】收集同步过程中的错误信息 + */ + private boolean syncUserCollectErrInfo(Response response, SysUser sysUser, SyncInfoVo syncInfo) { + if (!response.isSuccess()) { + String str = String.format("用户 %s(%s) 同步失败!错误码:%s——%s", sysUser.getUsername(), sysUser.getRealname(), response.getErrcode(), response.getErrmsg()); + syncInfo.addFailInfo(str); + return false; + } else { + String str = String.format("用户 %s(%s) 同步成功!", sysUser.getUsername(), sysUser.getRealname()); + syncInfo.addSuccessInfo(str); + return true; + } + } + + /** + * 【同步用户】收集同步过程中的错误信息 + */ + private boolean syncUserCollectErrInfo(Exception e, User dtUser, SyncInfoVo syncInfo) { + String msg; + if (e instanceof DuplicateKeyException) { + msg = e.getCause().getMessage(); + String emailUniq = "uniq_sys_user_email"; + if(msg.contains(emailUniq)){ + msg = "邮箱重复,请更换邮箱"; + } + String workNoUniq="uniq_sys_user_work_no"; + if(msg.contains(workNoUniq)){ + msg = "工号重复,请更换工号"; + } + } else { + msg = e.getMessage(); + } + String str = String.format("用户 %s(%s) 同步失败!错误信息:%s", dtUser.getUserid(), dtUser.getName(), msg); + syncInfo.addFailInfo(str); + return false; + } + + + /** + * 【同步用户】将SysUser转为【钉钉】的User对象(创建新用户) + */ + private User sysUserToDtUser(SysUser sysUser, List allDepartment) { + User user = new User(); + // 通过 username 来关联 + user.setUserid(sysUser.getUsername()); + return this.sysUserToDtUser(sysUser, user, allDepartment); + } + + /** + * 【同步用户】将SysUser转为【钉钉】的User对象(更新旧用户) + */ + private User sysUserToDtUser(SysUser sysUser, User user, List allDepartment) { + user.setName(sysUser.getRealname()); + user.setMobile(sysUser.getPhone()); + user.setTelephone(sysUser.getTelephone()); + user.setJob_number(sysUser.getWorkNo()); + // 职务翻译 + //获取用户职位名称 + List positionList = sysPositionService.getPositionList(sysUser.getId()); + if(null != positionList && positionList.size()>0){ + String positionName = positionList.stream().map(SysPosition::getName).collect(Collectors.joining(SymbolConstant.COMMA)); + user.setTitle(positionName); + } + user.setEmail(sysUser.getEmail()); + // 查询并同步用户部门关系 + List departList = this.getUserDepart(sysUser); + if (departList != null) { + List departmentIdList = new ArrayList<>(); + for (SysDepart sysDepart : departList) { + // 企业微信的部门id + Department department = this.getDepartmentByDepartId(sysDepart.getId(), allDepartment); + if (department != null) { + departmentIdList.add(department.getDept_id()); + } + } + user.setDept_id_list(departmentIdList.toArray(new Integer[]{})); + user.setDept_order_list(null); + } + if (oConvertUtils.isEmpty(user.getDept_id_list())) { + // 没有找到匹配部门,同步到根部门下 + user.setDept_id_list(1); + user.setDept_order_list(null); + } + // --- 钉钉没有逻辑删除功能 + // sysUser.getDelFlag() + // --- 钉钉没有冻结、启用禁用功能 + // sysUser.getStatus() + return user; + } + + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + /** + * 【同步用户】将SysUser转为【钉钉】的User对象(创建新用户,使用批量预加载Map,消除N+1查询) + */ + private User sysUserToDtUser(SysUser sysUser, List allDepartment, + Map> userDepartIdsMap, Map departMap, + Map> positionMap) { + User user = new User(); + user.setUserid(sysUser.getUsername()); + return this.sysUserToDtUser(sysUser, user, allDepartment, userDepartIdsMap, departMap, positionMap); + } + + /** + * 【同步用户】将SysUser转为【钉钉】的User对象(更新旧用户,使用批量预加载Map,消除N+1查询) + */ + private User sysUserToDtUser(SysUser sysUser, User user, List allDepartment, + Map> userDepartIdsMap, Map departMap, + Map> positionMap) { + user.setName(sysUser.getRealname()); + user.setMobile(sysUser.getPhone()); + user.setTelephone(sysUser.getTelephone()); + user.setJob_number(sysUser.getWorkNo()); + // 职务翻译(使用预加载Map替代单次查询) + List positionList = positionMap.getOrDefault(sysUser.getId(), Collections.emptyList()); + if (!positionList.isEmpty()) { + String positionName = positionList.stream().map(SysPositionVO::getName).collect(Collectors.joining(SymbolConstant.COMMA)); + user.setTitle(positionName); + } + user.setEmail(sysUser.getEmail()); + // 查询并同步用户部门关系(使用预加载Map替代单次查询) + List departList = this.getUserDepart(sysUser, userDepartIdsMap, departMap); + if (departList != null) { + List departmentIdList = new ArrayList<>(); + for (SysDepart sysDepart : departList) { + Department department = this.getDepartmentByDepartId(sysDepart.getId(), allDepartment); + if (department != null) { + departmentIdList.add(department.getDept_id()); + } + } + user.setDept_id_list(departmentIdList.toArray(new Integer[]{})); + user.setDept_order_list(null); + } + if (oConvertUtils.isEmpty(user.getDept_id_list())) { + user.setDept_id_list(1); + user.setDept_order_list(null); + } + return user; + } + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + + + /** + * 【同步用户】将【钉钉】的User对象转为SysUser(创建新用户) + */ + private SysUser dtUserToSysUser(User dtUser) { + SysUser sysUser = new SysUser(); + sysUser.setDelFlag(0); + // 通过 username 来关联 + sysUser.setUsername(dtUser.getMobile()); + // 密码默认为为手机号加门牌号,随机加盐 + String password = "", salt = oConvertUtils.randomGen(8); + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + if(tenantId>0){ + SysTenant tenant = tenantMapper.selectById(tenantId); + password = tenant.getHouseNumber()+dtUser.getMobile(); + }else{ + password = dtUser.getMobile(); + } + String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(), password, salt); + sysUser.setSalt(salt); + sysUser.setPassword(passwordEncode); + // 代码逻辑说明: 钉钉同步到本地的人员没有状态,导致同步之后无法登录 #I3ZC2L + sysUser.setStatus(1); + sysUser.setLastPwdUpdateTime(new Date()); + return this.dtUserToSysUser(dtUser, sysUser); + } + + /** + * 【同步用户】将【钉钉】的User对象转为SysUser(更新旧用户) + */ + private SysUser dtUserToSysUser(User dtUser, SysUser oldSysUser) { + SysUser sysUser = new SysUser(); + BeanUtils.copyProperties(oldSysUser, sysUser); + sysUser.setTelephone(dtUser.getTelephone()); + //如果真实姓名为空的情况下,才会改真实姓名 + if(oConvertUtils.isEmpty(oldSysUser.getRealname())){ + sysUser.setRealname(dtUser.getName()); + } + // 因为唯一键约束的原因,如果原数据和旧数据相同,就不更新 + if (oConvertUtils.isNotEmpty(dtUser.getEmail()) && !dtUser.getEmail().equals(sysUser.getEmail())) { + sysUser.setEmail(dtUser.getEmail()); + } else { + sysUser.setEmail(null); + } + // 因为唯一键约束的原因,如果原数据和旧数据相同,就不更新 + if (oConvertUtils.isNotEmpty(dtUser.getMobile()) && !dtUser.getMobile().equals(sysUser.getPhone())) { + sysUser.setPhone(dtUser.getMobile()); + } else { + sysUser.setPhone(null); + } + // 设置工号,如果工号为空,则使用username + if (oConvertUtils.isEmpty(dtUser.getJob_number())) { + sysUser.setWorkNo(dtUser.getUserid()); + } else { + sysUser.setWorkNo(dtUser.getJob_number()); + } + // --- 钉钉没有逻辑删除功能 + // sysUser.getDelFlag() + // --- 钉钉没有冻结、启用禁用功能 + // sysUser.getStatus() + return sysUser; + } + + + /** + * 查询用户和部门的关系 + */ + private List getUserDepart(SysUser sysUser) { + // 根据用户部门关系表查询出用户的部门 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysUserDepart::getUserId, sysUser.getId()); + List sysUserDepartList = sysUserDepartService.list(queryWrapper); + if (sysUserDepartList.size() == 0) { + return null; + } + // 根据用户部门 + LambdaQueryWrapper departQueryWrapper = new LambdaQueryWrapper<>(); + List departIdList = sysUserDepartList.stream().map(SysUserDepart::getDepId).collect(Collectors.toList()); + departQueryWrapper.in(SysDepart::getId, departIdList); + List departList = sysDepartService.list(departQueryWrapper); + return departList.size() == 0 ? null : departList; + } + + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + /** + * 查询用户和部门的关系(使用批量预加载Map,消除N+1查询) + */ + private List getUserDepart(SysUser sysUser, Map> userDepartIdsMap, + Map departMap) { + List departIds = userDepartIdsMap.get(sysUser.getId()); + if (departIds == null || departIds.isEmpty()) { + return null; + } + List departList = departIds.stream() + .map(departMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return departList.isEmpty() ? null : departList; + } + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + + /** + * 根据sysDepartId查询钉钉的部门 + */ + private Department getDepartmentByDepartId(String departId, List allDepartment) { + for (Department department : allDepartment) { + if (departId.equals(department.getSource_identifier())) { + return department; + } + } + return null; + } + + + /** + * 【同步部门】将SysDepartTreeModel转为【钉钉】的Department对象(创建新部门) + */ + private Department sysDepartToDtDepartment(SysDepartTreeModel departTree, Integer parentId) { + Department department = new Department(); + department.setSource_identifier(departTree.getId()); + return this.sysDepartToDtDepartment(departTree, department, parentId); + } + + /** + * 【同步部门】将SysDepartTreeModel转为【钉钉】的Department对象 + */ + private Department sysDepartToDtDepartment(SysDepartTreeModel departTree, Department department, Integer parentId) { + department.setName(departTree.getDepartName()); + department.setParent_id(parentId); + department.setOrder(departTree.getDepartOrder()); + return department; + } + + + /** + * 【同步部门】将【钉钉】的Department对象转为SysDepartTreeModel + */ + private SysDepart dtDepartmentToSysDepart(Department department, SysDepart departTree) { + SysDepart sysDepart = new SysDepart(); + if (departTree != null) { + BeanUtils.copyProperties(departTree, sysDepart); + } + sysDepart.setDepartName(department.getName()); + sysDepart.setDepartOrder(department.getOrder()); + sysDepart.setDingIdentifier(department.getSource_identifier()); + return sysDepart; + } + + @Override + public int removeThirdAppUser(List userIdList) { + // 判断启用状态 + SysThirdAppConfig appConfig = getDingThirdAppConfig(); + if (null == appConfig) { + return -1; + } + int count = 0; + if (userIdList != null && userIdList.size() > 0) { + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + String accessToken = this.getTenantAccessToken(appConfig); + if (accessToken == null) { + return count; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAccount::getThirdType, THIRD_TYPE); + queryWrapper.in(SysThirdAccount::getSysUserId, userIdList); + // 根据userId,获取第三方用户的id + List thirdAccountList = sysThirdAccountService.list(queryWrapper); + List thirdUserIdList = thirdAccountList.stream().map(SysThirdAccount::getThirdUserId).collect(Collectors.toList()); + + for (String thirdUserId : thirdUserIdList) { + if (oConvertUtils.isNotEmpty(thirdUserId)) { + // 没有批量删除的接口 + Response response = JdtUserAPI.delete(thirdUserId, accessToken); + if (response.getErrcode() == 0) { + count++; + } + } + } + } + return count; + + } + + @Override + public boolean sendMessage(MessageDTO message) { + return this.sendMessage(message, false); + } + + /** + * 发送消息 + * + * @param message + * @param verifyConfig + * @return + */ + @Override + public boolean sendMessage(MessageDTO message, boolean verifyConfig) { + Response response; + if (message.isMarkdown()) { + response = this.sendMarkdownResponse(message, verifyConfig); + } else { + response = this.sendMessageResponse(message, verifyConfig); + } + if (response != null) { + return response.isSuccess(); + } + return false; + } + + /** + * 发送Markdown消息 + * @param message + * @param verifyConfig + * @return + */ + public Response sendMarkdownResponse(MessageDTO message, boolean verifyConfig) { + SysThirdAppConfig config = this.getDingThirdAppConfig(); + if (verifyConfig && null == config) { + return null; + } + String accessToken = this.getAccessToken(); + if (accessToken == null) { + return null; + } + // 封装钉钉消息 + String title = message.getTitle(); + String content = message.getContent(); + String agentId = config.getAgentId(); + Message mdMessage = new Message<>(agentId, new MarkdownMessage(title, content)); + if (message.getToAll()) { + mdMessage.setTo_all_user(true); + } else { + String[] toUsers = message.getToUser().split(","); + // 通过第三方账号表查询出第三方userId + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), CommonConstant.TENANT_ID_DEFAULT_VALUE); + List thirdAccountList = sysThirdAccountService.listThirdUserIdByUsername(toUsers, THIRD_TYPE,tenantId); + List dtUserIds = thirdAccountList.stream().map(SysThirdAccount::getThirdUserId).collect(Collectors.toList()); + mdMessage.setUserid_list(dtUserIds); + } + return JdtMessageAPI.sendMarkdownMessage(mdMessage, accessToken); + } + + public Response sendMessageResponse(MessageDTO message, boolean verifyConfig) { + SysThirdAppConfig config = this.getDingThirdAppConfig(); + if (verifyConfig && null == config) { + return null; + } + String accessToken = this.getAccessToken(); + if (accessToken == null) { + return null; + } + // 封装钉钉消息 + String content = message.getContent(); + String agentId = config.getAgentId(); + Message textMessage = new Message<>(agentId, new TextMessage(content)); + if (message.getToAll()) { + textMessage.setTo_all_user(true); + } else { + String[] toUsers = message.getToUser().split(","); + // 通过第三方账号表查询出第三方userId + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), CommonConstant.TENANT_ID_DEFAULT_VALUE); + List thirdAccountList = sysThirdAccountService.listThirdUserIdByUsername(toUsers, THIRD_TYPE, tenantId); + List dtUserIds = thirdAccountList.stream().map(SysThirdAccount::getThirdUserId).collect(Collectors.toList()); + textMessage.setUserid_list(dtUserIds); + } + return JdtMessageAPI.sendTextMessage(textMessage, accessToken); + } + + public boolean recallMessage(String msgTaskId) { + Response response = this.recallMessageResponse(msgTaskId); + if (response == null) { + return false; + } + return response.isSuccess(); + } + + /** + * 撤回消息 + * + * @param msgTaskId + * @return + */ + public Response recallMessageResponse(String msgTaskId) { + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + SysThirdAppConfig config = this.getDingThirdAppConfig(); + String accessToken = this.getTenantAccessToken(config); + if (accessToken == null) { + return null; + } + String agentId = config.getAgentId(); + return JdtMessageAPI.recallMessage(agentId, msgTaskId, accessToken); + } + + /** + * 发送卡片消息(SysAnnouncement定制) + * + * @param announcement + * @param ddMobileUrl 钉钉打开网页地址 + * @param verifyConfig 是否验证配置(未启用的APP会拒绝发送) + * @return + */ + public Response sendActionCardMessage(SysAnnouncement announcement, String ddMobileUrl, boolean verifyConfig) { + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + SysThirdAppConfig config = this.getDingThirdAppConfig(); + if (verifyConfig && null == config) { + return null; + } + String accessToken = this.getTenantAccessToken(config); + if (accessToken == null) { + return null; + } + String agentId = config.getAgentId(); + String emptySuffix = null; + if (oConvertUtils.isNotEmpty(announcement.getMsgAbstract())) { + String msgAbstract = announcement.getMsgAbstract().trim(); + log.info("获取钉钉通知参数,msgAbstract: {}", msgAbstract); + if (msgAbstract.startsWith("{") && msgAbstract.endsWith("}")) { + //如果摘要存的是业务扩展参数json,则取公告内容 + emptySuffix = announcement.getMsgContent(); + } else { + //如果摘要不为空且是文本格式,则使用摘要 + emptySuffix = msgAbstract; + } + } else { + emptySuffix = "空"; + } + + String markdown = "### " + announcement.getTitile() + "\n" + emptySuffix; + log.info("钉钉推送参数, markdown: {}", markdown); + ActionCardMessage actionCard = new ActionCardMessage(markdown); + actionCard.setTitle(announcement.getTitile()); + actionCard.setSingle_title("详情"); + String baseUrl = null; + //优先通过请求获取basepath,获取不到读取 Ghb.domainUrl.pc + try { + baseUrl = RestUtil.getBaseUrl(); + } catch (Exception e) { + log.warn(e.getMessage()); + baseUrl = GhbBaseConfig.getDomainUrl().getPc(); + //e.printStackTrace(); + } + + log.info("获取钉钉打开网页地址,参数 ddMobileUrl: {}", ddMobileUrl); + String ddSingleUrl = null; + if (oConvertUtils.isNotEmpty(ddMobileUrl)) { + ddSingleUrl = ddMobileUrl; + } else { + ddSingleUrl = baseUrl + "/sys/annountCement/show/" + announcement.getId(); + } + actionCard.setSingle_url(ddSingleUrl); + log.info("获取钉钉打开网页地址,最终地址 ddSingleUrl: {}", ddSingleUrl); + + Message actionCardMessage = new Message<>(agentId, actionCard); + if (CommonConstant.MSG_TYPE_ALL.equals(announcement.getMsgType())) { + actionCardMessage.setTo_all_user(true); + return JdtMessageAPI.sendActionCardMessage(actionCardMessage, accessToken); + } else { + // 将userId转为username + String[] userIds = null; + String userId = announcement.getUserIds(); + if(oConvertUtils.isNotEmpty(userId)){ + userIds = userId.substring(0, (userId.length() - 1)).split(","); + }else{ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysAnnouncementSend::getAnntId, announcement.getId()); + SysAnnouncementSend sysAnnouncementSend = sysAnnouncementSendMapper.selectOne(queryWrapper); + userIds = new String[] {sysAnnouncementSend.getUserId()}; + } + + if(userIds!=null){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getId, userIds); + List userList = userMapper.selectList(queryWrapper); + String[] usernameList = userList.stream().map(SysUser::getUsername).toArray(String[] :: new); + + // 通过第三方账号表查询出第三方userId + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), CommonConstant.TENANT_ID_DEFAULT_VALUE); + List thirdAccountList = sysThirdAccountService.listThirdUserIdByUsername(usernameList, THIRD_TYPE, tenantId); + List dtUserIds = thirdAccountList.stream().map(SysThirdAccount::getThirdUserId).collect(Collectors.toList()); + actionCardMessage.setUserid_list(dtUserIds); + return JdtMessageAPI.sendActionCardMessage(actionCardMessage, accessToken); + } + } + return null; + } + + /** + * OAuth2登录,成功返回登录的SysUser,失败返回null + */ + public SysUser oauth2Login(String authCode,Integer tenantId) { + this.tenantIzExist(tenantId); + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + SysThirdAppConfig dtConfig = configMapper.getThirdConfigByThirdType(tenantId, MessageTypeEnum.DD.getType()); + // 1. 根据免登授权码获取用户 AccessToken + String userAccessToken = JdtOauth2API.getUserAccessToken(dtConfig.getClientId(), dtConfig.getClientSecret(), authCode); + if (userAccessToken == null) { + log.error("oauth2Login userAccessToken is null"); + throw new GhbBootException("请查看应用key和应用秘钥是否正确,组织ID是否匹配"); + } + // 2. 根据用户 AccessToken 获取当前用户的基本信息(不包括userId) + ContactUser contactUser = JdtOauth2API.getContactUsers("me", userAccessToken); + if (contactUser == null) { + log.error("oauth2Login contactUser is null"); + throw new GhbBootException("获取钉钉用户信息失败"); + } + String unionId = contactUser.getUnionId(); + // 3. 根据获取到的 unionId 换取用户 userId + String accessToken = this.getTenantAccessToken(dtConfig); + if (accessToken == null) { + log.error("oauth2Login accessToken is null"); + throw new GhbBootException("请查看应用key和应用秘钥是否正确,组织ID是否匹配"); + } + Response getUserIdRes = JdtUserAPI.getUseridByUnionid(unionId, accessToken); + if (!getUserIdRes.isSuccess()) { + log.error("oauth2Login getUseridByUnionid failed: " + JSON.toJSONString(getUserIdRes)); + throw new GhbBootException("获取钉钉用户信息失败"); + } + String appUserId = getUserIdRes.getResult(); + log.info("appUserId: " + appUserId); + if (appUserId != null) { + // 判断第三方用户表有没有这个人 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAccount::getThirdType, THIRD_TYPE); + queryWrapper.eq(SysThirdAccount::getTenantId, tenantId); + // 代码逻辑说明: auth登录需要联查一下--- + queryWrapper.and((wrapper)->wrapper.eq(SysThirdAccount::getThirdUserUuid,appUserId).or().eq(SysThirdAccount::getThirdUserId,appUserId)); + SysThirdAccount thirdAccount = sysThirdAccountService.getOne(queryWrapper); + if (thirdAccount != null) { + return this.getSysUserByThird(thirdAccount, null, appUserId, accessToken,tenantId); + } else { + // 直接创建新账号 + User appUser = JdtUserAPI.getUserById(appUserId, accessToken).getResult(); + //代码逻辑说明: [QQYUN-4883]钉钉auth登录同一个租户下有同一个用户id------------ + //应该存uuid + ThirdLoginModel tlm = new ThirdLoginModel(THIRD_TYPE, appUser.getUnionid(), appUser.getName(), appUser.getAvatar()); + thirdAccount = sysThirdAccountService.saveThirdUser(tlm,tenantId); + return this.getSysUserByThird(thirdAccount, appUser, null, null,tenantId); + } + } + return null; + } + + /** + * 根据第三方账号获取本地账号,如果不存在就创建 + * + * @param thirdAccount + * @param appUser + * @param appUserId + * @param accessToken + * @param tenantId + * @return + */ + private SysUser getSysUserByThird(SysThirdAccount thirdAccount, User appUser, String appUserId, String accessToken, Integer tenantId) { + String sysUserId = thirdAccount.getSysUserId(); + if (oConvertUtils.isNotEmpty(sysUserId)) { + return userMapper.selectById(sysUserId); + } else { + // 如果没有 sysUserId ,说明没有绑定账号,获取到手机号之后进行绑定 + if (appUser == null) { + appUser = JdtUserAPI.getUserById(appUserId, accessToken).getResult(); + } + // 判断系统里是否有这个手机号的用户 + SysUser sysUser = userMapper.getUserByPhone(appUser.getMobile()); + if (sysUser != null) { + thirdAccount.setAvatar(appUser.getAvatar()); + thirdAccount.setRealname(appUser.getName()); + thirdAccount.setThirdUserId(appUser.getUserid()); + // 代码逻辑说明: [QQYUN-4883]钉钉auth登录同一个租户下有同一个用户id------------ + thirdAccount.setThirdUserUuid(appUser.getUnionid()); + thirdAccount.setSysUserId(sysUser.getId()); + sysThirdAccountService.updateById(thirdAccount); + return sysUser; + } else { + // 没有就走创建逻辑 + return sysThirdAccountService.createUser(appUser.getMobile(), appUser.getUnionid(),tenantId); + } + + } + } + + //========================begin 应用低代码钉钉同步用户部门专用 ==================== + + /** + * 根据类型和租户id获取钉钉配置 + * @return + */ + private SysThirdAppConfig getDingThirdAppConfig(){ + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + this.tenantIzExist(tenantId); + return configMapper.getThirdConfigByThirdType(tenantId,MessageTypeEnum.DD.getType()); + } + + /** + * 获取钉钉accessToken + * @param config + * @return + */ + private String getTenantAccessToken(SysThirdAppConfig config) { + if(null == config){ + return null; + } + AccessToken accessToken = JdtBaseAPI.getAccessToken(config.getClientId(), config.getClientSecret()); + if (accessToken != null) { + return accessToken.getAccessToken(); + } + log.warn("获取AccessToken失败"); + return null; + } + + /** + * 添加或保存用户租户 + * @param userId + * @param isUpdate 是否是新增 + */ + private void createUserTenant(String userId,Boolean isUpdate){ + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + //判断当前用户是否已在该租户下面 + Integer count = userTenantMapper.userTenantIzExist(userId, tenantId); + //count 为0 新增租户用户,否则不用新增 + if(count == 0){ + SysUserTenant userTenant = new SysUserTenant(); + userTenant.setTenantId(tenantId); + userTenant.setUserId(userId); + userTenant.setStatus(isUpdate?CommonConstant.USER_TENANT_UNDER_REVIEW:CommonConstant.USER_TENANT_NORMAL); + userTenantMapper.insert(userTenant); + } + } + } + + /** + * 同步用户和部门 + * @return + */ + public SyncInfoVo syncThirdAppDepartmentUserToLocal() { + SyncInfoVo syncInfo = new SyncInfoVo(); + String accessToken = this.getAccessToken(); + if (accessToken == null) { + syncInfo.addFailInfo("accessToken获取失败!"); + return syncInfo; + } + // 获取【钉钉】所有的部门 + List departments = JdtDepartmentAPI.listAll(accessToken); + // 代码逻辑说明: 【TV360X-1316】钉钉同步提示消息不正确--- + if(departments.isEmpty()){ + throw new GhbBootBizTipException("请查看配置参数和白名单是否配置!"); + } + String username = JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()); + List departmentTreeList = JdtDepartmentTreeVo.listToTree(departments); + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + // 递归同步部门 + this.syncDepartmentToLocalRecursion(departmentTreeList, null, username, syncInfo, accessToken,true,tenantId); + return syncInfo; + } + + + /** + * 添加用户及用户部门关系 + * @param departId 部门id + * @param dingDepartId 钉钉部门id + * @param accessToken + * @param syncInfo + * @param syncedUserIdSet + */ + private void addDepartUser(String departId, Integer dingDepartId, String accessToken, SyncInfoVo syncInfo, Set syncedUserIdSet, Integer tenantId) { + List userList = new ArrayList<>(); + getUserListByDeptIdRecursion(dingDepartId, 0, userList, accessToken); + for (User user : userList) { + if (syncedUserIdSet.contains(user.getUserid())) { + //需要同步用户部门 + this.syncAddOrUpdateUserDepart(user.getUserid(),departId); + continue; + } + syncedUserIdSet.add(user.getUserid()); + SysUser userByPhone = userMapper.getUserByPhone(user.getMobile()); + SysThirdAccount sysThirdAccount = sysThirdAccountService.getOneByUuidAndThirdType(user.getUnionid(), THIRD_TYPE,tenantId,user.getUserid()); + if (null != userByPhone) { + // 循环到此说明用户匹配成功,进行更新操作 + SysUser updateSysUser = this.dtUserToSysUser(user, userByPhone); + try { + userMapper.updateById(updateSysUser); + String str = String.format("用户 %s(%s) 更新成功!", updateSysUser.getRealname(), updateSysUser.getUsername()); + // 代码逻辑说明: 【TV360X-1317】钉钉同步 同步成功之后 重复提示--- + if(!syncInfo.getSuccessInfo().contains(str)){ + syncInfo.addSuccessInfo(str); + } + } catch (Exception e) { + this.syncUserCollectErrInfo(e, user, syncInfo); + } + //第三方账号关系表 + this.thirdAccountSaveOrUpdate(sysThirdAccount, updateSysUser.getId(), user, tenantId); + //创建当前租户 + this.createUserTenant(updateSysUser.getId(),true); + //需要同步用户部门 + this.syncAddOrUpdateUserDepart(updateSysUser.getId(),departId); + } else { + // 如果没有匹配到用户,则走创建逻辑 + SysUser newSysUser = this.dtUserToSysUser(user); + try { + userMapper.insert(newSysUser); + String str = String.format("用户 %s(%s) 创建成功!", newSysUser.getRealname(), newSysUser.getUsername()); + syncInfo.addSuccessInfo(str); + } catch (Exception e) { + this.syncUserCollectErrInfo(e, user, syncInfo); + } + //第三方账号关系表 + this.thirdAccountSaveOrUpdate(sysThirdAccount, newSysUser.getId(), user,tenantId); + //创建当前租户 + this.createUserTenant(newSysUser.getId(),false); + //需要同步用户部门 + this.syncAddOrUpdateUserDepart(newSysUser.getId(),departId); + } + } + } + + /** + * 通过用户id和部门id新增用户部门关系表 + * @param userId + * @param departId + */ + private void syncAddOrUpdateUserDepart(String userId, String departId) { + //查询用户是否在部门里面 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserDepart::getDepId,departId); + query.eq(SysUserDepart::getUserId,userId); + long count = sysUserDepartService.count(query); + if(count == 0){ + //不存在,则新增部门用户关系 + SysUserDepart sysUserDepart = new SysUserDepart(null,userId,departId); + sysUserDepartService.save(sysUserDepart); + } + } + + //========================end 应用低代码钉钉同步用户部门专用 ==================== + + /** + * 验证租户是否存在 + * @param tenantId + */ + public void tenantIzExist(Integer tenantId){ + if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){ + Long count = tenantMapper.tenantIzExist(tenantId); + if(ObjectUtil.isEmpty(count) || 0 == count){ + throw new GhbBootException("租户ID:" + tenantId + "无效,平台中不存在!"); + } + } + } + + //=================================== begin 新版钉钉登录 ============================================ + /** + * 钉钉登录获取用户信息 + * 【QQYUN-9421】钉钉登录后打开了敲敲云,换其他账号登录后,再打开敲敲云显示的是原来账号的应用 + * @param authCode + * @param tenantId + * @return + */ + public SysUser oauthDingDingLogin(String authCode, Integer tenantId) { + Long count = tenantMapper.tenantIzExist(tenantId); + if(ObjectUtil.isEmpty(count) || 0 == count){ + throw new GhbBootException("租户不存在!"); + } + SysThirdAppConfig config = configMapper.getThirdConfigByThirdType(tenantId, MessageTypeEnum.DD.getType()); + String accessToken = this.getTenantAccessToken(config); + if(StringUtils.isEmpty(accessToken)){ + throw new GhbBootBizTipException("accessToken获取失败"); + } + String getUserInfoUrl = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo?access_token=" + accessToken; + Map params = new HashMap<>(); + params.put("code",authCode); + Response userInfoResponse = HttpUtil.post(getUserInfoUrl, JSON.toJSONString(params)); + if (userInfoResponse.isSuccess()) { + String userId = userInfoResponse.getResult().getString("userid"); + // 判断第三方用户表有没有这个人 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAccount::getThirdType, THIRD_TYPE); + queryWrapper.eq(SysThirdAccount::getTenantId, tenantId); + queryWrapper.and((wrapper)->wrapper.eq(SysThirdAccount::getThirdUserUuid,userId).or().eq(SysThirdAccount::getThirdUserId,userId)); + SysThirdAccount thirdAccount = sysThirdAccountService.getOne(queryWrapper); + if (thirdAccount != null) { + return this.getSysUserByThird(thirdAccount, null, userId, accessToken, tenantId); + }else{ + throw new GhbBootException("该用户没有同步,请先同步!"); + } + } + return null; + } + + /** + * 根据租户id获取企业id和应用id + * 【QQYUN-9421】钉钉登录后打开了敲敲云,换其他账号登录后,再打开敲敲云显示的是原来账号的应用 + * @param tenantId + */ + public SysThirdAppConfig getCorpIdClientId(Integer tenantId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAppConfig::getThirdType, THIRD_TYPE); + queryWrapper.eq(SysThirdAppConfig::getTenantId, tenantId); + queryWrapper.select(SysThirdAppConfig::getCorpId,SysThirdAppConfig::getClientId); + return configMapper.selectOne(queryWrapper); + } + //=================================== end 新版钉钉登录 ============================================ +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ThirdAppWechatEnterpriseServiceImpl.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ThirdAppWechatEnterpriseServiceImpl.java new file mode 100644 index 0000000..effaeee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/service/impl/ThirdAppWechatEnterpriseServiceImpl.java @@ -0,0 +1,1438 @@ +package com.ghb.base.modules.system.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.jeecg.qywx.api.base.JwAccessTokenAPI; +import com.jeecg.qywx.api.core.common.AccessToken; +import com.jeecg.qywx.api.department.JwDepartmentAPI; +import com.jeecg.qywx.api.department.vo.DepartMsgResponse; +import com.jeecg.qywx.api.department.vo.Department; +import com.jeecg.qywx.api.message.JwMessageAPI; +import com.jeecg.qywx.api.message.vo.*; +import com.jeecg.qywx.api.user.JwUserAPI; +import com.jeecg.qywx.api.user.vo.User; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import com.ghb.base.common.api.dto.message.MessageDTO; +import org.jeecg.common.config.TenantContext; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.constant.enums.MessageTypeEnum; +import com.ghb.base.common.exception.GhbBootException; +import com.ghb.base.common.system.util.JwtUtil; +import com.ghb.base.common.util.PasswordUtil; +import com.ghb.base.common.util.RestUtil; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.config.GhbBaseConfig; +import com.ghb.base.config.mybatis.MybatisPlusSaasConfig; +import com.ghb.base.modules.system.entity.*; +import com.ghb.base.modules.system.mapper.*; +import com.ghb.base.modules.system.model.SysDepartTreeModel; +import com.ghb.base.modules.system.service.*; +import com.ghb.base.modules.system.vo.SysPositionVO; +import com.ghb.base.modules.system.vo.thirdapp.JwDepartmentTreeVo; +import com.ghb.base.modules.system.vo.thirdapp.JwSysUserDepartVo; +import com.ghb.base.modules.system.vo.thirdapp.JwUserDepartVo; +import com.ghb.base.modules.system.vo.thirdapp.SyncInfoVo; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +/** + * 第三方App对接:企业微信实现类 + * @author: Ghb-boot + */ +@Slf4j +@Service +public class ThirdAppWechatEnterpriseServiceImpl implements IThirdAppService { + + @Autowired + GhbBaseConfig GhbBaseConfig; + @Autowired + private ISysDepartService sysDepartService; + @Autowired + private SysUserMapper userMapper; + @Autowired + private ISysThirdAccountService sysThirdAccountService; + @Autowired + private ISysUserDepartService sysUserDepartService; + @Autowired + private ISysPositionService sysPositionService; + @Autowired + private SysAnnouncementSendMapper sysAnnouncementSendMapper; + @Autowired + private SysThirdAppConfigMapper configMapper; + @Autowired + private SysTenantMapper sysTenantMapper; + @Autowired + private SysUserTenantMapper sysUserTenantMapper; + @Autowired + private SysThirdAccountMapper sysThirdAccountMapper; + @Autowired + private SysTenantMapper tenantMapper; + + + /** + * errcode + */ + private static final String ERR_CODE = "errcode"; + + /** + * 第三方APP类型,当前固定为 wechat_enterprise + */ + public final String THIRD_TYPE = "wechat_enterprise"; + + @Override + public String getAccessToken() { + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + SysThirdAppConfig config = this.getWeChatThirdAppConfig(); + String corpId = config.getClientId(); + String secret = config.getClientSecret(); + AccessToken accessToken = JwAccessTokenAPI.getAccessToken(corpId, secret); + if (accessToken != null) { + return accessToken.getAccesstoken(); + } + log.warn("获取AccessToken失败"); + return null; + } + + /** 获取APPToken,新版企业微信的秘钥是分开的 */ + public String getAppAccessToken(SysThirdAppConfig config) { + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + String corpId = config.getClientId(); + // 如果没有配置APP秘钥,就说明是老企业,可以通用秘钥 + String secret = config.getClientSecret(); + + AccessToken accessToken = JwAccessTokenAPI.getAccessToken(corpId, secret); + if (accessToken != null) { + return accessToken.getAccesstoken(); + } + log.warn("获取AccessToken失败"); + return null; + } + + @Override + public SyncInfoVo syncLocalDepartmentToThirdApp(String ids) { + SyncInfoVo syncInfo = new SyncInfoVo(); + String accessToken = this.getAccessToken(); + if (accessToken == null) { + syncInfo.addFailInfo("accessToken获取失败!"); + return syncInfo; + } + // 获取企业微信所有的部门 + List departments = JwDepartmentAPI.getAllDepartment(accessToken); + if (departments == null) { + syncInfo.addFailInfo("获取企业微信所有部门失败!"); + return syncInfo; + } + // 删除企业微信有但本地没有的部门(以本地部门数据为主)(以为企业微信不能创建同名部门,所以只能先删除) + List departmentTreeList = JwDepartmentTreeVo.listToTree(departments); + this.deleteDepartRecursion(departmentTreeList, accessToken, true); + // 获取本地所有部门树结构 + List sysDepartsTree = sysDepartService.queryTreeList(); + // -- 企业微信不能创建新的顶级部门,所以新的顶级部门的parentId就为1 + Department parent = new Department(); + parent.setId("1"); + // 递归同步部门 + departments = JwDepartmentAPI.getAllDepartment(accessToken); + this.syncDepartmentRecursion(sysDepartsTree, departments, parent, accessToken); + return syncInfo; + } + + /** + * 递归删除部门以及子部门,由于企业微信不允许删除带有成员和子部门的部门,所以需要递归删除下子部门,然后把部门成员移动端根部门下 + * @param children + * @param accessToken + * @param ifLocal + */ + private void deleteDepartRecursion(List children, String accessToken, boolean ifLocal) { + for (JwDepartmentTreeVo departmentTree : children) { + String depId = departmentTree.getId(); + // 过滤根部门 + if (!"1".equals(depId)) { + // 判断本地是否有该部门 + if (ifLocal) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDepart::getQywxIdentifier, depId); + SysDepart sysDepart = sysDepartService.getOne(queryWrapper); + // 本地有该部门,不删除 + if (sysDepart != null) { + if (departmentTree.hasChildren()) { + this.deleteDepartRecursion(departmentTree.getChildren(), accessToken, true); + } + continue; + } + } + // 判断是否有成员,有就移动到根部门 + List departUserList = JwUserAPI.getUsersByDepartid(depId, "1", null, accessToken); + if (departUserList != null && departUserList.size() > 0) { + for (User user : departUserList) { + User updateUser = new User(); + updateUser.setUserid(user.getUserid()); + updateUser.setDepartment(new Integer[]{1}); + JwUserAPI.updateUser(updateUser, accessToken); + } + } + // 有子部门优先删除子部门 + if (departmentTree.hasChildren()) { + this.deleteDepartRecursion(departmentTree.getChildren(), accessToken, false); + } + // 执行删除操作 + JwDepartmentAPI.deleteDepart(depId, accessToken); + } + } + } + + /** + * 递归同步部门到第三方APP + * @param sysDepartsTree + * @param departments + * @param parent + * @param accessToken + */ + private void syncDepartmentRecursion(List sysDepartsTree, List departments, Department parent, String accessToken) { + if (sysDepartsTree != null && sysDepartsTree.size() != 0) { + for1: + for (SysDepartTreeModel depart : sysDepartsTree) { + for (Department department : departments) { + // id相同,代表已存在,执行修改操作 + if (department.getId().equals(depart.getQywxIdentifier())) { + this.sysDepartToQwDepartment(depart, department, parent.getId()); + JwDepartmentAPI.updateDepart(department, accessToken); + // 紧接着同步子级 + this.syncDepartmentRecursion(depart.getChildren(), departments, department, accessToken); + // 跳出外部循环 + continue for1; + } + } + // 循环到此说明是新部门,直接调接口创建 + Department newDepartment = this.sysDepartToQwDepartment(depart, parent.getId()); + DepartMsgResponse response = JwDepartmentAPI.createDepartment(newDepartment, accessToken); + // 创建成功,将返回的id绑定到本地 + if (response != null && response.getId() != null) { + SysDepart sysDepart = new SysDepart(); + sysDepart.setId(depart.getId()); + sysDepart.setQywxIdentifier(response.getId().toString()); + sysDepartService.updateById(sysDepart); + Department newParent = new Department(); + newParent.setId(response.getId().toString()); + // 紧接着同步子级 + this.syncDepartmentRecursion(depart.getChildren(), departments, newParent, accessToken); + } + // 收集错误信息 +// this.syncUserCollectErrInfo(errCode, sysUser, errInfo); + } + } + } + + public SyncInfoVo syncThirdAppDepartmentToLocal(Integer tenantId, Map map) { + SyncInfoVo syncInfo = new SyncInfoVo(); + String accessToken = this.getAccessToken(); + if (accessToken == null) { + syncInfo.addFailInfo("accessToken获取失败!"); + return syncInfo; + } + // 获取企业微信所有的部门 + List departments = JwDepartmentAPI.getAllDepartment(accessToken); + if (departments == null) { + syncInfo.addFailInfo("企业微信部门信息获取失败!"); + return syncInfo; + } + String username = JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()); + // 将list转为tree + List departmentTreeList = JwDepartmentTreeVo.listToTree(departments); + // 递归同步部门 + this.syncDepartmentToLocalRecursion(departmentTreeList, null, username, syncInfo, tenantId, map); + return syncInfo; + } + + /** + * 递归同步部门到本地 + */ + private void syncDepartmentToLocalRecursion(List departmentTreeList, String sysParentId, String username, SyncInfoVo syncInfo,Integer tenantId, Map map) { + if (departmentTreeList != null && departmentTreeList.size() != 0) { + for (JwDepartmentTreeVo departmentTree : departmentTreeList) { + String depId = departmentTree.getId(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + // 根据 qywxIdentifier 字段和租户id查询,租户id默认为0 + queryWrapper.eq(SysDepart::getQywxIdentifier, depId); + queryWrapper.eq(SysDepart::getTenantId, tenantId); + SysDepart sysDepart = sysDepartService.getOne(queryWrapper); + if (sysDepart != null) { + // 执行更新操作 + SysDepart updateSysDepart = this.qwDepartmentToSysDepart(departmentTree, sysDepart); + // 代码逻辑说明: 【issues/6017】企业微信同步部门时没有最顶层的部门名,同步用户时,用户没有部门信息--- + if (sysParentId != null && !"0".equals(sysParentId)) { + updateSysDepart.setParentId(sysParentId); + } + try { + sysDepartService.updateDepartDataById(updateSysDepart, username); + String str = String.format("部门 %s 更新成功!", updateSysDepart.getDepartName()); + syncInfo.addSuccessInfo(str); + map.put(depId,updateSysDepart.getId()); + } catch (Exception e) { + this.syncDepartCollectErrInfo(e, departmentTree, syncInfo); + } + if (departmentTree.hasChildren()) { + // 紧接着同步子级 + this.syncDepartmentToLocalRecursion(departmentTree.getChildren(), updateSysDepart.getId(), username, syncInfo, tenantId, map); + } + } else { + // 执行新增操作 + SysDepart newSysDepart = this.qwDepartmentToSysDepart(departmentTree, null); + if (sysParentId != null && !"0".equals(sysParentId)) { + newSysDepart.setParentId(sysParentId); + // 2 = 组织机构 + newSysDepart.setOrgCategory("2"); + } else { + // 1 = 公司 + newSysDepart.setOrgCategory("1"); + } + newSysDepart.setTenantId(tenantId); + try { + sysDepartService.saveDepartData(newSysDepart, username); + String str = String.format("部门 %s 创建成功!", newSysDepart.getDepartName()); + syncInfo.addSuccessInfo(str); + map.put(depId,newSysDepart.getId()); + } catch (Exception e) { + this.syncDepartCollectErrInfo(e, departmentTree, syncInfo); + } + // 紧接着同步子级 + if (departmentTree.hasChildren()) { + this.syncDepartmentToLocalRecursion(departmentTree.getChildren(), newSysDepart.getId(), username, syncInfo, tenantId, map); + } + } + } + } + } + + @Override + public SyncInfoVo syncLocalUserToThirdApp(String ids) { + SyncInfoVo syncInfo = new SyncInfoVo(); + String accessToken = this.getAccessToken(); + if (accessToken == null) { + syncInfo.addFailInfo("accessToken获取失败!"); + return syncInfo; + } + // 获取企业微信所有的用户 +// List qwUsers = JwUserAPI.getDetailUsersByDepartid("1", null, null, accessToken); + // 获取企业微信所有的用户(只能获取userid) + List qwUsers = JwUserAPI.getUserIdList(accessToken); + + if (qwUsers == null) { + syncInfo.addFailInfo("企业微信用户列表查询失败!"); + return syncInfo; + } + List sysUsers; + if (StringUtils.isNotBlank(ids)) { + String[] idList = ids.split(","); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getId, (Object[]) idList); + // 获取本地指定用户 + sysUsers = userMapper.selectList(queryWrapper); + } else { + // 获取本地所有用户 + sysUsers = userMapper.selectList(Wrappers.emptyWrapper()); + } + if (CollectionUtils.isEmpty(sysUsers)) { + return syncInfo; + } + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + List userIds = sysUsers.stream().map(SysUser::getId).collect(Collectors.toList()); + // ① 批量预加载 sys_third_account → Map + Map thirdAccountMap = sysThirdAccountService + .listBySysUserIds(userIds, THIRD_TYPE) + .stream() + .collect(Collectors.toMap(SysThirdAccount::getSysUserId, a -> a, (a, b) -> a)); + // ② 批量预加载用户-部门关系 → Map> + LambdaQueryWrapper udQw = new LambdaQueryWrapper<>(); + udQw.in(SysUserDepart::getUserId, userIds); + Map> userDepartIdsMap = sysUserDepartService.list(udQw) + .stream() + .collect(Collectors.groupingBy( + SysUserDepart::getUserId, + Collectors.mapping(SysUserDepart::getDepId, Collectors.toList()) + )); + // ③ 批量预加载所有涉及的部门 → Map + Set allDepartIds = userDepartIdsMap.values().stream() + .flatMap(Collection::stream).collect(Collectors.toSet()); + Map departMap = Collections.emptyMap(); + if (!allDepartIds.isEmpty()) { + departMap = sysDepartService.listByIds(allDepartIds) + .stream() + .collect(Collectors.toMap(SysDepart::getId, d -> d, (a, b) -> a)); + } + // ④ 批量预加载职位 → Map> + Map> positionMap = sysPositionService + .getPositionListByUserIds(userIds) + .stream() + .collect(Collectors.groupingBy(SysPositionVO::getUserId)); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + + // 循环判断新用户和需要更新的用户 + for1: + for (SysUser sysUser : sysUsers) { + // 外部模拟登陆临时账号,不同步 + if ("_reserve_user_external".equals(sysUser.getUsername())) { + continue; + } + /* + * 判断是否同步过的逻辑: + * 1. 查询 sys_third_account(第三方账号表)是否有数据,如果有代表已同步 + * 2. 本地表里没有,就先用手机号判断,不通过再用username判断。 + */ + User qwUser; + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + SysThirdAccount sysThirdAccount = thirdAccountMap.get(sysUser.getId()); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + for (User qwUserTemp : qwUsers) { + if (sysThirdAccount == null || oConvertUtils.isEmpty(sysThirdAccount.getThirdUserId()) || !sysThirdAccount.getThirdUserId().equals(qwUserTemp.getUserid())) { + // sys_third_account 表匹配失败,尝试用手机号匹配 + // 新版企业微信调整了API,现在只能通过userid来判断是否同步过了 +// String phone = sysUser.getPhone(); +// if (!(oConvertUtils.isEmpty(phone) || phone.equals(qwUserTemp.getMobile()))) { + // 手机号匹配失败,再尝试用username匹配 + String username = sysUser.getUsername(); + if (!(oConvertUtils.isEmpty(username) || username.equals(qwUserTemp.getUserid()))) { + // username 匹配失败,直接跳到下一次循环继续 + continue; + } +// } + } + // 循环到此说明用户匹配成功,进行更新操作 + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + qwUser = this.sysUserToQwUser(sysUser, qwUserTemp, userDepartIdsMap, departMap, positionMap); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + int errCode = JwUserAPI.updateUser(qwUser, accessToken); + // 收集错误信息 + this.syncUserCollectErrInfo(errCode, sysUser, syncInfo); + this.thirdAccountSaveOrUpdate(sysThirdAccount, sysUser.getId(), qwUser.getUserid(),qwUser.getName(), null); + // 更新完成,直接跳到下一次外部循环继续 + continue for1; + } + // 循环到此说明是新用户,直接调接口创建 + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + qwUser = this.sysUserToQwUser(sysUser, userDepartIdsMap, departMap, positionMap); + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + int errCode = JwUserAPI.createUser(qwUser, accessToken); + // 收集错误信息 + boolean apiSuccess = this.syncUserCollectErrInfo(errCode, sysUser, syncInfo); + if (apiSuccess) { + this.thirdAccountSaveOrUpdate(sysThirdAccount, sysUser.getId(), qwUser.getUserid(),qwUser.getName(), null); + } + } + return syncInfo; + } + +// @Override +// public SyncInfoVo syncThirdAppUserToLocal() { +// SyncInfoVo syncInfo = new SyncInfoVo(); +// String accessToken = this.getAccessToken(); +// if (accessToken == null) { +// syncInfo.addFailInfo("accessToken获取失败!"); +// return syncInfo; +// } +// // 获取企业微信所有的用户 +// List qwUsersList = JwUserAPI.getDetailUsersByDepartid("1", null, null, accessToken); +// if (qwUsersList == null) { +// syncInfo.addFailInfo("企业微信用户列表查询失败!"); +// return syncInfo; +// } +// //查询本地用户 +// List sysUsersList = userMapper.selectList(Wrappers.emptyWrapper()); +// // 循环判断新用户和需要更新的用户 +// for (User qwUser : qwUsersList) { +// /* +// * 判断是否同步过的逻辑: +// * 1. 查询 sys_third_account(第三方账号表)是否有数据,如果有代表已同步 +// * 2. 本地表里没有,就先用手机号判断,不通过再用username判断。 +// */ +// SysThirdAccount sysThirdAccount = sysThirdAccountService.getOneByThirdUserId(qwUser.getUserid(), THIRD_TYPE); +// List collect = sysUsersList.stream().filter(user -> (qwUser.getMobile().equals(user.getPhone()) || qwUser.getUserid().equals(user.getUsername())) +// ).collect(Collectors.toList()); +// +// if (collect != null && collect.size() > 0) { +// SysUser sysUserTemp = collect.get(0); +// // 循环到此说明用户匹配成功,进行更新操作 +// SysUser updateSysUser = this.qwUserToSysUser(qwUser, sysUserTemp); +// try { +// userMapper.updateById(updateSysUser); +// String str = String.format("用户 %s(%s) 更新成功!", updateSysUser.getRealname(), updateSysUser.getUsername()); +// syncInfo.addSuccessInfo(str); +// } catch (Exception e) { +// this.syncUserCollectErrInfo(e, qwUser, syncInfo); +// } +// +// this.thirdAccountSaveOrUpdate(sysThirdAccount, updateSysUser.getId(), qwUser.getUserid()); +// // 更新完成,直接跳到下一次外部循环继续 +// }else{ +// // 没匹配到用户则走新增逻辑 +// SysUser newSysUser = this.qwUserToSysUser(qwUser); +// try { +// userMapper.insert(newSysUser); +// String str = String.format("用户 %s(%s) 创建成功!", newSysUser.getRealname(), newSysUser.getUsername()); +// syncInfo.addSuccessInfo(str); +// } catch (Exception e) { +// this.syncUserCollectErrInfo(e, qwUser, syncInfo); +// } +// this.thirdAccountSaveOrUpdate(sysThirdAccount, newSysUser.getId(), qwUser.getUserid()); +// } +// } +// return syncInfo; +// } + + /** + * 保存或修改第三方登录表 + * + * @param sysThirdAccount 第三方账户表对象,为null就新增数据,否则就修改 + * @param sysUserId 本地系统用户ID + * @param qwUserId 企业微信用户ID + * @param wechatRealName 企业微信用户真实姓名 + */ + private void thirdAccountSaveOrUpdate(SysThirdAccount sysThirdAccount, String sysUserId, String qwUserId, String wechatRealName, Integer tenantId) { + if (sysThirdAccount == null) { + sysThirdAccount = new SysThirdAccount(); + sysThirdAccount.setSysUserId(sysUserId); + sysThirdAccount.setStatus(1); + sysThirdAccount.setDelFlag(0); + sysThirdAccount.setThirdType(THIRD_TYPE); + if(oConvertUtils.isNotEmpty(tenantId)){ + sysThirdAccount.setTenantId(tenantId); + } + } + sysThirdAccount.setThirdUserId(qwUserId); + sysThirdAccount.setThirdUserUuid(qwUserId); + sysThirdAccount.setRealname(wechatRealName); + sysThirdAccountService.saveOrUpdate(sysThirdAccount); + } + + /** + * 【同步用户】收集同步过程中的错误信息 + */ + private boolean syncUserCollectErrInfo(int errCode, SysUser sysUser, SyncInfoVo syncInfo) { + if (errCode != 0) { + String msg = ""; + // https://open.work.weixin.qq.com/api/doc/90000/90139/90313 + switch (errCode) { + case 40003: + msg = "无效的UserID"; + break; + case 60129: + msg = "手机和邮箱不能都为空"; + break; + case 60102: + msg = "UserID已存在"; + break; + case 60103: + msg = "手机号码不合法"; + break; + case 60104: + msg = "手机号码已存在"; + break; + default: + } + String str = String.format("用户 %s(%s) 同步失败!错误码:%s——%s", sysUser.getUsername(), sysUser.getRealname(), errCode, msg); + syncInfo.addFailInfo(str); + return false; + } else { + String str = String.format("用户 %s(%s) 同步成功!", sysUser.getUsername(), sysUser.getRealname()); + syncInfo.addSuccessInfo(str); + return true; + } + } + + private boolean syncUserCollectErrInfo(Exception e, User qwUser, SyncInfoVo syncInfo) { + String msg; + if (e instanceof DuplicateKeyException) { + msg = e.getCause().getMessage(); + } else { + msg = e.getMessage(); + } + String str = String.format("用户 %s(%s) 同步失败!错误信息:%s", qwUser.getUserid(), qwUser.getName(), msg); + syncInfo.addFailInfo(str); + return false; + } + + private boolean syncDepartCollectErrInfo(Exception e, Department department, SyncInfoVo syncInfo) { + String msg; + if (e instanceof DuplicateKeyException) { + msg = e.getCause().getMessage(); + } else { + msg = e.getMessage(); + } + String str = String.format("部门 %s(%s) 同步失败!错误信息:%s", department.getName(), department.getId(), msg); + syncInfo.addFailInfo(str); + return false; + } + + /** + * 【同步用户】将SysUser转为企业微信的User对象(创建新用户) + */ + private User sysUserToQwUser(SysUser sysUser) { + User user = new User(); + // 通过 username 来关联 + user.setUserid(sysUser.getUsername()); + return this.sysUserToQwUser(sysUser, user); + } + + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + /** + * 【同步用户】将SysUser转为企业微信的User对象(创建新用户,使用批量预加载Map) + */ + private User sysUserToQwUser(SysUser sysUser, Map> userDepartIdsMap, + Map departMap, Map> positionMap) { + User user = new User(); + user.setUserid(sysUser.getUsername()); + return this.sysUserToQwUser(sysUser, user, userDepartIdsMap, departMap, positionMap); + } + + /** + * 【同步用户】将SysUser转为企业微信的User对象(更新旧用户,使用批量预加载Map) + */ + private User sysUserToQwUser(SysUser sysUser, User user, Map> userDepartIdsMap, + Map departMap, Map> positionMap) { + user.setName(sysUser.getRealname()); + user.setMobile(sysUser.getPhone()); + // 查询并同步用户部门关系(使用预加载Map替代单次查询) + List departList = this.getUserDepart(sysUser, userDepartIdsMap, departMap); + if (departList != null) { + List departmentIdList = new ArrayList<>(); + List isLeaderInDept = new ArrayList<>(); + List manageDepartIdList = new ArrayList<>(); + if (oConvertUtils.isNotEmpty(sysUser.getDepartIds())) { + manageDepartIdList = Arrays.asList(sysUser.getDepartIds().split(",")); + } + for (SysDepart sysDepart : departList) { + if (oConvertUtils.isNotEmpty(sysDepart.getQywxIdentifier())) { + try { + departmentIdList.add(Integer.parseInt(sysDepart.getQywxIdentifier())); + } catch (NumberFormatException ignored) { + continue; + } + if (CommonConstant.USER_IDENTITY_2.equals(sysUser.getUserIdentity())) { + isLeaderInDept.add(manageDepartIdList.contains(sysDepart.getId()) ? 1 : 0); + } else { + isLeaderInDept.add(0); + } + } + } + user.setDepartment(departmentIdList.toArray(new Integer[]{})); + user.setIs_leader_in_dept(isLeaderInDept.toArray(new Integer[]{})); + } + if (user.getDepartment() == null || user.getDepartment().length == 0) { + user.setDepartment(new Integer[]{1}); + user.setIs_leader_in_dept(new Integer[]{0}); + } + // 职务翻译(使用预加载Map替代单次查询) + List positionList = positionMap.getOrDefault(sysUser.getId(), Collections.emptyList()); + if (!positionList.isEmpty()) { + String positionName = positionList.stream().map(SysPositionVO::getName).collect(Collectors.joining(SymbolConstant.COMMA)); + user.setPosition(positionName); + } + if (sysUser.getSex() != null) { + user.setGender(sysUser.getSex().toString()); + } + user.setEmail(sysUser.getEmail()); + if (sysUser.getStatus() != null) { + if (CommonConstant.USER_UNFREEZE.equals(sysUser.getStatus()) || CommonConstant.USER_FREEZE.equals(sysUser.getStatus())) { + user.setEnable(sysUser.getStatus() == 1 ? 1 : 0); + } else { + user.setEnable(1); + } + } + user.setTelephone(sysUser.getTelephone()); + if (CommonConstant.DEL_FLAG_1.equals(sysUser.getDelFlag())) { + user.setEnable(0); + } + return user; + } + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + + /** + * 【同步用户】将SysUser转为企业微信的User对象(更新旧用户) + */ + private User sysUserToQwUser(SysUser sysUser, User user) { + user.setName(sysUser.getRealname()); + user.setMobile(sysUser.getPhone()); + // 查询并同步用户部门关系 + List departList = this.getUserDepart(sysUser); + if (departList != null) { + List departmentIdList = new ArrayList<>(); + // 企业微信 1表示为上级,0表示非上级 + List isLeaderInDept = new ArrayList<>(); + // 当前用户管理的部门 + List manageDepartIdList = new ArrayList<>(); + if (oConvertUtils.isNotEmpty(sysUser.getDepartIds())) { + manageDepartIdList = Arrays.asList(sysUser.getDepartIds().split(",")); + } + for (SysDepart sysDepart : departList) { + // 企业微信的部门id + if (oConvertUtils.isNotEmpty(sysDepart.getQywxIdentifier())) { + try { + departmentIdList.add(Integer.parseInt(sysDepart.getQywxIdentifier())); + } catch (NumberFormatException ignored) { + continue; + } + // 判断用户身份,是否为上级 + if (CommonConstant.USER_IDENTITY_2.equals(sysUser.getUserIdentity())) { + // 判断当前部门是否为该用户管理的部门 + isLeaderInDept.add(manageDepartIdList.contains(sysDepart.getId()) ? 1 : 0); + } else { + isLeaderInDept.add(0); + } + } + } + user.setDepartment(departmentIdList.toArray(new Integer[]{})); + // 个数必须和参数department的个数一致,表示在所在的部门内是否为上级。1表示为上级,0表示非上级。在审批等应用里可以用来标识上级审批人 + user.setIs_leader_in_dept(isLeaderInDept.toArray(new Integer[]{})); + } + if (user.getDepartment() == null || user.getDepartment().length == 0) { + // 没有找到匹配部门,同步到根部门下 + user.setDepartment(new Integer[]{1}); + user.setIs_leader_in_dept(new Integer[]{0}); + } + // 职务翻译 + // 代码逻辑说明: [QQYUN-3980]组织管理中 职位功能 职位表加租户id 加职位-用户关联表------------ + List positionList = sysPositionService.getPositionList(sysUser.getId()); + if(null != positionList && positionList.size()>0){ + String positionName = positionList.stream().map(SysPosition::getName).collect(Collectors.joining(SymbolConstant.COMMA)); + user.setPosition(positionName); + } + if (sysUser.getSex() != null) { + user.setGender(sysUser.getSex().toString()); + } + user.setEmail(sysUser.getEmail()); + // 启用/禁用成员(状态),规则不同,需要转换 + // 企业微信规则:1表示启用成员,0表示禁用成员 + // Ghb规则:1正常,2冻结 + if (sysUser.getStatus() != null) { + if (CommonConstant.USER_UNFREEZE.equals(sysUser.getStatus()) || CommonConstant.USER_FREEZE.equals(sysUser.getStatus())) { + user.setEnable(sysUser.getStatus() == 1 ? 1 : 0); + } else { + user.setEnable(1); + } + } + // 座机号 + user.setTelephone(sysUser.getTelephone()); + // --- 企业微信没有逻辑删除的功能 + // 代码逻辑说明: 本地逻辑删除的用户,在企业微信里禁用 ----- + if (CommonConstant.DEL_FLAG_1.equals(sysUser.getDelFlag())) { + user.setEnable(0); + } + + return user; + } + + /** + * 查询用户和部门的关系 + */ + private List getUserDepart(SysUser sysUser) { + // 根据用户部门关系表查询出用户的部门 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysUserDepart::getUserId, sysUser.getId()); + List sysUserDepartList = sysUserDepartService.list(queryWrapper); + if (sysUserDepartList.size() == 0) { + return null; + } + // 根据用户部门 + LambdaQueryWrapper departQueryWrapper = new LambdaQueryWrapper<>(); + List departIdList = sysUserDepartList.stream().map(SysUserDepart::getDepId).collect(Collectors.toList()); + departQueryWrapper.in(SysDepart::getId, departIdList); + List departList = sysDepartService.list(departQueryWrapper); + return departList.size() == 0 ? null : departList; + } + + //update-begin---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + /** + * 查询用户和部门的关系(使用批量预加载Map,消除N+1查询) + */ + private List getUserDepart(SysUser sysUser, Map> userDepartIdsMap, + Map departMap) { + List departIds = userDepartIdsMap.get(sysUser.getId()); + if (departIds == null || departIds.isEmpty()) { + return null; + } + List departList = departIds.stream() + .map(departMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return departList.isEmpty() ? null : departList; + } + //update-end---author:sjlei ---date:2026-04-17 for:【#9496】全量同步N+1查询性能优化----------- + + /** + * 【同步用户】将企业微信的User对象转为SysUser(创建新用户) + */ + private SysUser qwUserToSysUser(User user) { + SysUser sysUser = new SysUser(); + sysUser.setDelFlag(0); + sysUser.setStatus(1); + // 通过 username 来关联 + sysUser.setUsername(user.getUserid()); + // 密码默认为 “123456”,随机加盐 + String password = "123456", salt = oConvertUtils.randomGen(8); + String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(), password, salt); + sysUser.setSalt(salt); + sysUser.setPassword(passwordEncode); + return this.qwUserToSysUser(user, sysUser); + } + + /** + * 【同步用户】将企业微信的User对象转为SysUser(更新旧用户) + */ + private SysUser qwUserToSysUser(User qwUser, SysUser oldSysUser) { + SysUser sysUser = new SysUser(); + BeanUtils.copyProperties(oldSysUser, sysUser); + sysUser.setRealname(qwUser.getName()); + sysUser.setPost(qwUser.getPosition()); + // 设置工号,由于企业微信没有工号的概念,所以只能用 userId 代替 + if (oConvertUtils.isEmpty(sysUser.getWorkNo())) { + sysUser.setWorkNo(qwUser.getUserid()); + } + try { + sysUser.setSex(Integer.parseInt(qwUser.getGender())); + } catch (NumberFormatException ignored) { + } + // 因为唯一键约束的原因,如果原数据和旧数据相同,就不更新 + if (oConvertUtils.isNotEmpty(qwUser.getEmail()) && !qwUser.getEmail().equals(sysUser.getEmail())) { + sysUser.setEmail(qwUser.getEmail()); + } else { + sysUser.setEmail(null); + } + // 因为唯一键约束的原因,如果原数据和旧数据相同,就不更新 + if (oConvertUtils.isNotEmpty(qwUser.getMobile()) && !qwUser.getMobile().equals(sysUser.getPhone())) { + sysUser.setPhone(qwUser.getMobile()); + } else { + sysUser.setPhone(null); + } + + // 启用/禁用成员(状态),规则不同,需要转换 + // 企业微信规则:1表示启用成员,0表示禁用成员 + // Ghb规则:1正常,2冻结 + if (qwUser.getEnable() != null) { + sysUser.setStatus(qwUser.getEnable() == 1 ? 1 : 2); + } + // 座机号 + sysUser.setTelephone(qwUser.getTelephone()); + + // --- 企业微信没有逻辑删除的功能 + // sysUser.setDelFlag() + return sysUser; + } + + /** + * 【同步部门】将SysDepartTreeModel转为企业微信的Department对象(创建新部门) + */ + private Department sysDepartToQwDepartment(SysDepartTreeModel departTree, String parentId) { + Department department = new Department(); + return this.sysDepartToQwDepartment(departTree, department, parentId); + } + + /** + * 【同步部门】将SysDepartTreeModel转为企业微信的Department对象 + */ + private Department sysDepartToQwDepartment(SysDepartTreeModel departTree, Department department, String parentId) { + department.setName(departTree.getDepartName()); + department.setParentid(parentId); + if (departTree.getDepartOrder() != null) { + department.setOrder(departTree.getDepartOrder().toString()); + } + return department; + } + + + /** + * 【同步部门】将企业微信的Department对象转为SysDepart + */ + private SysDepart qwDepartmentToSysDepart(Department department, SysDepart oldSysDepart) { + SysDepart sysDepart = new SysDepart(); + if (oldSysDepart != null) { + BeanUtils.copyProperties(oldSysDepart, sysDepart); + } + sysDepart.setQywxIdentifier(department.getId()); + sysDepart.setDepartName(department.getName()); + try { + sysDepart.setDepartOrder(Integer.parseInt(department.getOrder())); + } catch (NumberFormatException ignored) { + } + return sysDepart; + } + + @Override + public int removeThirdAppUser(List userIdList) { + // 判断启用状态 + SysThirdAppConfig config = this.getWeChatThirdAppConfig(); + if (null == config) { + return -1; + } + int count = 0; + if (userIdList != null && userIdList.size() > 0) { + String accessToken = this.getAccessToken(); + if (accessToken == null) { + return count; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAccount::getThirdType, THIRD_TYPE); + queryWrapper.in(SysThirdAccount::getSysUserId, userIdList); + // 根据userId,获取第三方用户的id + List thirdAccountList = sysThirdAccountService.list(queryWrapper); + List thirdUserIdList = thirdAccountList.stream().map(SysThirdAccount::getThirdUserId).collect(Collectors.toList()); + + for (String thirdUserId : thirdUserIdList) { + if (oConvertUtils.isNotEmpty(thirdUserId)) { + // 没有批量删除的接口 + int err = JwUserAPI.deleteUser(thirdUserId, accessToken); + if (err == 0) { + count++; + } + } + } + } + return count; + } + + @Override + public boolean sendMessage(MessageDTO message) { + return this.sendMessage(message, false); + } + + @Override + public boolean sendMessage(MessageDTO message, boolean verifyConfig) { + JSONObject response; + if (message.isMarkdown()) { + response = this.sendMarkdownResponse(message, verifyConfig); + } else { + response = this.sendMessageResponse(message, verifyConfig); + } + if (response != null) { + return response.getIntValue("errcode") == 0; + } + return false; + } + + public JSONObject sendMessageResponse(MessageDTO message, boolean verifyConfig) { + SysThirdAppConfig config = this.getWeChatThirdAppConfig(); + if (verifyConfig && null == config) { + return null; + } + String accessToken = this.getAppAccessToken(config); + if (accessToken == null) { + return null; + } + Text text = new Text(); + text.setMsgtype("text"); + text.setTouser(this.getTouser(message.getToUser(), message.getToAll())); + TextEntity entity = new TextEntity(); + entity.setContent(message.getContent()); + text.setText(entity); + text.setAgentid(Integer.parseInt(config.getAgentId())); + return JwMessageAPI.sendTextMessage(text, accessToken); + } + + public JSONObject sendMarkdownResponse(MessageDTO message, boolean verifyConfig) { + SysThirdAppConfig config = this.getWeChatThirdAppConfig(); + if (verifyConfig && null == config) { + return null; + } + String accessToken = this.getAppAccessToken(config); + if (accessToken == null) { + return null; + } + Markdown markdown = new Markdown(); + markdown.setTouser(this.getTouser(message.getToUser(), message.getToAll())); + MarkdownEntity entity = new MarkdownEntity(); + entity.setContent(message.getContent()); + markdown.setMarkdown(entity); + markdown.setAgentid(Integer.parseInt(config.getAgentId())); + return JwMessageAPI.sendMarkdownMessage(markdown, accessToken); + } + + /** + * 发送文本卡片消息(SysAnnouncement定制) + * + * @param announcement + * @param verifyConfig 是否验证配置(未启用的APP会拒绝发送) + * @return + */ + public JSONObject sendTextCardMessage(SysAnnouncement announcement,String mobileOpenUrl, boolean verifyConfig) { + SysThirdAppConfig config = this.getWeChatThirdAppConfig(); + if (verifyConfig && null == config) { + return null; + } + String accessToken = this.getAppAccessToken(config); + if (accessToken == null) { + return null; + } + TextCard textCard = new TextCard(); + textCard.setAgentid(Integer.parseInt(config.getAgentId())); + boolean isToAll = CommonConstant.MSG_TYPE_ALL.equals(announcement.getMsgType()); + String usernameString = ""; + if (!isToAll) { + // 将userId转为username + String userId = announcement.getUserIds(); + String[] userIds = null; + if(oConvertUtils.isNotEmpty(userId)){ + userIds = userId.substring(0, (userId.length() - 1)).split(","); + }else{ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysAnnouncementSend::getAnntId, announcement.getId()); + SysAnnouncementSend sysAnnouncementSend = sysAnnouncementSendMapper.selectOne(queryWrapper); + userIds = new String[] {sysAnnouncementSend.getUserId()}; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getId, userIds); + List userList = userMapper.selectList(queryWrapper); + List usernameList = userList.stream().map(SysUser::getUsername).collect(Collectors.toList()); + usernameString = String.join(",", usernameList); + } + + textCard.setTouser(this.getTouser(usernameString, isToAll)); + TextCardEntity entity = new TextCardEntity(); + entity.setTitle(announcement.getTitile()); + + //update-begin---author:scott ---date:2025-08-05 for:【QQYUN-13257】【h5】催办、抄送消息,在企业微信中显示json乱码--- + // 判断announcement.getMsgAbstract()值是json格式 + if(oConvertUtils.isJson(announcement.getMsgAbstract()) && oConvertUtils.isNotEmpty(mobileOpenUrl)){ + entity.setDescription(announcement.getMsgContent()); + entity.setUrl(mobileOpenUrl); + }else{ + entity.setDescription(oConvertUtils.getString(announcement.getMsgAbstract(),"空")); + entity.setUrl(geQywxtAnnouncementUrl(announcement)); + } + + textCard.setTextcard(entity); + return JwMessageAPI.sendTextCardMessage(textCard, accessToken); + } + + + /** + * 获取企业微信的公告链接 + * + * @return + */ + private String geQywxtAnnouncementUrl(SysAnnouncement announcement){ + String baseUrl = null; + //优先通过请求获取basepath,获取不到读取 Ghb.domainUrl.pc + try { + baseUrl = RestUtil.getBaseUrl(); + } catch (Exception e) { + log.warn(e.getMessage()); + baseUrl = GhbBaseConfig.getDomainUrl().getPc(); + //e.printStackTrace(); + } + return baseUrl + "/sys/annountCement/show/" + announcement.getId(); + } + + private String getTouser(String origin, boolean toAll) { + if (toAll) { + return "@all"; + } else { + String[] toUsers = origin.split(","); + // 通过第三方账号表查询出第三方userId + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), CommonConstant.TENANT_ID_DEFAULT_VALUE); + List thirdAccountList = sysThirdAccountService.listThirdUserIdByUsername(toUsers, THIRD_TYPE,tenantId); + List toUserList = thirdAccountList.stream().map(SysThirdAccount::getThirdUserId).collect(Collectors.toList()); + // 多个接收者用‘|’分隔 + return String.join("|", toUserList); + } + } + + /** + * 根据第三方登录获取到的code来获取第三方app的用户ID + * + * @param code + * @return + */ + public Map getUserIdByThirdCode(String code, String accessToken) { + JSONObject response = JwUserAPI.getUserInfoByCode(code, accessToken); + if (response != null) { + Map map = new HashMap<>(5); + log.info("response: " + response.toJSONString()); + if (response.getIntValue(ERR_CODE) == 0) { + //将userTicket也返回,用于获取手机号 + String userTicket = response.getString("user_ticket"); + String appUserId = response.getString("UserId"); + map.put("userTicket",userTicket); + map.put("appUserId",appUserId); + return map; + } + } + return null; + } + + /** + * OAuth2登录,成功返回登录的SysUser,失败返回null + */ + public SysUser oauth2Login(String code,Integer tenantId) { + Long count = tenantMapper.tenantIzExist(tenantId); + if(ObjectUtil.isEmpty(count) || 0 == count){ + throw new GhbBootException("租户不存在!"); + } + // 代码逻辑说明: [QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------ + SysThirdAppConfig config = configMapper.getThirdConfigByThirdType(tenantId, MessageTypeEnum.QYWX.getType()); + String accessToken = this.getAppAccessToken(config); + if (accessToken == null) { + return null; + } + Map map = this.getUserIdByThirdCode(code, accessToken); + if (null != map) { + //企业微信需要通过userTicket获取用户信息 + String appUserId = map.get("appUserId"); + String userTicket = map.get("userTicket"); + // 判断第三方用户表有没有这个人 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysThirdAccount::getThirdUserId, appUserId); + queryWrapper.eq(SysThirdAccount::getThirdType, THIRD_TYPE); + queryWrapper.eq(SysThirdAccount::getTenantId, tenantId); + SysThirdAccount thirdAccount = sysThirdAccountService.getOne(queryWrapper); + if (thirdAccount != null) { + return this.getSysUserByThird(thirdAccount, null, appUserId, accessToken, userTicket,tenantId); + } else { + throw new GhbBootException("该用户尚未同步,请同步后再次登录!"); + } + } + return null; + } + + /** + * 根据第三方账号获取本地账号,如果不存在就创建 + * + * @param thirdAccount + * @param appUser + * @param appUserId + * @param accessToken + * @param userTicket 获取访问用户敏感信息 + * @return + */ + private SysUser getSysUserByThird(SysThirdAccount thirdAccount, User appUser, String appUserId, String accessToken, String userTicket,Integer tenantId) { + String sysUserId = thirdAccount.getSysUserId(); + if (oConvertUtils.isNotEmpty(sysUserId)) { + return userMapper.selectById(sysUserId); + } else { + // 如果没有 sysUserId ,说明没有绑定账号,获取到手机号之后进行绑定 + if (appUser == null) { + appUser = this.getUserByUserTicket(userTicket, accessToken); + } + // 判断系统里是否有这个手机号的用户 + SysUser sysUser = userMapper.getUserByPhone(appUser.getMobile()); + if (sysUser != null) { + thirdAccount.setAvatar(appUser.getAvatar()); + thirdAccount.setRealname(appUser.getName()); + thirdAccount.setThirdUserId(appUser.getUserid()); + thirdAccount.setThirdUserUuid(appUser.getUserid()); + thirdAccount.setSysUserId(sysUser.getId()); + sysThirdAccountService.updateById(thirdAccount); + return sysUser; + } else { + // 没有就走创建逻辑 + return sysThirdAccountService.createUser(appUser.getMobile(), appUser.getUserid(),tenantId); + } + + } + } + + /** + * 根据类型和租户id获取企业微信配置 + * @return + */ + private SysThirdAppConfig getWeChatThirdAppConfig(){ + int tenantId = oConvertUtils.getInt(TenantContext.getTenant(), 0); + return configMapper.getThirdConfigByThirdType(tenantId, MessageTypeEnum.QYWX.getType()); + } + + /** + * 获取企业微信第三方用户信息 + * @param userTicket + * @param accessToken + * @return + */ + private User getUserByUserTicket(String userTicket, String accessToken){ + Map map = new HashMap<>(5); + map.put("user_ticket",userTicket); + //建立连接 + CloseableHttpClient httpClient = null; + CloseableHttpResponse httpResponse = null; + try { + httpClient = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token="+accessToken); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(10000).setConnectionRequestTimeout(10000).setSocketTimeout(10000) + .build(); + httpPost.setConfig(requestConfig); + httpPost.setEntity(new StringEntity(JSONObject.toJSONString(map), ContentType.create("application/json", "utf-8"))); + httpResponse = httpClient.execute(httpPost); + // 从响应对象中获取响应内容 + HttpEntity entity = httpResponse.getEntity(); + String result = EntityUtils.toString(entity); + JSONObject jsonObject = JSONObject.parseObject(result); + Integer errcode = jsonObject.getInteger("errcode"); + if(0 == errcode){ + return JSONObject.toJavaObject(jsonObject, User.class); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 获取企业微信绑定的用户信息 + * @return + */ + public JwSysUserDepartVo getThirdUserByWechat(Integer tenantId) { + JwSysUserDepartVo sysUserDepartVo = new JwSysUserDepartVo(); + //step1 获取用户id和部门id + String accessToken = this.getAccessToken(); + if (accessToken == null) { + throw new GhbBootException("accessToken获取失败!"); + } + //获取当前租户下的用户 + List userList = sysUserTenantMapper.getUsersByTenantIdAndName(tenantId); + // 获取企业微信所有的用户(只能获取userid) + List qwUsers = JwUserAPI.getUsersByDepartid("1","1",null,accessToken); + if(oConvertUtils.isEmpty(qwUsers)){ + throw new GhbBootException("企业微信下没查询到用户!"); + } + List userIds = new ArrayList<>(); + List userWechatList = new ArrayList<>(); + + for (int i = 0; i < qwUsers.size(); i++) { + User user = qwUsers.get(i); + String userId = qwUsers.get(i).getUserid(); + //保证用户唯一 + if(!userIds.contains(userId)){ + //step2 查看是否已经同步过了,同步过的不做处理 + SysThirdAccount oneBySysUserId = sysThirdAccountService.getOneByUuidAndThirdType(userId, THIRD_TYPE,tenantId, userId); + if(null != oneBySysUserId){ + userIds.add(qwUsers.get(i).getUserid()); + userList = userList.stream().filter(item -> !item.getUserId().equals(oneBySysUserId.getSysUserId())).collect(Collectors.toList());; + continue; + } + AtomicBoolean excludeUser = new AtomicBoolean(false); + if(ObjectUtil.isNotEmpty(qwUsers)){ + //step3 通过名称匹配敲敲云 + userList.forEach(item ->{ + if(item.getRealName().equals(user.getName())){ + item.setWechatUserId(user.getUserid()); + item.setWechatRealName(user.getName()); + if(ObjectUtil.isNotEmpty(user.getDepartment())){ + item.setWechatDepartId(Arrays.toString(user.getDepartment())); + } + excludeUser.set(true); + } + }); + userIds.add(user.getUserid()); + } + if(!excludeUser.get()){ + JwUserDepartVo userDepartVo = new JwUserDepartVo(); + userDepartVo.setWechatRealName(user.getName()); + userDepartVo.setWechatUserId(user.getUserid()); + if(ObjectUtil.isNotEmpty(user.getDepartment())){ + userDepartVo.setWechatDepartId(Arrays.toString(user.getDepartment())); + } + userWechatList.add(userDepartVo); + } + } + } + //step4 返回用户信息 + sysUserDepartVo.setUserList(userWechatList); + sysUserDepartVo.setJwUserDepartVos(userList); + return sysUserDepartVo; + } + + /** + * 同步企业微信和部门 + * @param jwUserDepartJson + * @return + */ + public SyncInfoVo syncWechatEnterpriseDepartAndUserToLocal(String jwUserDepartJson, Integer tenantId) { + //step 1 同步部门 + //存放部门id的map + Map idsMap = new HashMap<>(); + SyncInfoVo syncInfoVo = this.syncThirdAppDepartmentToLocal(tenantId, idsMap); + //step 2 同步用户及用户部门 + this.syncDepartAndUser(syncInfoVo, tenantId, idsMap, jwUserDepartJson); + //step 3 返回同步成功或者同步失败的消息 + return syncInfoVo; + } + + /** + * 同步用户和部门 + * @param syncInfoVo 存放错误信息的日志 + * @param tenantId 租户id + * @param idsMap 部门id集合 key为企业微信的id value 为系统部门的id + * @param jwUserDepartJson + */ + private void syncDepartAndUser(SyncInfoVo syncInfoVo, Integer tenantId, Map idsMap, String jwUserDepartJson) { + if (oConvertUtils.isNotEmpty(jwUserDepartJson)) { + JSONArray jsonArray = JSONObject.parseArray(jwUserDepartJson); + for (Object object : jsonArray) { + JSONObject jsonObject = JSONObject.parseObject(object.toString()); + Object userId = jsonObject.get("userId"); + String wechatUserId = jsonObject.getString("wechatUserId"); + String wechatRealName = jsonObject.getString("wechatRealName"); + Object wechatDepartId = jsonObject.get("wechatDepartId"); + String sysUserId = ""; + //step 1 新建或更新用户 + //用户id为空说明需要创建用户 + if (null == userId) { + SysTenant sysTenant = sysTenantMapper.selectById(tenantId); + String houseNumber = ""; + //空说明没有租户直接用用户名 + if (null != sysTenant) { + houseNumber = sysTenant.getHouseNumber(); + } + //用户名和密码用门牌号+用户id的格式,避免用户名重复 + String username = houseNumber + wechatUserId; + //新建用户 + sysUserId = this.saveUser(username, wechatRealName, syncInfoVo, wechatUserId); + } else { + //根据id查询用户 + SysUser sysUser = userMapper.selectById(userId.toString()); + if (null != sysUser) { + sysUserId = sysUser.getId(); + //如果真实姓名为空的情况下,才会改真实姓名 + if(oConvertUtils.isEmpty(sysUser.getRealname())){ + sysUser.setRealname(wechatRealName); + //更新用户 + userMapper.updateById(sysUser); + } + String str = String.format("用户 %s(%s) 更新成功!", sysUser.getRealname(), sysUser.getUsername()); + syncInfoVo.addSuccessInfo(str); + }else{ + syncInfoVo.addFailInfo("企业微信用户 "+wechatRealName+" 对应的组织用户没有匹配到!"); + continue; + } + } + if (oConvertUtils.isNotEmpty(sysUserId)) { + //step 2 新增租户用户表 + this.createUserTenant(sysUserId,false,tenantId); + //step 3 新建或更新第三方账号表 + SysThirdAccount sysThirdAccount = sysThirdAccountService.getOneByUuidAndThirdType(wechatUserId, THIRD_TYPE, tenantId, wechatUserId); + this.thirdAccountSaveOrUpdate(sysThirdAccount,sysUserId,wechatUserId,wechatRealName,tenantId); + //step 4 新建或更新用户部门关系表 + if(oConvertUtils.isNotEmpty(wechatDepartId)){ + String wechatDepartIds = wechatDepartId.toString(); + String[] departIds = wechatDepartIds.substring(1, wechatDepartIds.length() - 1).split(","); + this.userDepartSaveOrUpdate(idsMap,sysUserId,departIds); + } + } + } + } else { + syncInfoVo.addFailInfo("用户同同步失败,请查看企业微信是否存在用户!"); + } + + } + + /** + * 保存用户 + * + * @param username 用户名 + * @param wechatRealName 企业微信用户真实姓名 + * @param syncInfo 存放成功或失败的信息 + * @param wechatUserId wechatUserId 企业微信对应的id + * @return + */ + private String saveUser(String username, String wechatRealName, SyncInfoVo syncInfo, String wechatUserId) { + SysUser sysUser = new SysUser(); + sysUser.setRealname(wechatRealName); + sysUser.setPassword(username); + sysUser.setUsername(username); + sysUser.setDelFlag(CommonConstant.DEL_FLAG_0); + //设置创建时间 + sysUser.setCreateTime(new Date()); + String salt = oConvertUtils.randomGen(8); + sysUser.setSalt(salt); + String passwordEncode = PasswordUtil.encrypt(sysUser.getUsername(), sysUser.getPassword(), salt); + sysUser.setPassword(passwordEncode); + sysUser.setStatus(1); + sysUser.setDelFlag(CommonConstant.DEL_FLAG_0); + //用户表字段org_code不能在这里设置他的值 + sysUser.setOrgCode(null); + try { + userMapper.insert(sysUser); + String str = String.format("用户 %s(%s) 创建成功!", sysUser.getRealname(), sysUser.getUsername()); + syncInfo.addSuccessInfo(str); + return sysUser.getId(); + } catch (Exception e) { + User user = new User(); + user.setUserid(wechatUserId); + user.setName(wechatRealName); + this.syncUserCollectErrInfo(e, user, syncInfo); + } + + return ""; + } + + /** + * 新增用户租户 + * + * @param userId + * @param isUpdate 是否是新增 + * @param tenantId + */ + private void createUserTenant(String userId, Boolean isUpdate, Integer tenantId) { + if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) { + //判断当前用户是否已在该租户下面 + Integer count = sysUserTenantMapper.userTenantIzExist(userId, tenantId); + //count 为0 新增租户用户,否则不用新增 + if (count == 0) { + SysUserTenant userTenant = new SysUserTenant(); + userTenant.setTenantId(tenantId); + userTenant.setUserId(userId); + userTenant.setStatus(isUpdate ? CommonConstant.USER_TENANT_UNDER_REVIEW : CommonConstant.USER_TENANT_NORMAL); + sysUserTenantMapper.insert(userTenant); + } + } + } + + /** + * 新建或更新用户部门关系表 + * @param idsMap 部门id集合 key为企业微信的id value 为系统部门的id + * @param sysUserId 系统对应的用户id + */ + private void userDepartSaveOrUpdate(Map idsMap, String sysUserId, String[] departIds) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(SysUserDepart::getUserId,sysUserId); + for (String departId:departIds) { + departId = departId.trim(); + if(idsMap.containsKey(departId)){ + String value = idsMap.get(departId); + //查询用户是否在部门里面 + query.eq(SysUserDepart::getDepId,value); + long count = sysUserDepartService.count(query); + if(count == 0){ + //不存在,则新增部门用户关系 + SysUserDepart sysUserDepart = new SysUserDepart(null,sysUserId,value); + sysUserDepartService.save(sysUserDepart); + } + } + } + } + + public List getThirdUserBindByWechat(int tenantId) { + return sysThirdAccountMapper.getThirdUserBindByWechat(tenantId,THIRD_TYPE); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/FindsDepartsChildrenUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/FindsDepartsChildrenUtil.java new file mode 100644 index 0000000..b6286f5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/FindsDepartsChildrenUtil.java @@ -0,0 +1,130 @@ +package com.ghb.base.modules.system.util; + +import com.ghb.base.common.constant.CommonConstant; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysDepart; +import com.ghb.base.modules.system.model.DepartIdModel; +import com.ghb.base.modules.system.model.SysDepartTreeModel; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * 对应部门的表,处理并查找树级数据 + *

+ * + * @Author: Steve + * @Date: 2019-01-22 + */ +public class FindsDepartsChildrenUtil { + + //部门树信息-树结构 + //private static List sysDepartTreeList = new ArrayList(); + + //部门树id-树结构 + //private static List idList = new ArrayList<>(); + + + /** + * queryTreeList的子方法 ====1===== + * 该方法是s将SysDepart类型的list集合转换成SysDepartTreeModel类型的集合 + */ + public static List wrapTreeDataToTreeList(List recordList) { + // 在该方法每请求一次,都要对全局list集合进行一次清理 + //idList.clear(); + List idList = new ArrayList(); + List records = new ArrayList<>(); + for (int i = 0; i < recordList.size(); i++) { + SysDepart depart = recordList.get(i); + records.add(new SysDepartTreeModel(depart)); + } + List tree = findChildren(records, idList); + setEmptyChildrenAsNull(tree); + return tree; + } + + /** + * 获取 DepartIdModel + * @param recordList + * @return + */ + public static List wrapTreeDataToDepartIdTreeList(List recordList) { + // 在该方法每请求一次,都要对全局list集合进行一次清理 + //idList.clear(); + List idList = new ArrayList(); + List records = new ArrayList<>(); + for (int i = 0; i < recordList.size(); i++) { + SysDepart depart = recordList.get(i); + records.add(new SysDepartTreeModel(depart)); + } + findChildren(records, idList); + return idList; + } + + /** + * queryTreeList的子方法 ====2===== + * 该方法是找到并封装顶级父类的节点到TreeList集合 + */ + private static List findChildren(List recordList, + List departIdList) { + + List treeList = new ArrayList<>(); + for (int i = 0; i < recordList.size(); i++) { + SysDepartTreeModel branch = recordList.get(i); + if (oConvertUtils.isEmpty(branch.getParentId())) { + treeList.add(branch); + DepartIdModel departIdModel = new DepartIdModel().convert(branch); + departIdList.add(departIdModel); + } + } + getGrandChildren(treeList,recordList,departIdList); + + //idList = departIdList; + return treeList; + } + + /** + * queryTreeList的子方法====3==== + *该方法是找到顶级父类下的所有子节点集合并封装到TreeList集合 + */ + private static void getGrandChildren(List treeList,List recordList,List idList) { + + for (int i = 0; i < treeList.size(); i++) { + SysDepartTreeModel model = treeList.get(i); + DepartIdModel idModel = idList.get(i); + for (int i1 = 0; i1 < recordList.size(); i1++) { + SysDepartTreeModel m = recordList.get(i1); + if (m.getParentId()!=null && m.getParentId().equals(model.getId())) { + model.getChildren().add(m); + DepartIdModel dim = new DepartIdModel().convert(m); + idModel.getChildren().add(dim); + } + } + getGrandChildren(treeList.get(i).getChildren(), recordList, idList.get(i).getChildren()); + } + + } + + + /** + * queryTreeList的子方法 ====4==== + * 该方法是将子节点为空的List集合设置为Null值 + */ + private static void setEmptyChildrenAsNull(List treeList) { + + for (int i = 0; i < treeList.size(); i++) { + SysDepartTreeModel model = treeList.get(i); + if (model.getChildren().size() == 0) { + model.setChildren(null); + model.setIsLeaf(true); + }else{ + setEmptyChildrenAsNull(model.getChildren()); + model.setIsLeaf(false); + } + } + // sysDepartTreeList = treeList; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/HttpFileToMultipartFileUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/HttpFileToMultipartFileUtil.java new file mode 100644 index 0000000..3489655 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/HttpFileToMultipartFileUtil.java @@ -0,0 +1,98 @@ +package com.ghb.base.modules.system.util; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import com.ghb.base.common.util.MyCommonsMultipartFile; +import com.ghb.base.common.util.filter.SsrfFileTypeFilter; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * @Description: http文件转MultipartFile + * @author: wangshuai + * @date: 2025/11/5 17:55 + */ +public class HttpFileToMultipartFileUtil { + + /** + * 获取 + * + * @param fileUrl + * @param filename + * @return + * @throws Exception + */ + public static MultipartFile httpFileToMultipartFile(String fileUrl, String filename) throws Exception { + SsrfFileTypeFilter.checkSsrfHttpUrl(fileUrl); + byte[] bytes = downloadImageData(fileUrl); + return convertByteToMultipartFile(bytes, filename); + } + + /** + * 下载图片数据 + */ + private static byte[] downloadImageData(String fileUrl) throws IOException { + URL url = new URL(fileUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(10000); + connection.setRequestProperty("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + connection.setRequestProperty("Accept", "image/*"); + + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new IOException("HTTP请求失败,响应码: " + responseCode); + } + + try (InputStream inputStream = connection.getInputStream(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + + byte[] buffer = new byte[4096]; + int bytesRead; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + + return outputStream.toByteArray(); + } finally { + connection.disconnect(); + } + } + + /** + * byte转 MultipartFile + * + * @param data + * @param fileName + * @return + */ + private static MultipartFile convertByteToMultipartFile(byte[] data, String fileName) { + FileItemFactory factory = new DiskFileItemFactory(); + FileItem item = factory.createItem(fileName, "application/octet-stream", true, fileName); + + try (OutputStream os = item.getOutputStream(); + ByteArrayInputStream bis = new ByteArrayInputStream(data)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = bis.read(buffer)) != -1) { + os.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + throw new RuntimeException("字节数组转换失败", e); + } + + try { + return new MyCommonsMultipartFile(item); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/ImportOldUserUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/ImportOldUserUtil.java new file mode 100644 index 0000000..0686a93 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/ImportOldUserUtil.java @@ -0,0 +1,112 @@ +package com.ghb.base.modules.system.util; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.util.ImportExcelUtil; +import com.ghb.base.common.util.PasswordUtil; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysUser; +import com.ghb.base.modules.system.entity.SysUserDepart; +import com.ghb.base.modules.system.service.ISysUserDepartService; +import com.ghb.base.modules.system.service.ISysUserService; +import com.ghb.base.modules.system.service.impl.SysUserDepartServiceImpl; +import com.ghb.base.modules.system.service.impl.SysUserServiceImpl; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @Description: 旧版导入 + * @author: wangshuai + * @date: 2025/4/2 10:19 + */ +@Slf4j +public class ImportOldUserUtil { + + public static Result importOldSysUser(HttpServletRequest request) throws IOException { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + // 错误信息 + List errorMessage = new ArrayList<>(); + int successLines = 0, errorLines = 0; + for (Map.Entry entity : fileMap.entrySet()) { + MultipartFile file = entity.getValue();// 获取上传文件对象 + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List listSysUsers = ExcelImportUtil.importExcel(file.getInputStream(), SysUser.class, params); + for (int i = 0; i < listSysUsers.size(); i++) { + SysUser sysUserExcel = listSysUsers.get(i); + if (StringUtils.isBlank(sysUserExcel.getPassword())) { + // 密码默认为 “123456” + sysUserExcel.setPassword("123456"); + } + // 密码加密加盐 + String salt = oConvertUtils.randomGen(8); + sysUserExcel.setSalt(salt); + String passwordEncode = PasswordUtil.encrypt(sysUserExcel.getUsername(), sysUserExcel.getPassword(), salt); + sysUserExcel.setPassword(passwordEncode); + try { + ISysUserService service = SpringContextUtils.getBean(SysUserServiceImpl.class); + service.save(sysUserExcel); + successLines++; + } catch (Exception e) { + errorLines++; + String message = e.getMessage().toLowerCase(); + int lineNumber = i + 1; + // 通过索引名判断出错信息 + if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_USERNAME)) { + errorMessage.add("第 " + lineNumber + " 行:用户名已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_WORK_NO)) { + errorMessage.add("第 " + lineNumber + " 行:工号已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_PHONE)) { + errorMessage.add("第 " + lineNumber + " 行:手机号已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER_EMAIL)) { + errorMessage.add("第 " + lineNumber + " 行:电子邮件已经存在,忽略导入。"); + } else if (message.contains(CommonConstant.SQL_INDEX_UNIQ_SYS_USER)) { + errorMessage.add("第 " + lineNumber + " 行:违反表唯一性约束。"); + } else { + errorMessage.add("第 " + lineNumber + " 行:未知错误,忽略导入"); + log.error(e.getMessage(), e); + } + } + // 批量将部门和用户信息建立关联关系 + String departIds = sysUserExcel.getDepartIds(); + if (StringUtils.isNotBlank(departIds)) { + String userId = sysUserExcel.getId(); + String[] departIdArray = departIds.split(","); + List userDepartList = new ArrayList<>(departIdArray.length); + for (String departId : departIdArray) { + userDepartList.add(new SysUserDepart(userId, departId)); + } + ISysUserDepartService service = SpringContextUtils.getBean(SysUserDepartServiceImpl.class); + service.saveBatch(userDepartList); + } + + } + } catch (Exception e) { + errorMessage.add("发生异常:" + e.getMessage()); + log.error(e.getMessage(), e); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + return ImportExcelUtil.imporReturnRes(errorLines, successLines, errorMessage); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/ImportSysUserCache.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/ImportSysUserCache.java new file mode 100644 index 0000000..8e4916b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/ImportSysUserCache.java @@ -0,0 +1,53 @@ +package com.ghb.base.modules.system.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Description: 导入缓存类,为了前台显示进度 + * @author: wangshuai + * @date: 2025/9/6 14:09 + */ +public class ImportSysUserCache { + + private static final Map importSysUserMap = new HashMap<>(); + + /** + * 获取导入的列 + * + * @param key + * @param type user 用户 可扩展 + * @return + */ + public static Double getImportSysUserMap(String key, String type) { + if (importSysUserMap.containsKey(key + "__" + type)) { + return importSysUserMap.get(key + "__" + type); + } + return 0.0; + } + + /** + * 设置导入缓存 + * + * @param key 前村传过来的随机key + * @param num 导入行数 + * @param length 总长度 + * @param type 导入类型 user 用户列表 + */ + public static void setImportSysUserMap(String key, int num, int length, String type) { + double percent = (num * 100.0) / length; + if(num == length){ + percent = 100; + } + importSysUserMap.put(key + "__" + type, percent); + } + + /** + * 移除导入缓存 + * + * @param key + */ + public static void removeImportLowAppMap(String key) { + importSysUserMap.remove(key); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/PermissionDataUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/PermissionDataUtil.java new file mode 100644 index 0000000..8c4b49d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/PermissionDataUtil.java @@ -0,0 +1,139 @@ +package com.ghb.base.modules.system.util; + +import com.ghb.base.common.constant.CommonConstant; +import com.ghb.base.common.constant.SymbolConstant; +import com.ghb.base.common.util.SpringContextUtils; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysPermission; +import com.ghb.base.modules.system.entity.SysRoleIndex; +import com.ghb.base.modules.system.service.ISysRoleIndexService; + +import java.util.List; + +/** + * @Author: scott + * @Date: 2019-04-03 + */ +public class PermissionDataUtil { + + /** + * 路径:views/ + */ + private static final String PATH_VIEWS = "views/"; + + /** + * 路径:src/views/ + */ + private static final String PATH_SRC_VIEWS = "src/views/"; + + /** + * .vue后缀 + */ + private static final String VUE_SUFFIX = ".vue"; + + /** + * 智能处理错误数据,简化用户失误操作 + * + * @param permission + */ + public static SysPermission intelligentProcessData(SysPermission permission) { + if (permission == null) { + return null; + } + + // 组件 + if (oConvertUtils.isNotEmpty(permission.getComponent())) { + String component = permission.getComponent(); + if (component.startsWith(SymbolConstant.SINGLE_SLASH)) { + component = component.substring(1); + } + if (component.startsWith(PATH_VIEWS)) { + component = component.replaceFirst(PATH_VIEWS, ""); + } + if (component.startsWith(PATH_SRC_VIEWS)) { + component = component.replaceFirst(PATH_SRC_VIEWS, ""); + } + if (component.endsWith(VUE_SUFFIX)) { + component = component.replace(VUE_SUFFIX, ""); + } + permission.setComponent(component); + } + + // 请求URL + if (oConvertUtils.isNotEmpty(permission.getUrl())) { + String url = permission.getUrl(); + if (url.endsWith(VUE_SUFFIX)) { + url = url.replace(VUE_SUFFIX, ""); + } + if (!url.startsWith(CommonConstant.STR_HTTP) && !url.startsWith(SymbolConstant.SINGLE_SLASH)&&!url.trim().startsWith(SymbolConstant.DOUBLE_LEFT_CURLY_BRACKET)) { + url = SymbolConstant.SINGLE_SLASH + url; + } + permission.setUrl(url); + } + + // 一级菜单默认组件 + if (0 == permission.getMenuType() && oConvertUtils.isEmpty(permission.getComponent())) { + // 一级菜单默认组件 + permission.setComponent("layouts/RouteView"); + } + return permission; + } + + /** + * 如果没有index页面 需要new 一个放到list中 + * @param metaList + */ + public static void addIndexPage(List metaList) { + boolean hasIndexMenu = false; + SysRoleIndex defIndexCfg = PermissionDataUtil.getDefIndexConfig(); + for (SysPermission sysPermission : metaList) { + if(defIndexCfg.getUrl().equals(sysPermission.getUrl())) { + hasIndexMenu = true; + break; + } + } + if(!hasIndexMenu) { + metaList.add(0,new SysPermission(true)); + } + } + + /** + * 判断是否授权首页 + * @param metaList + * @return + */ + public static boolean hasIndexPage(List metaList, SysRoleIndex defIndexCfg){ + boolean hasIndexMenu = false; + for (SysPermission sysPermission : metaList) { + if(defIndexCfg.getUrl().equals(sysPermission.getUrl())) { + hasIndexMenu = true; + break; + } + } + return hasIndexMenu; + } + + /** + * 通过id判断是否授权某个页面 + * + * @param metaList + * @return + */ + public static boolean hasMenuById(List metaList, String id) { + for (SysPermission sysPermission : metaList) { + if (id.equals(sysPermission.getId())) { + return true; + } + } + return false; + } + + /** + * 获取默认首页配置 + */ + public static SysRoleIndex getDefIndexConfig() { + ISysRoleIndexService sysRoleIndexService = SpringContextUtils.getBean(ISysRoleIndexService.class); + return sysRoleIndexService.queryDefaultIndex(); + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/RandImageUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/RandImageUtil.java new file mode 100644 index 0000000..26e64df --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/RandImageUtil.java @@ -0,0 +1,303 @@ +package com.ghb.base.modules.system.util; + +import javax.imageio.ImageIO; +import jakarta.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Random; + +/** + * 登录验证码工具类 + * @date 2025-09-11 + * @author AI优化 + */ +public class RandImageUtil { + + // 静态初始化块,解决无头环境字体配置问题 + static { + System.setProperty("java.awt.headless", "true"); + } + + public static final String KEY = "Ghb_LOGIN_KEY"; + + /** 验证码图片宽度 */ + private static final int WIDTH = 105; + + /** 验证码图片高度 */ + private static final int HEIGHT = 35; + + /** 干扰线数量 */ + private static final int INTERFERENCE_LINE_COUNT = 200; + + /** 干扰线宽度 */ + private static final int LINE_WIDTH = 2; + + /** 图片格式 */ + private static final String IMG_FORMAT = "JPEG"; + + /** base64 图片前缀 */ + private static final String BASE64_PREFIX = "data:image/jpg;base64,"; + + /** 字符间距 */ + private static final int CHAR_SPACING = 23; + + /** 字体大小 */ + private static final int FONT_SIZE = 24; + + /** 字符Y轴偏移 */ + private static final int CHAR_Y_OFFSET = 26; + + /** 字符X轴起始偏移 */ + private static final int CHAR_X_OFFSET = 8; + + /** + * 直接通过response输出验证码图片 + * + * @param response HTTP响应对象 + * @param verifyCode 验证码字符串 + * @throws IOException 输出异常 + */ + public static void generate(HttpServletResponse response, String verifyCode) throws IOException { + if (response == null || verifyCode == null || verifyCode.trim().isEmpty()) { + throw new IllegalArgumentException("参数不能为空"); + } + + try { + BufferedImage image = createVerifyCodeImage(verifyCode); + ImageIO.write(image, IMG_FORMAT, response.getOutputStream()); + } catch (Exception e) { + throw new IOException("生成验证码图片失败", e); + } + } + + /** + * 生成验证码的base64字符串 + * + * @param verifyCode 验证码字符串 + * @return base64编码的图片字符串 + * @throws IOException 生成异常 + */ + public static String generate(String verifyCode) throws IOException { + if (verifyCode == null || verifyCode.trim().isEmpty()) { + throw new IllegalArgumentException("验证码不能为空"); + } + + try { + BufferedImage image = createVerifyCodeImage(verifyCode); + + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream()) { + ImageIO.write(image, IMG_FORMAT, byteStream); + byte[] bytes = byteStream.toByteArray(); + String base64 = Base64.getEncoder().encodeToString(bytes).trim(); + // 清理换行符 + base64 = base64.replaceAll("[\r\n]", ""); + return BASE64_PREFIX + base64; + } + } catch (Exception e) { + throw new IOException("生成验证码base64失败", e); + } + } + + /** + * 创建验证码图像 + * + * @param verifyCode 验证码字符串 + * @return 验证码图像 + */ + private static BufferedImage createVerifyCodeImage(String verifyCode) { + BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = null; + + try { + graphics = (Graphics2D) image.getGraphics(); + + // 设置图形渲染质量 + setupRenderingHints(graphics); + + // 绘制背景 + drawBackground(graphics); + + // 绘制边框 + drawBorder(graphics); + + // 获取安全的随机数生成器 + SecureRandom random = createSecureRandom(); + + // 绘制干扰线 + drawInterferenceLines(graphics, random); + + // 绘制验证码字符 + drawVerifyCodeText(graphics, verifyCode); + + } catch (Exception e) { + // 如果绘制失败,创建简单的错误图像 + return createErrorImage(verifyCode); + } finally { + if (graphics != null) { + graphics.dispose(); + } + } + + return image; + } + + /** + * 设置图形渲染质量 + */ + private static void setupRenderingHints(Graphics2D graphics) { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + } + + /** + * 绘制白色背景 + */ + private static void drawBackground(Graphics2D graphics) { + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, WIDTH, HEIGHT); + } + + /** + * 绘制边框 + */ + private static void drawBorder(Graphics2D graphics) { + graphics.setColor(Color.GRAY); + graphics.drawRect(0, 0, WIDTH - 1, HEIGHT - 1); + } + + /** + * 创建安全的随机数生成器 + */ + private static SecureRandom createSecureRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + return new SecureRandom(); + } + } + + /** + * 绘制干扰线 + */ + private static void drawInterferenceLines(Graphics2D graphics, SecureRandom random) { + for (int i = 0; i < INTERFERENCE_LINE_COUNT; i++) { + graphics.setColor(getRandomColor(150, 200, random)); + + // 确保干扰线在边框内 + int x1 = random.nextInt(WIDTH - LINE_WIDTH - 1) + 1; + int y1 = random.nextInt(HEIGHT - LINE_WIDTH - 1) + 1; + int x2 = x1 + random.nextInt(LINE_WIDTH); + int y2 = y1 + random.nextInt(LINE_WIDTH); + + graphics.drawLine(x1, y1, x2, y2); + } + } + + /** + * 绘制验证码文本 + */ + private static void drawVerifyCodeText(Graphics2D graphics, String verifyCode) { + graphics.setColor(Color.BLACK); + Font font = createSafeFont(); + graphics.setFont(font); + + for (int i = 0; i < verifyCode.length(); i++) { + char character = verifyCode.charAt(i); + int x = i * CHAR_SPACING + CHAR_X_OFFSET; + graphics.drawString(String.valueOf(character), x, CHAR_Y_OFFSET); + } + } + + /** + * 创建安全的字体,避免字体配置问题 + */ + private static Font createSafeFont() { + // 使用逻辑字体名称,在所有平台都可用 + String[] safeFontNames = {Font.SERIF, Font.SANS_SERIF, Font.MONOSPACED, "Dialog"}; + + for (String fontName : safeFontNames) { + try { + Font font = new Font(fontName, Font.BOLD, FONT_SIZE); + if (font.getFamily() != null) { + return font; + } + } catch (Exception e) { + // 继续尝试下一个字体 + } + } + + // 最后的回退方案 + return new Font(Font.MONOSPACED, Font.BOLD, FONT_SIZE); + } + + /** + * 创建错误图像(当正常绘制失败时使用) + */ + private static BufferedImage createErrorImage(String verifyCode) { + BufferedImage errorImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = null; + + try { + graphics = (Graphics2D) errorImage.getGraphics(); + + // 白色背景 + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, WIDTH, HEIGHT); + + // 黑色边框 + graphics.setColor(Color.BLACK); + graphics.drawRect(0, 0, WIDTH - 1, HEIGHT - 1); + + // 尝试绘制验证码 + try { + graphics.setFont(new Font(Font.MONOSPACED, Font.BOLD, 20)); + graphics.setColor(Color.BLUE); + for (int i = 0; i < verifyCode.length(); i++) { + graphics.drawString(String.valueOf(verifyCode.charAt(i)), + i * CHAR_SPACING + CHAR_X_OFFSET, CHAR_Y_OFFSET); + } + } catch (Exception fontException) { + // 如果连基本字体都失败,显示ERROR + graphics.setColor(Color.RED); + graphics.drawString("ERROR", 10, 20); + } + } finally { + if (graphics != null) { + graphics.dispose(); + } + } + + return errorImage; + } + + /** + * 获取指定范围内的随机颜色 + * + * @param minColorValue 最小颜色值 + * @param maxColorValue 最大颜色值 + * @param random 随机数生成器 + * @return 随机颜色 + */ + private static Color getRandomColor(int minColorValue, int maxColorValue, Random random) { + // 确保颜色值在有效范围内 + int min = Math.max(0, Math.min(minColorValue, 255)); + int max = Math.max(min, Math.min(maxColorValue, 255)); + + int range = max - min; + if (range == 0) { + return new Color(min, min, min); + } + + int red = min + random.nextInt(range); + int green = min + random.nextInt(range); + int blue = min + random.nextInt(range); + + return new Color(red, green, blue); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/SecurityUtil.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/SecurityUtil.java new file mode 100644 index 0000000..53f6f32 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/SecurityUtil.java @@ -0,0 +1,51 @@ +package com.ghb.base.modules.system.util; + + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; +import cn.hutool.crypto.symmetric.SymmetricCrypto; + +/** + * @Description: 密码加密解密 + * @author: lsq + * @date: 2020年09月07日 14:26 + */ +public class SecurityUtil { + /**加密key*/ + private static String key = "GhbBOOT1423670"; + + //---AES加密---------begin--------- + /**加密 + * @param content + * @return + */ + public static String jiami(String content) { + SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key.getBytes()); + String encryptResultStr = aes.encryptHex(content); + return encryptResultStr; + } + + /**解密 + * @param encryptResultStr + * @return + */ + public static String jiemi(String encryptResultStr){ + SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key.getBytes()); + //解密为字符串 + String decryptResult = aes.decryptStr(encryptResultStr, CharsetUtil.CHARSET_UTF_8); + return decryptResult; + } + //---AES加密---------end--------- + /** + * 主函数 + */ + public static void main(String[] args) { + String content="test1111"; + String encrypt = jiami(content); + System.out.println(encrypt); + //构建 + String decrypt = jiemi(encrypt); + //解密为字符串 + System.out.println(decrypt); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/XssUtils.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/XssUtils.java new file mode 100644 index 0000000..231e5a1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/util/XssUtils.java @@ -0,0 +1,86 @@ +package com.ghb.base.modules.system.util; + +import org.springframework.web.util.HtmlUtils; + +import java.util.regex.Pattern; + +/** + * @Description: 工具类XSSUtils,现在的做法是替换成空字符,CSDN的是进行转义,比如文字开头的"<"转成< + * @author: lsq + * @date: 2021年07月26日 19:13 + */ +public class XssUtils { + + private static Pattern[] patterns = new Pattern[]{ + //Script fragments + Pattern.compile("", Pattern.CASE_INSENSITIVE), + //src='...' + Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), + Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), + //script tags + Pattern.compile("", Pattern.CASE_INSENSITIVE), + Pattern.compile("", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), + //eval(...) + Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), + //expression(...) + Pattern.compile("e­xpression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), + //javascript:... + Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE), + //vbscript:... + Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE), + //onload(...)=... + Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL), + }; + + //update-begin---author:liusq ---date:2025-04-13 for:【issues/9521】富文本msgContent字段存储型XSS过滤----------- + /** + * 针对富文本HTML内容的XSS过滤:移除危险脚本和事件处理器,保留合法HTML标签和样式。 + * 与 scriptXss() 的区别:本方法不对HTML实体进行全局转义,适用于富文本内容(如公告正文)。 + */ + private static final Pattern[] RICH_TEXT_PATTERNS = new Pattern[]{ + // ", Pattern.CASE_INSENSITIVE), + // 自闭合 javascript:eval()\\\\."); + System.err.println("s======>" + s); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysChangeDepartVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysChangeDepartVo.java new file mode 100644 index 0000000..4113db8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysChangeDepartVo.java @@ -0,0 +1,32 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +/** +* @Description: 部门修改替换类 +* +* @author: wangshuai +* @date: 2025/9/28 18:52 +*/ +@Data +public class SysChangeDepartVo { + /** + * 最终停止的部门id + */ + private String dropId; + + /** + * 拖拽的部门id + */ + private String dragId; + + /** + * 拖拽位置(-1上方 1下方 0子级) + */ + private Integer dropPosition; + + /** + * 当前位置 + */ + private Integer sort; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysCommentFileVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysCommentFileVo.java new file mode 100644 index 0000000..8b74c11 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysCommentFileVo.java @@ -0,0 +1,44 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +/** + * @Description: 文档VO + * @Author: Ghb-boot + * @Date: 2022-07-21 + * @Version: V1.0 + */ +@Data +public class SysCommentFileVo { + + /** + * sys_files id + */ + private String fileId; + /** + * sys_form_file id + */ + private String sysFormFileId; + /** + * 文件名称 + */ + private String name; + + private Double fileSize; + + /** + * 文件地址 + */ + private String url; + + /** + * 文档类型(folder:文件夹 excel:excel doc:word pp:ppt image:图片 archive:其他文档 video:视频) + */ + private String type; + + /** + * 文件上传类型(temp/本地上传(临时文件) manage/知识库) + */ + private String storeType; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysCommentVO.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysCommentVO.java new file mode 100644 index 0000000..b6c99f6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysCommentVO.java @@ -0,0 +1,91 @@ +package com.ghb.base.modules.system.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * @Description: VO 评论信息+文件信息 + * @Author: Ghb-boot + * @Date: 2022-07-19 + * @Version: V1.0 + */ +@Data +public class SysCommentVO implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * id + */ + private String id; + /** + * 表名 + */ + private String tableName; + /** + * 数据id + */ + private String tableDataId; + /** + * 来源用户id + */ + private String fromUserId; + /** + * 回复内容 + */ + private String commentContent; + /** + * 创建日期 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "创建日期") + private Date createTime; + + /** + * 文件信息 + */ + private List fileList; + + /** + * 发送给用户id(允许为空) + */ + @Dict(dictTable = "sys_user", dicCode = "id", dicText = "realname") + private String toUserId; + + /** + * 评论id(允许为空,不为空时,则为回复) + */ + private String commentId; + + /** + * 发消息人的realname + */ + private String fromUserId_dictText; + + /** + * 被回复消息人的realname + */ + private String toUserId_dictText; + + /** + * 发消息人的头像 + */ + private String fromUserAvatar; + + /** + * 被回复消息人的头像 + */ + private String toUserAvatar; + + public SysCommentVO() { + + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartExportVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartExportVo.java new file mode 100644 index 0000000..6727065 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartExportVo.java @@ -0,0 +1,49 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; + +@Data +public class SysDepartExportVo { + /**部门路径*/ + @Excel(name="部门路径",width=50) + private String departNameUrl; + /**机构/部门名称*/ + @Excel(name="部门名称",width=50) + private String departName; + /**id*/ + private String id; + /**父级id*/ + private String parentId; + /**英文名*/ + @Excel(name="英文名",width=15) + private String departNameEn; + /**排序*/ + @Excel(name="排序",width=15) + private Integer departOrder; + /**描述*/ + @Excel(name="描述",width=15) + private String description; + /**机构类别 1=公司,2=组织机构,3=岗位*/ + @Excel(name="机构类别",width=15,dicCode="org_category") + private String orgCategory; + /** 职级id */ + @Excel(name="职级",width=15,dictTable = "sys_position", dicCode = "id", dicText = "name") + private String positionId; + /**机构编码*/ + @Excel(name="机构编码",width=15) + private String orgCode; + /**手机号*/ + @Excel(name="手机号",width=15) + private String mobile; + /**传真*/ + @Excel(name="传真",width=15) + private String fax; + /**地址*/ + @Excel(name="地址",width=15) + private String address; + /**备注*/ + @Excel(name="备注",width=15) + private String memo; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartPositionVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartPositionVo.java new file mode 100644 index 0000000..78236cd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartPositionVo.java @@ -0,0 +1,53 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +/** +* @Description: 部门职务 +* +* @author: wangshuai +* @date: 2025/8/18 10:11 +*/ +@Data +public class SysDepartPositionVo { + + /** + * 部门id + */ + private String id; + + /** + * 是否为叶子节点(数据返回) + */ + private Integer izLeaf; + + /** + * 部门名称 + */ + private String departName; + + /** + * 职务名称 + */ + private String positionName; + + /** + * 父级id + */ + private String parentId; + + /** + * 部门编码 + */ + private String orgCode; + + /** + * 机构类型 + */ + private String orgCategory; + + /** + * 上级岗位id + */ + private String depPostParentId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartUsersVO.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartUsersVO.java new file mode 100644 index 0000000..1919d1c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDepartUsersVO.java @@ -0,0 +1,30 @@ +package com.ghb.base.modules.system.vo; + +import java.io.Serializable; +import java.util.List; + +import lombok.Data; + +/** + * @Description: 系统部门VO + * @author: Ghb-boot + */ +@Data +public class SysDepartUsersVO implements Serializable{ + private static final long serialVersionUID = 1L; + + /**部门id*/ + private String depId; + /**对应的用户id集合*/ + private List userIdList; + public SysDepartUsersVO(String depId, List userIdList) { + super(); + this.depId = depId; + this.userIdList = userIdList; + } + + public SysDepartUsersVO(){ + + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDictBatchVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDictBatchVo.java new file mode 100644 index 0000000..1158cb8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDictBatchVo.java @@ -0,0 +1,20 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; +import com.ghb.base.modules.system.entity.SysDictItem; + +import java.util.List; + +/** + * @Description: 批量字典VO + * @author: zzl + */ +@Data +public class SysDictBatchVo { + + /** + * 字典列表 + */ + private List dictList; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDictPage.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDictPage.java new file mode 100644 index 0000000..ca5295c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysDictPage.java @@ -0,0 +1,45 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; +import com.ghb.base.modules.system.entity.SysDictItem; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecgframework.poi.excel.annotation.ExcelCollection; + +import java.util.List; + +/** + * @Description: 系统字典分页 + * @author: Ghb-boot + */ +@Data +public class SysDictPage { + + /** + * 主键 + */ + private String id; + /** + * 字典名称 + */ + @Excel(name = "字典名称", width = 20) + private String dictName; + + /** + * 字典编码 + */ + @Excel(name = "字典编码", width = 30) + private String dictCode; + /** + * 删除状态 + */ + private Integer delFlag; + /** + * 描述 + */ + @Excel(name = "描述", width = 30) + private String description; + + @ExcelCollection(name = "字典列表") + private List sysDictItemList; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysPositionSelectTreeVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysPositionSelectTreeVo.java new file mode 100644 index 0000000..92fe0ce --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysPositionSelectTreeVo.java @@ -0,0 +1,83 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; +import com.ghb.base.common.constant.enums.DepartCategoryEnum; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.system.entity.SysDepart; + +import java.util.ArrayList; +import java.util.List; + +/** +* @Description: 岗位下拉选择树 +* +* @author: wangshuai +* @date: 2025/8/18 9:40 +*/ +@Data +public class SysPositionSelectTreeVo { + /** 对应SysDepart中的id字段,前端数据树中的value*/ + private String value; + + /** 对应depart_name字段,前端数据树中的title*/ + private String title; + private boolean isLeaf; + /** 是否显示复选框 */ + private boolean checkable; + /** 是否禁用 */ + private boolean disabled; + // 以下所有字段均与SysDepart相同 + private String id; + /**父级id*/ + private String parentId; + /**部门类别*/ + private String orgCategory; + /**部门编码*/ + private String orgCode; + + private List children = new ArrayList<>(); + + /** + * 将SysDepart对象转换成SysDepartTreeModel对象 + * @param sysDepart + */ + public SysPositionSelectTreeVo(SysDepart sysDepart) { + this.value = sysDepart.getId(); + this.title = sysDepart.getDepartName(); + this.id = sysDepart.getId(); + this.parentId = sysDepart.getParentId(); + this.orgCategory = sysDepart.getOrgCategory(); + this.orgCode = sysDepart.getOrgCode(); + if(0 == sysDepart.getIzLeaf()){ + this.isLeaf = false; + }else{ + this.isLeaf = true; + } + if(DepartCategoryEnum.DEPART_CATEGORY_POST.getValue().equals(sysDepart.getOrgCategory())){ + this.checkable = true; + this.disabled = false; + }else{ + this.checkable = false; + this.disabled = true; + } + } + + public SysPositionSelectTreeVo(SysDepartPositionVo position) { + this.value = position.getId(); + if(oConvertUtils.isNotEmpty(position.getDepartName())){ + this.title = position.getPositionName() + "("+position.getDepartName()+")"; + }else{ + this.title = position.getPositionName(); + } + this.id = position.getId(); + this.parentId = position.getDepPostParentId(); + this.orgCategory = "3"; + if(0 == position.getIzLeaf()){ + this.isLeaf = false; + }else{ + this.isLeaf = true; + } + this.checkable = true; + this.disabled = false; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysPositionVO.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysPositionVO.java new file mode 100644 index 0000000..0d4262e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysPositionVO.java @@ -0,0 +1,23 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import com.ghb.base.modules.system.entity.SysPosition; + +/** + * 职务VO,扩展了userId字段,用于批量查询职位时携带用户ID(供全量同步批量预加载场景) + * + * @author sjlei + * @version V1.0 + * @date 2026-04-17 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SysPositionVO extends SysPosition { + + /** + * 批量查询时携带的用户ID(非数据库字段,仅用于查询结果分组) + */ + private String userId; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserDepVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserDepVo.java new file mode 100644 index 0000000..7f3babc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserDepVo.java @@ -0,0 +1,39 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +/** + * @Author qinfeng + * @Date 2020/1/2 21:58 + * @Description: + * @Version 1.0 + */ +@Data +public class SysUserDepVo { + private String userId; + private String departName; + /** + * 部门id + */ + private String deptId; + + /** + * 部门的父级id + */ + private String parentId; + + /** + * 部门类型 + */ + private String orgCategory; + + /** + * 职级 + */ + private String positionId; + + /** + * 部门编码 + */ + private String orgCode; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserExportVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserExportVo.java new file mode 100644 index 0000000..5b42d38 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserExportVo.java @@ -0,0 +1,144 @@ +package com.ghb.base.modules.system.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @Description: 低代码用户导出 + * @author: wangshuai + * @date: 2025/3/28 12:01 + */ +@Data +public class SysUserExportVo { + + /** + * 登录账号 + */ + @Excel(name = "登录账号", width = 15) + private String username; + + /** + * 真实姓名 + */ + @Excel(name = "真实姓名", width = 15) + private String realname; + + /** + * 头像 + */ + @Excel(name = "头像", width = 15, type = 2) + private String avatar; + + /** + * 生日 + */ + @Excel(name = "生日", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date birthday; + + /** + * 性别(1:男 2:女) + */ + @Excel(name = "性别", width = 15, dicCode = "sex") + private Integer sex; + + /** + * 电子邮件 + */ + @Excel(name = "电子邮件", width = 15) + private String email; + + /** + * 电话 + */ + @Excel(name = "电话", width = 15) + private String phone; + + /** + * 状态(1:正常 2:冻结 ) + */ + @Excel(name = "状态", width = 15, dicCode = "user_status") + private Integer status; + + /** + * 删除状态(0,正常,1已删除) + */ + @Excel(name = "删除状态", width = 15, dicCode = "del_flag") + private Integer delFlag; + + /** + * 工号,唯一键 + */ + @Excel(name = "工号", width = 15) + private String workNo; + + /** + * 主岗位 + */ + @Excel(name="主岗位",width = 15,dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + @Dict(dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + private String mainDepPostId; + + /** + * 职级 + */ + @Excel(name="职级", width = 15) + private String postName; + + /** + * 兼职岗位 + */ + @Excel(name="兼职岗位",width = 15,dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + @Dict(dictTable ="sys_depart",dicText = "depart_name",dicCode = "id") + private String otherDepPostId; + + /** + * 座机号 + */ + @Excel(name = "座机号", width = 15) + private String telephone; + + + /** + * 身份(0 普通成员 1 上级) + */ + @Excel(name = "(1普通成员 2上级)", width = 15) + private Integer userIdentity; + + /** + * 角色名称 + */ + @Excel(name = "角色", width = 15) + private String roleNames; + + /** + * 部门名称 + */ + @Excel(name = "所属部门", width = 15) + private String departNames; + + /** + * 机构类型 + * 公司(1)、部门(2)、岗位(3)、子公司(4) + */ + @Excel(name = "部门类型(1-公司,2-部门,3-岗位,4-子公司)",width = 15) + private String orgCategorys; + + /** + * 负责部门 + */ + @Excel(name = "负责部门", width = 15) + private String departIds; + + /** + * 职务 + */ + @Excel(name="职务", dicCode = "user_position") + private String positionType; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserGroupVO.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserGroupVO.java new file mode 100644 index 0000000..f017450 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserGroupVO.java @@ -0,0 +1,31 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @Description: 用户组vo + * @author: Ghb-boot + */ +@Data +public class SysUserGroupVO implements Serializable{ + private static final long serialVersionUID = 1L; + + /**用户组id*/ + private String groupId; + /**对应的用户id集合*/ + private List userIdList; + + public SysUserGroupVO() { + super(); + } + + public SysUserGroupVO(String groupId, List userIdList) { + super(); + this.groupId = groupId; + this.userIdList = userIdList; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserImportVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserImportVo.java new file mode 100644 index 0000000..14ca297 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserImportVo.java @@ -0,0 +1,135 @@ +package com.ghb.base.modules.system.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @Description: 低代码用户导入 + * @author: wangshuai + * @date: 2025/8/27 11:58 + */ +@Data +public class SysUserImportVo { + + /** + * 登录账号 + */ + @Excel(name = "登录账号", width = 15) + private String username; + + /** + * 真实姓名 + */ + @Excel(name = "真实姓名", width = 15) + private String realname; + + /** + * 头像 + */ + @Excel(name = "头像", width = 15, type = 2) + private String avatar; + + /** + * 生日 + */ + @Excel(name = "生日", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date birthday; + + /** + * 性别(1:男 2:女) + */ + @Excel(name = "性别", width = 15, dicCode = "sex") + private Integer sex; + + /** + * 电子邮件 + */ + @Excel(name = "电子邮件", width = 15) + private String email; + + /** + * 电话 + */ + @Excel(name = "电话", width = 15) + private String phone; + + /** + * 状态(1:正常 2:冻结 ) + */ + @Excel(name = "状态", width = 15, dicCode = "user_status") + private Integer status; + + /** + * 删除状态(0,正常,1已删除) + */ + @Excel(name = "删除状态", width = 15, dicCode = "del_flag") + private Integer delFlag; + + /** + * 工号,唯一键 + */ + @Excel(name = "工号", width = 15) + private String workNo; + + /** + * 主岗位 + */ + @Excel(name="主岗位",width = 15) + private String mainDepPostId; + + /** + * 兼职岗位 + */ + @Excel(name="兼职岗位",width = 15) + private String otherDepPostId; + + /** + * 职级 + */ + @Excel(name="职级", width = 15) + private String postName; + + /** + * 身份(0 普通成员 1 上级) + */ + @Excel(name = "(1普通成员 2上级)", width = 15) + private Integer userIdentity; + + /** + * 角色名称 + */ + @Excel(name = "角色", width = 15) + private String roleNames; + + /** + * 部门名称 + */ + @Excel(name = "所属部门", width = 15) + private String departNames; + + /** + * 机构类型 + * 公司(1)、部门(2)、岗位(3)、子公司(4) + */ + @Excel(name = "部门类型(1-公司,2-部门,3-岗位,4-子公司)",width = 15) + private String orgCategorys; + + /** + * 负责部门 + */ + @Excel(name = "负责部门", width = 15) + private String departIds; + + /** + * 职务 + */ + @Excel(name="职务", dicCode = "user_position") + private String positionType; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserOnlineVO.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserOnlineVO.java new file mode 100644 index 0000000..3a04014 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserOnlineVO.java @@ -0,0 +1,62 @@ +package com.ghb.base.modules.system.vo; + +import java.util.Date; + +import com.ghb.base.common.aspect.annotation.Dict; +import org.springframework.format.annotation.DateTimeFormat; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Data; + +/** + * + * @Author: chenli + * @Date: 2020-06-07 + * @Version: V1.0 + */ +@Data +public class SysUserOnlineVO { + /** + * 会话id + */ + private String id; + + /** + * 会话编号 + */ + private String token; + + /** + * 用户名 + */ + private String username; + + /** + * 用户名 + */ + private String realname; + + /** + * 头像 + */ + private String avatar; + + /** + * 生日 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date birthday; + + /** + * 性别(1:男 2:女) + */ + @Dict(dicCode = "sex") + private Integer sex; + + /** + * 手机号 + */ + private String phone; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserPositionVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserPositionVo.java new file mode 100644 index 0000000..aa68d17 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserPositionVo.java @@ -0,0 +1,22 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +/** +* @Description: 用户职位实体类 +* +* @author: wangshuai +* @date: 2023/6/14 16:41 +*/ +@Data +public class SysUserPositionVo { + + /**职位id*/ + private String id; + + /**职务名称*/ + private String name; + + /**用户id*/ + private String userId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserRoleCountVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserRoleCountVo.java new file mode 100644 index 0000000..c1d5bab --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserRoleCountVo.java @@ -0,0 +1,32 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +/** + * @Description: + * @author: wangshuai + * @date: 2022年12月07日 16:41 + */ +@Data +public class SysUserRoleCountVo { + /** + * 角色id + */ + private String id; + /** + * 角色名称 + */ + private String roleName; + /** + * 角色描述 + */ + private String description; + /** + * 角色编码 + */ + private String roleCode; + /** + * 角色下的用户数量 + */ + private Long count; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserRoleVO.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserRoleVO.java new file mode 100644 index 0000000..291093d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserRoleVO.java @@ -0,0 +1,31 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @Description: 用户角色vo + * @author: Ghb-boot + */ +@Data +public class SysUserRoleVO implements Serializable{ + private static final long serialVersionUID = 1L; + + /**部门id*/ + private String roleId; + /**对应的用户id集合*/ + private List userIdList; + + public SysUserRoleVO() { + super(); + } + + public SysUserRoleVO(String roleId, List userIdList) { + super(); + this.roleId = roleId; + this.userIdList = userIdList; + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserTenantVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserTenantVo.java new file mode 100644 index 0000000..5b950e3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/SysUserTenantVo.java @@ -0,0 +1,126 @@ +package com.ghb.base.modules.system.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import com.ghb.base.common.aspect.annotation.Dict; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @Description: 用户租户类(用户数据租户数据) + * @author: wangshuai + * @date: 2023年01月08日 17:27 + */ +@Data +public class SysUserTenantVo { + + /** + * 用户id + */ + private String id; + + /** + * 用户账号 + */ + private String username; + + /** + * 用户昵称 + */ + private String realname; + + /** + * 工号 + */ + private String workNo; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 头像 + */ + private String avatar; + + /** + * 创建日期 + */ + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 职位 + */ + @Dict(dictTable ="sys_position",dicText = "name",dicCode = "id") + private String post; + + /** + * 审核状态 + */ + private String status; + + /** + * 部门名称 + */ + private String orgCodeTxt; + + /** + * 部门code + */ + private String orgCode; + + /** + * 租户id + */ + private String relTenantIds; + + /** + * 租户创建人 + */ + private String createBy; + + /** + * 用户租户状态 + */ + private String userTenantStatus; + + /** + * 用户租户id + */ + private String tenantUserId; + + /** + * 租户名称 + */ + private String name; + + /** + * 所属行业 + */ + @Dict(dicCode = "trade") + private String trade; + + /** + * 门牌号 + */ + private String houseNumber; + + /** + * 是否为会员 + */ + private String memberType; + + /** + * 是否为租户管理员 + */ + private Boolean tenantAdmin = false; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/UserAvatar.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/UserAvatar.java new file mode 100644 index 0000000..a630152 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/UserAvatar.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.system.vo; + +import lombok.Data; +import com.ghb.base.modules.system.entity.SysUser; + +/** + * 用户名和头像信息 + * @Author taoYan + * @Date 2022/8/8 17:06 + **/ +@Data +public class UserAvatar { + + private String id; + + private String realname; + + private String avatar; + + public UserAvatar(){ + + } + public UserAvatar(SysUser sysUser){ + this.id = sysUser.getId(); + this.realname = sysUser.getRealname(); + this.avatar = sysUser.getAvatar(); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/AppExportUserVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/AppExportUserVo.java new file mode 100644 index 0000000..43688ad --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/AppExportUserVo.java @@ -0,0 +1,48 @@ +package com.ghb.base.modules.system.vo.lowapp; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; + +import java.util.Date; + +/** +* @Description: 应用用户与部门 用户导出/导入实体类 +* +* @author: wangshuai +* @date: 2023/6/14 16:42 +*/ +@Data +public class AppExportUserVo { + + /**用户编号*/ + @Excel(name="用户编号",width=30) + private String id; + + /**姓名*/ + @Excel(name="姓名",width=30) + private String realname; + + /**职位*/ + @Excel(name = "职位",width = 30) + private String position; + + /**部门*/ + @Excel(name = "部门",width = 30) + private String depart; + + /**工号*/ + @Excel(name = "工号",width = 30) + private String workNo; + + /**手机号*/ + @Excel(name = "手机号",width = 30) + private String phone; + + /**邮箱*/ + @Excel(name = "邮箱",width = 30) + private String email; + + /**加入时间*/ + @Excel(name = "加入时间",width = 30, format = "yyyy-MM-dd") + private Date createTime; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/DepartAndUserInfo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/DepartAndUserInfo.java new file mode 100644 index 0000000..6391145 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/DepartAndUserInfo.java @@ -0,0 +1,23 @@ +package com.ghb.base.modules.system.vo.lowapp; + +import lombok.Data; +import com.ghb.base.modules.system.vo.UserAvatar; + +import java.io.Serializable; +import java.util.List; + +/** + * 用户或者部门的信息 + * 用于 成员与部门 的搜索 + * @Author taoYan + * @Date 2022/12/30 10:47 + **/ +@Data +public class DepartAndUserInfo implements Serializable { + private static final long serialVersionUID = 1L; + + List userList; + + List departList; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/DepartInfo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/DepartInfo.java new file mode 100644 index 0000000..d68de69 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/DepartInfo.java @@ -0,0 +1,26 @@ +package com.ghb.base.modules.system.vo.lowapp; + +import lombok.Data; + +import java.util.List; + +/** + * @Author taoYan + * @Date 2022/12/30 10:52 + **/ +@Data +public class DepartInfo { + + private String id; + + /** + * 上级名称-下级名称 + */ + private List orgName; + + /** + * 上级ID-下级ID + */ + private List orgId; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/ExportDepartVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/ExportDepartVo.java new file mode 100644 index 0000000..5e29dac --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/ExportDepartVo.java @@ -0,0 +1,18 @@ +package com.ghb.base.modules.system.vo.lowapp; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; + +@Data +public class ExportDepartVo { + /**部门路径*/ + @Excel(name="部门路径",width=50) + private String departNameUrl; + /**机构/部门名称*/ + @Excel(name="部门名称",width=50) + private String departName; + /**id*/ + private String id; + /**父级id*/ + private String parentId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/SysDictVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/SysDictVo.java new file mode 100644 index 0000000..ecbdab4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/SysDictVo.java @@ -0,0 +1,43 @@ +package com.ghb.base.modules.system.vo.lowapp; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; +import com.ghb.base.modules.system.entity.SysDict; +import com.ghb.base.modules.system.entity.SysDictItem; + +import java.util.List; + +@Data +public class SysDictVo { + /** + * 字典id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 字典名称 + */ + private String dictName; + + /** + * 字典编码 + */ + private String dictCode; + + /** + * 应用id + */ + private String lowAppId; + + /** + * 租户ID + */ + private Integer tenantId; + + /** + * 字典子项 + */ + private List dictItemsList; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/UpdateDepartInfo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/UpdateDepartInfo.java new file mode 100644 index 0000000..0eeff5c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/lowapp/UpdateDepartInfo.java @@ -0,0 +1,38 @@ +package com.ghb.base.modules.system.vo.lowapp; + +import lombok.Data; +import com.ghb.base.modules.system.entity.SysDepart; + +import java.util.List; + +/** + * @Author taoYan + * @Date 2022/12/30 16:25 + **/ +@Data +public class UpdateDepartInfo { + + private String departId; + + private String departName; + + private String parentId; + + private Boolean hasSub; + + public UpdateDepartInfo(){ + + } + + public UpdateDepartInfo(SysDepart depart){ + this.departId = depart.getId(); + this.departName = depart.getDepartName(); + this.parentId = depart.getParentId(); + this.hasSub = false; + } + + /** + * 部门负责人ID + */ + private List chargePersonList; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantDepartAuthInfo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantDepartAuthInfo.java new file mode 100644 index 0000000..42f41fd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantDepartAuthInfo.java @@ -0,0 +1,36 @@ +package com.ghb.base.modules.system.vo.tenant; + +import lombok.Data; +import com.ghb.base.modules.system.entity.SysTenant; + +import java.util.List; + +/** + * 进入租户组织页面 查询租户信息及操作权限 + * @Author taoYan + * @Date 2023/2/16 16:18 + **/ +@Data +public class TenantDepartAuthInfo { + + /** + * 当前用户是不是 超级管理员 + */ + private boolean superAdmin; + + /** + * 租户信息 + */ + private SysTenant sysTenant; + + /** + * 统计租户产品包人员数量 + */ + private List packCountList; + + /** + * 租户产品包 编码(这个编码只有3个admin产品包有,便于区分) + */ + private List packCodes; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackAuth.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackAuth.java new file mode 100644 index 0000000..70a38e0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackAuth.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.system.vo.tenant; + +import lombok.Data; + +/** + * 租户产品包 关联权限详情 + * @Author taoYan + * @Date 2023/2/16 21:02 + **/ +@Data +public class TenantPackAuth { + + /** + * 一级菜单 + */ + private String category; + + /** + * 权限菜单名称 + */ + private String authName; + + + /** + * 权限菜单描述 + */ + private String authNote; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackModel.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackModel.java new file mode 100644 index 0000000..05c0105 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackModel.java @@ -0,0 +1,56 @@ +package com.ghb.base.modules.system.vo.tenant; + +import lombok.Data; + +import java.util.List; + +/** + * 租户产品包信息 + * 包括+ 用户信息 + 权限信息 + * @Author taoYan + * @Date 2023/2/16 21:01 + **/ +@Data +public class TenantPackModel { + + /** + * 租户Id + */ + private Integer tenantId; + /** + * 产品包编码 + */ + private String packCode; + + /** + * 产品包ID + */ + private String packId; + + /** + * 产品包名称 + */ + private String packName; + + /** + * 产品包 权限信息 + */ + private List authList; + + /** + * 产品包 用户列表 + */ + private List userList; + + /** + * 状态 正常状态1 申请状态0 + */ + private Integer packUserStatus; + + public Integer getPackUserStatus(){ + if(packUserStatus==null){ + return 1; + } + return packUserStatus; + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackUser.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackUser.java new file mode 100644 index 0000000..9aa1feb --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackUser.java @@ -0,0 +1,62 @@ +package com.ghb.base.modules.system.vo.tenant; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 用户产品包 关联的用户信息 + * @Author taoYan + * @Date 2023/2/16 21:02 + **/ +@Data +public class TenantPackUser { + /** + * 用户ID + */ + private String id; + + private String username; + + private String realname; + + private String avatar; + + private String phone; + + /** + * 多个 部门名称集合 + */ + private Set departNames; + + /** + * 多个 职位名称集合 + */ + private Set positionNames; + + /** + * 租户产品包名称 + */ + private String packName; + + /** + * 租户产品包ID + */ + private String packId; + + public void addDepart(String name){ + if(departNames==null){ + departNames = new HashSet<>(); + } + departNames.add(name); + } + + + public void addPosition(String name){ + if(positionNames==null){ + positionNames = new HashSet<>(); + } + positionNames.add(name); + } +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackUserCount.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackUserCount.java new file mode 100644 index 0000000..cdb8aed --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/TenantPackUserCount.java @@ -0,0 +1,23 @@ +package com.ghb.base.modules.system.vo.tenant; + +import lombok.Data; + +/** + * 用于统计 租户产品包的人员数量 + * @Author taoYan + * @Date 2023/2/16 15:59 + **/ +@Data +public class TenantPackUserCount { + + /** + * 租户产品包编码 + */ + private String packCode; + + /** + * 用户数量 + */ + private String userCount; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/UserDepart.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/UserDepart.java new file mode 100644 index 0000000..478fc89 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/UserDepart.java @@ -0,0 +1,22 @@ +package com.ghb.base.modules.system.vo.tenant; + +import lombok.Data; + +/** + * 用户与部门信息 + * @Author taoYan + * @Date 2023/2/17 10:10 + **/ +@Data +public class UserDepart { + + /** + * 用户ID + */ + private String userId; + + /** + * 部门名称 + */ + private String departName; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/UserPosition.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/UserPosition.java new file mode 100644 index 0000000..82feefa --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/tenant/UserPosition.java @@ -0,0 +1,22 @@ +package com.ghb.base.modules.system.vo.tenant; + +import lombok.Data; + +/** + * 用户与职位信息 + * @Author taoYan + * @Date 2023/2/17 10:10 + **/ +@Data +public class UserPosition { + + /** + * 用户ID + */ + private String userId; + + /** + * 职位名称 + */ + private String positionName; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JdtDepartmentTreeVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JdtDepartmentTreeVo.java new file mode 100644 index 0000000..8a47b67 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JdtDepartmentTreeVo.java @@ -0,0 +1,88 @@ +package com.ghb.base.modules.system.vo.thirdapp; + +import com.jeecg.dingtalk.api.department.vo.Department; +import org.springframework.beans.BeanUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 钉钉树结构的部门 + * + * @author sunjianlei + */ +public class JdtDepartmentTreeVo extends Department { + + private List children; + + public List getChildren() { + return children; + } + + public JdtDepartmentTreeVo setChildren(List children) { + this.children = children; + return this; + } + + public JdtDepartmentTreeVo(Department department) { + BeanUtils.copyProperties(department, this); + } + + /** + * 是否有子项 + */ + public boolean hasChildren() { + return children != null && children.size() > 0; + } + + @Override + public String toString() { + return "JwDepartmentTree{" + + "children=" + children + + "} " + super.toString(); + } + + /** + * 静态辅助方法,将list转为tree结构 + */ + public static List listToTree(List allDepartment) { + // 先找出所有的父级 + List treeList = getByParentId(1, allDepartment); + Optional departmentOptional = allDepartment.stream().filter(item -> item.getParent_id() == null).findAny(); + Department department = new Department(); + //判断是否找到数据 + if(departmentOptional.isPresent()){ + department = departmentOptional.get(); + } + getChildrenRecursion(treeList, allDepartment); + // 代码逻辑说明: 【issues/6017】钉钉同步部门时没有最顶层的部门名,同步用户时,用户没有部门信息--- + JdtDepartmentTreeVo treeVo = new JdtDepartmentTreeVo(department); + treeVo.setChildren(treeList); + List list = new ArrayList<>(); + list.add(treeVo); + return list; + } + + private static List getByParentId(Integer parentId, List allDepartment) { + List list = new ArrayList<>(); + for (Department department : allDepartment) { + if (parentId.equals(department.getParent_id())) { + list.add(new JdtDepartmentTreeVo(department)); + } + } + return list; + } + + private static void getChildrenRecursion(List treeList, List allDepartment) { + for (JdtDepartmentTreeVo departmentTree : treeList) { + // 递归寻找子级 + List children = getByParentId(departmentTree.getDept_id(), allDepartment); + if (children.size() > 0) { + departmentTree.setChildren(children); + getChildrenRecursion(children, allDepartment); + } + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwDepartmentTreeVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwDepartmentTreeVo.java new file mode 100644 index 0000000..ca5ee4b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwDepartmentTreeVo.java @@ -0,0 +1,88 @@ +package com.ghb.base.modules.system.vo.thirdapp; + +import com.jeecg.qywx.api.department.vo.Department; +import org.springframework.beans.BeanUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 企业微信树结构的部门 + * + * @author sunjianlei + */ +public class JwDepartmentTreeVo extends Department { + + private List children; + + public List getChildren() { + return children; + } + + public JwDepartmentTreeVo setChildren(List children) { + this.children = children; + return this; + } + + public JwDepartmentTreeVo(Department department) { + BeanUtils.copyProperties(department, this); + } + + /** + * 是否有子项 + */ + public boolean hasChildren() { + return children != null && children.size() > 0; + } + + @Override + public String toString() { + return "JwDepartmentTree{" + + "children=" + children + + "} " + super.toString(); + } + + /** + * 静态辅助方法,将list转为tree结构 + */ + public static List listToTree(List allDepartment) { + // 先找出所有的父级 + List treeList = getByParentId("1", allDepartment); + Optional departmentOptional = allDepartment.stream().filter(item -> "0".equals(item.getParentid())).findAny(); + Department department = new Department(); + //判断是否找到数据 + if(departmentOptional.isPresent()){ + department = departmentOptional.get(); + } + getChildrenRecursion(treeList, allDepartment); + // 代码逻辑说明: 【issues/6017】企业微信同步部门时没有最顶层的部门名,同步用户时,用户没有部门信息--- + JwDepartmentTreeVo treeVo = new JwDepartmentTreeVo(department); + treeVo.setChildren(treeList); + List list = new ArrayList<>(); + list.add(treeVo); + return list; + } + + private static List getByParentId(String parentId, List allDepartment) { + List list = new ArrayList<>(); + for (Department department : allDepartment) { + if (parentId.equals(department.getParentid())) { + list.add(new JwDepartmentTreeVo(department)); + } + } + return list; + } + + private static void getChildrenRecursion(List treeList, List allDepartment) { + for (JwDepartmentTreeVo departmentTree : treeList) { + // 递归寻找子级 + List children = getByParentId(departmentTree.getId(), allDepartment); + if (children.size() > 0) { + departmentTree.setChildren(children); + getChildrenRecursion(children, allDepartment); + } + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwSysUserDepartVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwSysUserDepartVo.java new file mode 100644 index 0000000..46e83d2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwSysUserDepartVo.java @@ -0,0 +1,23 @@ +package com.ghb.base.modules.system.vo.thirdapp; + +import lombok.Data; + +import java.util.List; + +/** + * 企业微信的实现类 + */ +@Data +public class JwSysUserDepartVo { + + /** + * 企业微信和用户的映射类 + */ + private List jwUserDepartVos; + + /** + * 用户列表 + */ + private List userList; + +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwUserDepartVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwUserDepartVo.java new file mode 100644 index 0000000..456d2c4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/JwUserDepartVo.java @@ -0,0 +1,48 @@ +package com.ghb.base.modules.system.vo.thirdapp; + +import lombok.Data; + +/** +* @Description: 企业微信用户同步工具类 +* +* @author: wangshuai +* @date: 2023/11/28 18:17 +*/ +@Data +public class JwUserDepartVo { + + /** + * 用户id + */ + private String userId; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 真实姓名 + */ + private String realName; + + /** + * 企业微信的名字 + */ + private String wechatRealName; + + /** + * 企业微信对应的部门 + */ + private String wechatDepartId; + + /** + * 企业微信对应的用户id + */ + private String wechatUserId; + + /** + * 第三方id + */ + private String thirdId; +} diff --git a/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/SyncInfoVo.java b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/SyncInfoVo.java new file mode 100644 index 0000000..7980819 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/java/com/ghb/base/modules/system/vo/thirdapp/SyncInfoVo.java @@ -0,0 +1,44 @@ +package com.ghb.base.modules.system.vo.thirdapp; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 同步结果信息,包含成功的信息和失败的信息 + * + * @author sunjianlei + */ +@Data +public class SyncInfoVo { + + /** + * 成功的信息 + */ + private List successInfo; + /** + * 失败的信息 + */ + private List failInfo; + + public SyncInfoVo() { + this.successInfo = new ArrayList<>(); + this.failInfo = new ArrayList<>(); + } + + public SyncInfoVo(List successInfo, List failInfo) { + this.successInfo = successInfo; + this.failInfo = failInfo; + } + + public SyncInfoVo addSuccessInfo(String info) { + this.successInfo.add(info); + return this; + } + + public SyncInfoVo addFailInfo(String info) { + this.failInfo.add(info); + return this; + } +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/blob.ftl b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/blob.ftl new file mode 100644 index 0000000..eb85ea9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/blob.ftl @@ -0,0 +1,67 @@ +<#if po.fieldDbType=='Blob'> + private transient java.lang.String ${po.fieldName}String; + + private byte[] ${po.fieldName}; + + public byte[] get${po.fieldName?cap_first}(){ + if(${po.fieldName}String==null){ + return null; + } + try { + return ${po.fieldName}String.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return null; + } + + public String get${po.fieldName?cap_first}String(){ + if(${po.fieldName}==null || ${po.fieldName}.length==0){ + return ""; + } + try { + return new String(${po.fieldName},"UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return ""; + } +<#elseif po.classType=='switch'> + <#assign switch_extend_arr=['Y','N']> + <#if po.dictField?default("")?contains("[")> + <#assign switch_extend_arr=po.dictField?eval> + + <#list switch_extend_arr as a> + <#if a_index == 0> + <#assign switch_extend_arr1=a> + <#else> + <#assign switch_extend_arr2=a> + + + @Excel(name = "${po.filedComment}", width = 15,replace = {"是_${switch_extend_arr1}","否_${switch_extend_arr2}"} ) + @Schema(description = "${po.filedComment}") + private ${po.fieldType} ${po.fieldName}; +<#elseif po.classType=='pca'> + @Excel(name = "${po.filedComment}", width = 15,exportConvert=true,importConvert = true ) + @Schema(description = "${po.filedComment}") + private ${po.fieldType} ${po.fieldName}; + + public String convertis${po.fieldName?cap_first}() { + return SpringContextUtils.getBean(ProvinceCityArea.class).getText(${po.fieldName}); + } + + public void convertset${po.fieldName?cap_first}(String text) { + this.${po.fieldName} = SpringContextUtils.getBean(ProvinceCityArea.class).getCode(text); + } +<#elseif po.classType=='cat_tree'> + <#assign list_field_dictCode=', dictTable = "sys_category", dicText = "name", dicCode = "id"'> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + @Schema(description = "${po.filedComment}") + private ${po.fieldType} ${po.fieldName}; +<#else> + @Schema(description = "${po.filedComment}") + <#if po.fieldDbName == 'del_flag'> + @TableLogic + + private ${po.fieldType} ${po.fieldName}; + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeComponents.ftl b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeComponents.ftl new file mode 100644 index 0000000..30a4861 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeComponents.ftl @@ -0,0 +1,53 @@ +<#if need_select_tag> + JDictSelectTag, + +<#if need_switch> + JSwitch, + +<#if need_multi> + JSelectMultiple, + +<#if need_search> + JSearchSelect, + +<#if need_popup> + JPopup, + +<#if need_popup_dict> + JPopupDict, + +<#if need_category> + JCategorySelect, + +<#if need_dept> + JSelectDept, + +<#if need_dept_user> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + JSelectUser, +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + +<#if need_select_tree> + JTreeSelect, + +<#if need_time> + TimePicker, + +<#if need_pca> + JAreaLinkage, + +<#if need_upload> + JUpload, + +<#if need_image_upload> + JImageUpload, + +<#if need_markdown> + JMarkdownEditor, + +<#if need_editor> + JEditor, + +<#if need_checkbox> + JCheckbox, + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeForm.ftl b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeForm.ftl new file mode 100644 index 0000000..519ecb5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeForm.ftl @@ -0,0 +1,119 @@ +<#include "/common/utils.ftl"> +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName != 'id' && po.fieldName !='delFlag' && isNotPidField(tableVo, po.fieldDbName)> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + + + <#if po.classType =='date'> + picker="${po.extendParams.picker}" v-model:value="formData.${po.fieldName}" value-format="YYYY-MM-DD" style="width: 100%" <#if po.readonly=='Y'>disabled allow-clear /> + <#elseif po.classType =='datetime'> + disabled allow-clear /> + <#elseif po.classType =='time'> + <#assign need_time = true> + disabled allow-clear /> + <#elseif po.classType =='popup'> + <#assign need_popup = true> + <#assign sourceFields = po.dictField?default("")?trim?split(",")/> + <#assign targetFields = po.dictText?default("")?trim?split(",")/> + disabled<#rt> allow-clear /> + <#elseif po.classType =='popup_dict'> + <#assign need_popup_dict = true> + <#assign sourceFields = po.dictField?default("")?trim?split(",")/> + <#assign targetFields = po.dictText?default("")?trim?split(",")/> + disabled /> + <#elseif po.classType =='sel_depart'> + <#assign need_dept = true> + labelKey="${po.extendParams.text}" <#if po.extendParams?exists && po.extendParams.store?exists>rowKey="${po.extendParams.store}" <#if po.readonly=='Y'>disabled :multiple="${po.extendParams.multi?default('true')}" checkStrictly <#if po.readonly=='Y'>disabled allow-clear /> + <#elseif po.classType =='switch'> + <#assign need_switch = true> + :options="${po.dictField}" <#if po.readonly=='Y'>disabled> + <#elseif po.classType =='pca'> + <#assign need_pca = true> + disabled allow-clear /> + <#elseif po.classType =='markdown'> + <#assign need_markdown = true> + disabled> + <#elseif po.classType =='password'> + disabled allow-clear /> + <#elseif po.classType =='sel_user'> + <#assign need_dept_user = true> + <#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + labelKey="${po.extendParams.text}" <#if po.extendParams?exists && po.extendParams.store?exists>rowKey="${po.extendParams.store}" <#if po.readonly=='Y'>disabled allow-clear /> + <#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + <#elseif po.classType =='textarea'> + disabled/> + <#elseif po.classType=='radio'> + <#assign need_select_tag = true> + disabled allow-clear /> + <#elseif po.classType=='list'> + <#assign need_select_tag = true> + disabled allow-clear /> + <#elseif po.classType=='list_multi'> + <#assign need_multi = true> + disabled :triggerChange="false"/> + <#elseif po.classType=='checkbox'> + <#assign need_checkbox = true> + disabled allow-clear /> + <#elseif po.classType=='sel_search'> + <#assign need_search = true> + disabled allow-clear /> + <#elseif po.classType=='cat_tree'> + <#assign need_category = true> + back="${dashedToCamel(po.dictText)}" <#if po.readonly=='Y'>disabled @change="(value) => handleFormChange('${po.fieldName}', value)" allow-clear /> + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + disabled/> + <#elseif po.classType=='file'> + <#assign need_upload = true> + disabled <#if po.uploadnum??>:maxCount=${po.uploadnum}> + <#elseif po.classType=='image'> + <#assign need_image_upload = true> + :fileMax=${po.uploadnum}<#else>:fileMax="0" v-model:value="formData.${po.fieldName}" <#if po.readonly=='Y'>disabled> + <#elseif po.classType=='umeditor'> + <#assign need_editor = true> + disabled :autoFocus="false"/> + <#elseif po.fieldDbType=='Blob'> + disabled allow-clear > + <#elseif po.classType == 'sel_tree'> + <#assign need_select_tree = true> + + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict="${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}" + <#elseif po.dictText?split(',')[1]??> + pidField="${po.dictText?split(',')[1]}" + <#elseif po.dictText?split(',')[3]??> + hasChildField="${po.dictText?split(',')[3]}" + + + pidValue="${po.dictField}" + <#if po.readonly=='Y'>disabled + v-model:value="formData.${po.fieldName}" + @change="(value) => handleFormChange('${po.fieldName}', value)" allow-clear > + + <#else> + disabled allow-clear > + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeImport.ftl b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeImport.ftl new file mode 100644 index 0000000..b770ae9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeImport.ftl @@ -0,0 +1,59 @@ +<#if need_select_tag> + import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue'; + +<#if need_switch> + import JSwitch from '/@/components/Form/src/jeecg/components/JSwitch.vue'; + +<#if need_multi> + import JSelectMultiple from '/@/components/Form/src/jeecg/components/JSelectMultiple.vue'; + +<#if need_search> + import JSearchSelect from '/@/components/Form/src/jeecg/components/JSearchSelect.vue'; + +<#if need_popup> + import JPopup from '/@/components/Form/src/jeecg/components/JPopup.vue'; + +<#if need_popup_dict> + import JPopupDict from '/@/components/Form/src/jeecg/components/JPopupDict.vue'; + +<#if need_category> + import JCategorySelect from '/@/components/Form/src/jeecg/components/JCategorySelect.vue'; + +<#if need_dept> + import JSelectDept from '/@/components/Form/src/jeecg/components/JSelectDept.vue'; + +<#if need_dept_user> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue'; +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + +<#if need_select_tree> + import JTreeSelect from '/@/components/Form/src/jeecg/components/JTreeSelect.vue'; + +<#if need_time> + import { TimePicker } from 'ant-design-vue'; + +<#if need_pca> + import JAreaLinkage from '/@/components/Form/src/jeecg/components/JAreaLinkage.vue'; + +<#if need_upload> + import JUpload from '/@/components/Form/src/jeecg/components/JUpload/JUpload.vue'; + +<#if need_image_upload> + import JImageUpload from '/@/components/Form/src/jeecg/components/JImageUpload.vue'; + +<#if need_markdown> + import JMarkdownEditor from '/@/components/Form/src/jeecg/components/JMarkdownEditor.vue'; + +<#if need_editor> + import JEditor from '/@/components/Form/src/jeecg/components/JEditor.vue'; + +<#if need_checkbox> + import JCheckbox from "/@/components/Form/src/jeecg/components/JCheckbox.vue"; + +<#if need_range_number> + import JRangeNumber from "/@/components/Form/src/jeecg/components/JRangeNumber.vue"; + +<#if is_like> + import JInput from "/@/components/Form/src/jeecg/components/JInput.vue"; + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeSearch.ftl b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeSearch.ftl new file mode 100644 index 0000000..f947b00 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/common/form/native/vue3NativeSearch.ftl @@ -0,0 +1,108 @@ +<#include "/common/utils.ftl"> +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isQuery=='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign query_flag=true> + <#if query_field_no==2> + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi new file mode 100644 index 0000000..61acd7f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi @@ -0,0 +1,72 @@ +import { defHttp } from '/@/utils/http/axios'; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/list', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackagePath}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', +} + +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; + +/** + * 列表接口 + * @param params + */ +export const list = (params) => defHttp.get({ url: Api.list, params }); + +/** + * 删除单个 + * @param params + * @param handleSuccess + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} + +/** + * 批量删除 + * @param params + * @param handleSuccess + */ +export const batchDelete = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} + +/** + * 保存或者更新 + * @param params + * @param isUpdate + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({ url: url, params }, { isTransformResponse: false }); +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi new file mode 100644 index 0000000..03bf054 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi @@ -0,0 +1,90 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align: "center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender: render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]); + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}'); + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : ''); + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei new file mode 100644 index 0000000..0c3f5ee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei @@ -0,0 +1,256 @@ +<#include "/common/utils.ftl"> + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei new file mode 100644 index 0000000..29dc076 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/one/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei @@ -0,0 +1,102 @@ +<#include "/common/utils.ftl"> + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..9192569 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,286 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecg.common.system.vo.LoginUser; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.util.oConvertUtils; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.vo.${entityName}Page; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.service.I${sub.entityName}Service; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.apache.shiro.authz.annotation.RequiresPermissions; +<#assign bpm_flag=false> +<#list originalColumns as po> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + + + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackagePath}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + <#list subTables as sub> + @Autowired + private I${sub.entityName}Service ${sub.entityName?uncap_first}Service; + + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @RequiresPermissions("${entityPackage}:${tableName}:add") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + <#if bpm_flag> + ${entityName?uncap_first}.setBpmStatus("1"); + + ${entityName?uncap_first}Service.saveMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequiresPermissions("${entityPackage}:${tableName}:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName} ${entityName?uncap_first}Entity = ${entityName?uncap_first}Service.getById(${entityName?uncap_first}.getId()); + if(${entityName?uncap_first}Entity==null) { + return Result.error("未找到对应数据"); + } + ${entityName?uncap_first}Service.updateMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @RequiresPermissions("${entityPackage}:${tableName}:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delMain(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @RequiresPermissions("${entityPackage}:${tableName}:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.delBatchMain(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result<${entityName}> queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + if(${entityName?uncap_first}==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(${entityName?uncap_first}); + + } + + <#list subTables as sub> + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${sub.ftlDescription}通过主表ID查询") + @Operation(summary="${sub.ftlDescription}主表ID查询") + @GetMapping(value = "/query${sub.entityName}ByMainId") + public Result> query${sub.entityName}ListByMainId(@RequestParam(name="id",required=true) String id) { + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(id); + return Result.OK(${sub.entityName?uncap_first}List); + } + + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequiresPermissions("${entityPackage}:${tableName}:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + // Step.1 组装查询条件查询数据 + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, request.getParameterMap()); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + //配置选中数据查询条件 + String selections = request.getParameter("selections"); + if(oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + queryWrapper.in("id",selectionList); + } + //Step.2 获取导出数据 + List<${entityName}> ${entityName?uncap_first}List = ${entityName?uncap_first}Service.list(queryWrapper); + + // Step.3 组装pageList + List<${entityName}Page> pageList = new ArrayList<${entityName}Page>(); + for (${entityName} main : ${entityName?uncap_first}List) { + ${entityName}Page vo = new ${entityName}Page(); + BeanUtils.copyProperties(main, vo); + <#list subTables as sub> + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(main.getId()); + vo.set${sub.entityName}List(${sub.entityName?uncap_first}List); + + pageList.add(vo); + } + + // Step.4 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + mv.addObject(NormalExcelConstants.FILE_NAME, "${tableVo.ftlDescription}列表"); + mv.addObject(NormalExcelConstants.CLASS, ${entityName}Page.class); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("${tableVo.ftlDescription}数据", "导出人:"+sysUser.getRealname(), "${tableVo.ftlDescription}", ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("${entityPackage}:${tableName}:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List<${entityName}Page> list = ExcelImportUtil.importExcel(file.getInputStream(), ${entityName}Page.class, params); + for (${entityName}Page page : list) { + ${entityName} po = new ${entityName}(); + BeanUtils.copyProperties(page, po); + ${entityName?uncap_first}Service.saveMain(po, <#list subTables as sub>page.get${sub.entityName}List()<#if sub_has_next>,); + } + return Result.OK("文件导入成功!数据行数:" + list.size()); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.OK("文件导入失败!"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..d5e992b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,77 @@ +<#include "/common/utils.ftl"> +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.aspect.annotation.Dict; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${tableVo.ftlDescription}") +@Data +@TableName("${tableName}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + + <#-- 大字段转换 --> + <#include "/common/blob.ftl"> + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai new file mode 100644 index 0000000..e4b78b9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai @@ -0,0 +1,76 @@ +<#include "/common/utils.ftl"> +<#list subTables as subTab> +#segment#${subTab.entityName}.java +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import java.util.Date; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.UnsupportedEncodingException; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${subTab.ftlDescription}") +@Data +@TableName("${subTab.tableName}") +public class ${subTab.entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#list subTab.originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#elseif !subTab.foreignKeys?seq_contains(po.fieldName?cap_first)> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + + <#-- 大字段转换 --> + <#include "/common/blob.ftl"> + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai new file mode 100644 index 0000000..a33449b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai @@ -0,0 +1,34 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Mapper.java +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${subTab.entityName}Mapper extends BaseMapper<${subTab.entityName}> { + + /** + * 通过主表id删除子表数据 + * + * @param mainId 主表id + * @return boolean + */ + public boolean deleteByMainId(@Param("mainId") String mainId); + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(@Param("mainId") String mainId); +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml new file mode 100644 index 0000000..117c9b6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml @@ -0,0 +1,26 @@ +<#list subTables as subTab> +<#assign originalForeignKeys = subTab.originalForeignKeys> +#segment#${subTab.entityName}Mapper.xml + + + + + + DELETE + FROM ${subTab.tableName} + WHERE + <#list originalForeignKeys as key> + ${key} = ${r'#'}{mainId} <#rt/> + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..d56db20 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,54 @@ +package ${bussiPackage}.${entityPackage}.service; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /** + * 添加一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void saveMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) ; + + /** + * 修改一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,); + + /** + * 删除一对多 + * + * @param id + */ + public void delMain (String id); + + /** + * 批量删除一对多 + * + * @param idList + */ + public void delBatchMain (Collection idList); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai new file mode 100644 index 0000000..3d45c9f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai @@ -0,0 +1,25 @@ +<#list subTables as subTab> +#segment#I${subTab.entityName}Service.java +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${subTab.entityName}Service extends IService<${subTab.entityName}> { + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(String mainId); +} + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..7f99d42 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,105 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.mapper.${sub.entityName}Mapper; + +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.io.Serializable; +import java.util.List; +import java.util.Collection; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Autowired + private ${entityName}Mapper ${entityName?uncap_first}Mapper; + <#list subTables as sub> + @Autowired + private ${sub.entityName}Mapper ${sub.entityName?uncap_first}Mapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMain(${entityName} ${entityName?uncap_first}, <#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.insert(${entityName?uncap_first}); + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.updateById(${entityName?uncap_first}); + + //1.先删除子表数据 + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(${entityName?uncap_first}.getId()); + + + //2.子表数据重新插入 + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delMain(String id) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatchMain(Collection idList) { + for(Serializable id:idList) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id.toString()); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai new file mode 100644 index 0000000..0ce41d3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai @@ -0,0 +1,30 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}ServiceImpl.java +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${subTab.entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${subTab.entityName}Service; +import org.springframework.stereotype.Service; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${subTab.entityName}ServiceImpl extends ServiceImpl<${subTab.entityName}Mapper, ${subTab.entityName}> implements I${subTab.entityName}Service { + + @Autowired + private ${subTab.entityName}Mapper ${subTab.entityName?uncap_first}Mapper; + + @Override + public List<${subTab.entityName}> selectByMainId(String mainId) { + return ${subTab.entityName?uncap_first}Mapper.selectByMainId(mainId); + } +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai new file mode 100644 index 0000000..b34674e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai @@ -0,0 +1,83 @@ +package ${bussiPackage}.${entityPackage}.vo; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecgframework.poi.excel.annotation.ExcelEntity; +import org.jeecgframework.poi.excel.annotation.ExcelCollection; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import java.util.Date; +import org.jeecg.common.aspect.annotation.Dict; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName}Page { + + <#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "realname", dicCode = "username"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "depart_name", dicCode = "id"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + + @Schema(description = "${po.filedComment}") + <#if po.fieldDbType=='Blob'> + private java.lang.String ${po.fieldName}String; + <#else> + private ${po.fieldType} ${po.fieldName}; + + + + <#list subTables as sub> + @ExcelCollection(name="${sub.ftlDescription}") + @Schema(description = "${sub.ftlDescription}") + private List<${sub.entityName}> ${sub.entityName?uncap_first}List; + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..6605f0c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,397 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei new file mode 100644 index 0000000..8951837 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei @@ -0,0 +1,562 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..8021bb8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,65 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei new file mode 100644 index 0000000..e625f88 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei @@ -0,0 +1,197 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..468ef47 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,372 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.common.system.vo.SelectTreeModel; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; + +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecg.common.system.base.controller.JeecgController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.apache.shiro.authz.annotation.RequiresPermissions; + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +<#assign pidFieldName = ""> +<#list originalColumns as po> +<#if po.fieldDbName == tableVo.extendParams.pidField> +<#assign pidFieldName = po.fieldName> + + +<#assign enhanceJavaList=[]> +<#if tableVo.extendParams?? && tableVo.extendParams.enhanceJavaList??> + <#assign enhanceJavaList = tableVo.extendParams.enhanceJavaList?filter(enhance -> enhance??)> + +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackagePath}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller extends JeecgController<${entityName}, I${entityName}Service>{ + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/rootList") + public Result> queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 查询前触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeQuery() + + + + String hasQuery = req.getParameter("hasQuery"); + if(hasQuery != null && "true".equals(hasQuery)){ + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + List<${entityName}> list = ${entityName?uncap_first}Service.queryTreeListNoPage(queryWrapper); + IPage<${entityName}> pageList = new Page<>(1, 10, list.size()); + pageList.setRecords(list); + return Result.OK(pageList); + }else{ + String parentId = ${entityName?uncap_first}.get${pidFieldName?cap_first}(); + if (oConvertUtils.isEmpty(parentId)) { + parentId = "0"; + } + ${entityName?uncap_first}.set${pidFieldName?cap_first}(null); + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + // 使用 eq 防止模糊查询 + queryWrapper.eq("${Format.humpToUnderline(pidFieldName)}", parentId); + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 查询后触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterQuery() + + + + return Result.OK(pageList); + } + } + + /** + * 【vue3专用】加载节点的子数据 + * + * @param pid + * @return + */ + @RequestMapping(value = "/loadTreeChildren", method = RequestMethod.GET) + public Result> loadTreeChildren(@RequestParam(name = "pid") String pid) { + Result> result = new Result<>(); + try { + List ls = ${entityName?uncap_first}Service.queryListByPid(pid); + result.setResult(ls); + result.setSuccess(true); + } catch (Exception e) { + e.printStackTrace(); + result.setMessage(e.getMessage()); + result.setSuccess(false); + } + return result; + } + + /** + * 【vue3专用】加载一级节点/如果是同步 则所有数据 + * + * @param async + * @param pcode + * @return + */ + @RequestMapping(value = "/loadTreeRoot", method = RequestMethod.GET) + public Result> loadTreeRoot(@RequestParam(name = "async") Boolean async, @RequestParam(name = "pcode") String pcode) { + Result> result = new Result<>(); + try { + List ls = ${entityName?uncap_first}Service.queryListByCode(pcode); + if (!async) { + loadAllChildren(ls); + } + result.setResult(ls); + result.setSuccess(true); + } catch (Exception e) { + e.printStackTrace(); + result.setMessage(e.getMessage()); + result.setSuccess(false); + } + return result; + } + + /** + * 【vue3专用】递归求子节点 同步加载用到 + * + * @param ls + */ + private void loadAllChildren(List ls) { + for (SelectTreeModel tsm : ls) { + List temp = ${entityName?uncap_first}Service.queryListByPid(tsm.getKey()); + if (temp != null && temp.size() > 0) { + tsm.setChildren(temp); + loadAllChildren(temp); + } + } + } + + /** + * 获取子数据 + * @param ${entityName?uncap_first} + * @param req + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-获取子数据") + @Operation(summary="${tableVo.ftlDescription}-获取子数据") + @GetMapping(value = "/childList") + public Result> queryPageList(${entityName} ${entityName?uncap_first},HttpServletRequest req) { + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + List<${entityName}> list = ${entityName?uncap_first}Service.list(queryWrapper); + IPage<${entityName}> pageList = new Page<>(1, 10, list.size()); + pageList.setRecords(list); + return Result.OK(pageList); + } + + /** + * 批量查询子节点 + * @param parentIds 父ID(多个采用半角逗号分割) + * @return 返回 IPage + * @param parentIds + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-批量获取子数据") + @Operation(summary="${tableVo.ftlDescription}-批量获取子数据") + @GetMapping("/getChildListBatch") + public Result getChildListBatch(@RequestParam("parentIds") String parentIds) { + try { + QueryWrapper<${entityName}> queryWrapper = new QueryWrapper<>(); + List parentIdList = Arrays.asList(parentIds.split(",")); + queryWrapper.in("${Format.humpToUnderline(pidFieldName)}", parentIdList); + List<${entityName}> list = ${entityName?uncap_first}Service.list(queryWrapper); + IPage<${entityName}> pageList = new Page<>(1, 10, list.size()); + pageList.setRecords(list); + return Result.OK(pageList); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("批量查询子节点失败:" + e.getMessage()); + } + } + + /** + * 添加 + * + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @RequiresPermissions("${entityPackage}:${tableName}:add") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName} ${entityName?uncap_first}) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 新增前的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeAdd() + + + + ${entityName?uncap_first}Service.add${entityName}(${entityName?uncap_first}); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 新增后的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterAdd() + + + + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequiresPermissions("${entityPackage}:${tableName}:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName} ${entityName?uncap_first}) { +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 编辑前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeEdit() + + + + ${entityName?uncap_first}Service.update${entityName}(${entityName?uncap_first}); +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 编辑后,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterEdit() + + + + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @RequiresPermissions("${entityPackage}:${tableName}:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delete${entityName}(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @RequiresPermissions("${entityPackage}:${tableName}:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result<${entityName}> queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + if(${entityName?uncap_first}==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(${entityName?uncap_first}); + } + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequiresPermissions("${entityPackage}:${tableName}:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='export' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导出前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeExport() + + + + return super.exportXls(request, ${entityName?uncap_first}, ${entityName}.class, "${tableVo.ftlDescription}"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("${entityPackage}:${tableName}:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='import' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导入前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeImport() + + + + return super.importExcel(request, response, ${entityName}.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..b634f67 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,91 @@ +<#include "/common/utils.ftl"> +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.aspect.annotation.Dict; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.UnsupportedEncodingException; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${tableName}") +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> +<#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#-- <#if po.classType!='popup'> + <#if po.dictTable?default("")?trim?length gt 1> + @Dict(dicCode="${po.dictField}",dicText="${po.dictText}",dictTable="${po.dictTable}") + <#elseif po.dictField?default("")?trim?length gt 1> + @Dict(dicCode="${po.dictField}") + + --> + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + + <#-- 大字段转换 --> + <#include "/common/blob.ftl"> + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..d894ff1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,35 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.jeecg.common.system.vo.SelectTreeModel; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; + +import java.util.List; +import java.util.Map; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + + /** + * 编辑节点状态 + * @param id + * @param status + */ + void updateTreeNodeStatus(@Param("id") String id,@Param("status") String status); + + /** + * 【vue3专用】根据父级ID查询树节点数据 + * + * @param pid + * @param query + * @return + */ + List queryListByPid(@Param("pid") String pid, @Param("query") Map query); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..170b395 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,45 @@ +<#assign hasChildrenField = ""> +<#assign pidFieldName = ""> +<#assign textFieldName = ""> +<#assign textDbFieldName = ""> +<#assign pidDbFieldName = ""> +<#list originalColumns as po> + <#if po.fieldDbName == tableVo.extendParams.hasChildren> + <#assign hasChildrenField = po.fieldName> + + <#-- begin 【vue3专用】 --> + <#if po.fieldDbName == tableVo.extendParams.pidField> + <#assign pidFieldName = po.fieldName> + <#assign pidDbFieldName = po.fieldDbName> + + <#if po.fieldDbName == tableVo.extendParams.textField> + <#assign textFieldName = po.fieldName> + <#assign textDbFieldName = po.fieldDbName> + + <#-- end 【vue3专用】 --> + + + + + + + update ${tableName} set ${Format.humpToUnderline(hasChildrenField)} = ${r'#'}{status} where id = ${r'#'}{id} + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..701da1a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,74 @@ +package ${bussiPackage}.${entityPackage}.service; + +import org.jeecg.common.system.vo.SelectTreeModel; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import org.jeecg.common.exception.JeecgBootException; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /**根节点父ID的值*/ + public static final String ROOT_PID_VALUE = "0"; + + /**树节点有子节点状态值*/ + public static final String HASCHILD = "1"; + + /**树节点无子节点状态值*/ + public static final String NOCHILD = "0"; + + /** + * 新增节点 + * + * @param ${entityName?uncap_first} + */ + void add${entityName}(${entityName} ${entityName?uncap_first}); + + /** + * 修改节点 + * + * @param ${entityName?uncap_first} + * @throws JeecgBootException + */ + void update${entityName}(${entityName} ${entityName?uncap_first}) throws JeecgBootException; + + /** + * 删除节点 + * + * @param id + * @throws JeecgBootException + */ + void delete${entityName}(String id) throws JeecgBootException; + + /** + * 查询所有数据,无分页 + * + * @param queryWrapper + * @return List<${entityName}> + */ + List<${entityName}> queryTreeListNoPage(QueryWrapper<${entityName}> queryWrapper); + + /** + * 【vue3专用】根据父级编码加载分类字典的数据 + * + * @param parentCode + * @return + */ + List queryListByCode(String parentCode); + + /** + * 【vue3专用】根据pid查询子节点集合 + * + * @param pid + * @return + */ + List queryListByPid(String pid); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..6372a2c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,229 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.common.system.vo.SelectTreeModel; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +<#assign pidFieldName = ""> +<#assign hasChildrenField = ""> +<#list originalColumns as po> +<#if po.fieldDbName == tableVo.extendParams.pidField> +<#assign pidFieldName = po.fieldName> + +<#if po.fieldDbName == tableVo.extendParams.hasChildren> +<#assign hasChildrenField = po.fieldName> + + + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Override + public void add${entityName}(${entityName} ${entityName?uncap_first}) { + //新增时设置hasChild为0 + ${entityName?uncap_first}.set${hasChildrenField?cap_first}(I${entityName}Service.NOCHILD); + if(oConvertUtils.isEmpty(${entityName?uncap_first}.get${pidFieldName?cap_first}())){ + ${entityName?uncap_first}.set${pidFieldName?cap_first}(I${entityName}Service.ROOT_PID_VALUE); + }else{ + //如果当前节点父ID不为空 则设置父节点的hasChildren 为1 + ${entityName} parent = baseMapper.selectById(${entityName?uncap_first}.get${pidFieldName?cap_first}()); + if(parent!=null && !"1".equals(parent.get${hasChildrenField?cap_first}())){ + parent.set${hasChildrenField?cap_first}("1"); + baseMapper.updateById(parent); + } + } + baseMapper.insert(${entityName?uncap_first}); + } + + @Override + public void update${entityName}(${entityName} ${entityName?uncap_first}) { + ${entityName} entity = this.getById(${entityName?uncap_first}.getId()); + if(entity==null) { + throw new JeecgBootException("未找到对应实体"); + } + String old_pid = entity.get${pidFieldName?cap_first}(); + String new_pid = ${entityName?uncap_first}.get${pidFieldName?cap_first}(); + if(!old_pid.equals(new_pid)) { + updateOldParentNode(old_pid); + if(oConvertUtils.isEmpty(new_pid)){ + ${entityName?uncap_first}.set${pidFieldName?cap_first}(I${entityName}Service.ROOT_PID_VALUE); + } + if(!I${entityName}Service.ROOT_PID_VALUE.equals(${entityName?uncap_first}.get${pidFieldName?cap_first}())) { + baseMapper.updateTreeNodeStatus(${entityName?uncap_first}.get${pidFieldName?cap_first}(), I${entityName}Service.HASCHILD); + } + } + baseMapper.updateById(${entityName?uncap_first}); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete${entityName}(String id) throws JeecgBootException { + //查询选中节点下所有子节点一并删除 + id = this.queryTreeChildIds(id); + if(id.indexOf(",")>0) { + StringBuffer sb = new StringBuffer(); + String[] idArr = id.split(","); + for (String idVal : idArr) { + if(idVal != null){ + ${entityName} ${entityName?uncap_first} = this.getById(idVal); + String pidVal = ${entityName?uncap_first}.get${pidFieldName?cap_first}(); + //查询此节点上一级是否还有其他子节点 + List<${entityName}> dataList = baseMapper.selectList(new QueryWrapper<${entityName}>().eq("${tableVo.extendParams.pidField}", pidVal).notIn("id",Arrays.asList(idArr))); + boolean flag = (dataList == null || dataList.size() == 0) && !Arrays.asList(idArr).contains(pidVal) && !sb.toString().contains(pidVal); + if(flag){ + //如果当前节点原本有子节点 现在木有了,更新状态 + sb.append(pidVal).append(","); + } + } + } + //批量删除节点 + baseMapper.deleteBatchIds(Arrays.asList(idArr)); + //修改已无子节点的标识 + String[] pidArr = sb.toString().split(","); + for(String pid : pidArr){ + this.updateOldParentNode(pid); + } + }else{ + ${entityName} ${entityName?uncap_first} = this.getById(id); + if(${entityName?uncap_first}==null) { + throw new JeecgBootException("未找到对应实体"); + } + updateOldParentNode(${entityName?uncap_first}.get${pidFieldName?cap_first}()); + baseMapper.deleteById(id); + } + } + + @Override + public List<${entityName}> queryTreeListNoPage(QueryWrapper<${entityName}> queryWrapper) { + List<${entityName}> dataList = baseMapper.selectList(queryWrapper); + List<${entityName}> mapList = new ArrayList<>(); + for(${entityName} data : dataList){ + String pidVal = data.get${pidFieldName?cap_first}(); + //递归查询子节点的根节点 + if(pidVal != null && !I${entityName}Service.NOCHILD.equals(pidVal)){ + ${entityName} rootVal = this.getTreeRoot(pidVal); + if(rootVal != null && !mapList.contains(rootVal)){ + mapList.add(rootVal); + } + }else{ + if(!mapList.contains(data)){ + mapList.add(data); + } + } + } + return mapList; + } + + @Override + public List queryListByCode(String parentCode) { + String pid = ROOT_PID_VALUE; + if (oConvertUtils.isNotEmpty(parentCode)) { + LambdaQueryWrapper<${entityName}> queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(${entityName}::get${pidFieldName?cap_first}, parentCode); + List<${entityName}> list = baseMapper.selectList(queryWrapper); + if (list == null || list.size() == 0) { + throw new JeecgBootException("该编码【" + parentCode + "】不存在,请核实!"); + } + if (list.size() > 1) { + throw new JeecgBootException("该编码【" + parentCode + "】存在多个,请核实!"); + } + pid = list.get(0).getId(); + } + return baseMapper.queryListByPid(pid, null); + } + + @Override + public List queryListByPid(String pid) { + if (oConvertUtils.isEmpty(pid)) { + pid = ROOT_PID_VALUE; + } + return baseMapper.queryListByPid(pid, null); + } + + /** + * 根据所传pid查询旧的父级节点的子节点并修改相应状态值 + * @param pid + */ + private void updateOldParentNode(String pid) { + if(!I${entityName}Service.ROOT_PID_VALUE.equals(pid)) { + Long count = baseMapper.selectCount(new QueryWrapper<${entityName}>().eq("${tableVo.extendParams.pidField}", pid)); + if(count==null || count<=1) { + baseMapper.updateTreeNodeStatus(pid, I${entityName}Service.NOCHILD); + } + } + } + + /** + * 递归查询节点的根节点 + * @param pidVal + * @return + */ + private ${entityName} getTreeRoot(String pidVal){ + ${entityName} data = baseMapper.selectById(pidVal); + if(data != null && !I${entityName}Service.ROOT_PID_VALUE.equals(data.get${pidFieldName?cap_first}()) && !data.get${pidFieldName?cap_first}().equals(data.getId())){ + return this.getTreeRoot(data.get${pidFieldName?cap_first}()); + }else{ + return data; + } + } + + /** + * 根据id查询所有子节点id + * @param ids + * @return + */ + private String queryTreeChildIds(String ids) { + //获取id数组 + String[] idArr = ids.split(","); + StringBuffer sb = new StringBuffer(); + for (String pidVal : idArr) { + if(pidVal != null){ + if(!sb.toString().contains(pidVal)){ + if(sb.toString().length() > 0){ + sb.append(","); + } + sb.append(pidVal); + this.getTreeChildIds(pidVal,sb); + } + } + } + return sb.toString(); + } + + /** + * 递归查询所有子节点 + * @param pidVal + * @param sb + * @return + */ + private StringBuffer getTreeChildIds(String pidVal,StringBuffer sb){ + List<${entityName}> dataList = baseMapper.selectList(new QueryWrapper<${entityName}>().eq("${tableVo.extendParams.pidField}", pidVal)); + if(dataList != null && dataList.size()>0){ + for(${entityName} tree : dataList) { + if(!sb.toString().contains(tree.getId())){ + sb.append(",").append(tree.getId()); + } + this.getTreeChildIds(tree.getId(),sb); + } + } + return sb; + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..b5758a2 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,523 @@ +<#assign pidFieldName = ""> +<#assign hasChildrenField = ""> +<#list originalColumns as po> +<#if po.fieldDbName == tableVo.extendParams.pidField> +<#assign pidFieldName = po.fieldName> + +<#if po.fieldDbName == tableVo.extendParams.hasChildren> +<#assign hasChildrenField = po.fieldName> + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..1a4fa5a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,246 @@ +<#include "/common/utils.ftl"> + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..acead0b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,564 @@ +<#include "/common/utils.ftl"> +<#assign pidFieldName = ""> +<#assign hasChildrenField = ""> +<#assign bpm_flag=false> +<#assign list_has_popup_dict=false> +<#list originalColumns as po> + <#if po.fieldDbName == tableVo.extendParams.pidField> + <#assign pidFieldName = po.fieldName> + + <#if po.fieldDbName == tableVo.extendParams.hasChildren> + <#assign hasChildrenField = po.fieldName> + + +<#assign list_need_pca=false> +<#assign buttonList=[]> +<#if tableVo.extendParams?? && tableVo.extendParams.cgButtonList??> + <#assign buttonList = tableVo.extendParams.cgButtonList?filter(btn -> btn??)> + +<#-- 开始循环 --> +<#list columns as po> + <#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.classType=='pca'> +<#assign list_need_pca=true> + +<#if po.classType=='popup_dict'> +<#assign list_has_popup_dict=true> + + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..b1fd11d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,85 @@ +import {defHttp} from "/@/utils/http/axios"; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/rootList', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + delete${entityName} = '/${entityPackagePath}/${entityName?uncap_first}/delete', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', + loadTreeData = '/${entityPackagePath}/${entityName?uncap_first}/loadTreeRoot', + getChildList = '/${entityPackagePath}/${entityName?uncap_first}/childList', + getChildListBatch = '/${entityPackagePath}/${entityName?uncap_first}/getChildListBatch', +} + +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; +/** + * 导入api + * @param params + */ +export const getImportUrl = Api.importExcel; +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); +/** + * 删除 + */ +export const delete${entityName} = (params,handleSuccess) => { + return defHttp.delete({url: Api.delete${entityName}, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const batchDelete${entityName} = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.delete${entityName}, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdateDict = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} +/** + * 查询全部树形节点数据 + * @param params + */ +export const loadTreeData = (params) => + defHttp.get({url: Api.loadTreeData,params}); +/** + * 查询子节点数据 + * @param params + */ +export const getChildList = (params) => + defHttp.get({url: Api.getChildList, params}); +/** + * 批量查询子节点数据 + * @param params + */ +export const getChildListBatch = (params) => + defHttp.get({url: Api.getChildListBatch, params},{isTransformResponse:false}); diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..dec5d76 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,560 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + <#if po.fieldDbName == tableVo.extendParams.textField> + align: 'left', + <#else> + align: 'center', + + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict' || po.classType=='link_table'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +<#-- 开始循环 --> +<#list columns as po> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isQuery=='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign query_flag=true> + <#assign query_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictField}"> + +<#if po.queryMode=='single'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${query_field_dictCode}" + }, +<#elseif po.classType=='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, +<#elseif po.classType=='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:"${po.dictField}" + + }, + <#elseif po.classType=='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}", + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}", + + triggerChange: true + }, + <#elseif po.classType=='cat_tree'> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}",//back和事件未添加,暂时有问题 + }, +<#elseif po.classType=='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, +<#elseif po.classType=='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true + }, +<#elseif po.classType=='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, +<#elseif po.classType=='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, +<#elseif po.classType=='popup'> + <#include "/common/form/vue3popup.ftl"> +<#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, +<#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + + <#if po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + + <#if po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, +<#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, +<#elseif po.classType=='list' || po.classType=='radio' || po.classType=='checkbox'> +<#-- ---------------------------下拉或是单选 判断数据字典是表字典还是普通字典------------------------------- --> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}" + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}" + + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', +<#else> + component: 'Input', + + //colProps: {span: 6}, + }, +<#elseif po.queryMode=='like'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", + component: 'JInput', + }, +<#else> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='date'> + component: 'RangePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueType: 'Date', + }, +<#elseif po.classType=='datetime'> + component: 'RangePicker', + componentProps: { + valueType: 'Date', + showTime:true + }, +<#elseif po.classType == 'time'> + component: 'TimePicker', + componentProps:{ + valueFormat: 'HH:mm:ss', + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'JRangeNumber', +<#-- update-begin---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#elseif po.classType=='time'> + component: 'RangeTime', +<#-- update-end---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#else> + component: 'Input', //TODO 范围查询 + + //colProps: {span: 6}, + }, + + + +<#-- 结束循环 --> +]; +//表单数据 +export const formSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign id_exists = false> +<#list columns as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName == 'id'> + <#assign id_exists = true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.fieldDbName == tableVo.extendParams.pidField> + component: 'JTreeSelect', + componentProps: { + dict: "${tableVo.tableName},${tableVo.extendParams.textField},id", + pidField: "${tableVo.extendParams.pidField}", + pidValue: "0", + hasChildField: "${tableVo.extendParams.hasChildren}", + }, + <#elseif po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps:{ + valueFormat: 'HH:mm:ss', + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + + <#if po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + + <#if po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern:/^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true + + }, + + +<#if id_exists == false> + // TODO 主键隐藏字段,目前写死为ID + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> + + +/** +* 流程表单调用这个方法获取formSchema +* @param param +*/ +export function getBpmFormSchema(_formData): FormSchema[]{ + // 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema + return formSchema; +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei new file mode 100644 index 0000000..3b5b65e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei @@ -0,0 +1,72 @@ +<#include "/common/utils.ftl"> + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei new file mode 100644 index 0000000..531645c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei @@ -0,0 +1,191 @@ +<#include "/common/utils.ftl"> +<#assign pidFieldName = ""> +<#assign hasChildrenField = ""> +<#list originalColumns as po> + <#if po.fieldDbName == tableVo.extendParams.pidField> + <#assign pidFieldName = po.fieldName> + + <#if po.fieldDbName == tableVo.extendParams.hasChildren> + <#assign hasChildrenField = po.fieldName> + + +<#assign buttonList=[]> +<#if tableVo.extendParams?? && tableVo.extendParams.cgButtonList??> + <#assign buttonList = tableVo.extendParams.cgButtonList?filter(btn -> btn??)> + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei new file mode 100644 index 0000000..01decc7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei @@ -0,0 +1,738 @@ +<#include "/common/utils.ftl"> + + +<#if query_flag> + + + + 查询 + 重置 + + {{ toggleSearchStatus ? '收起' : '展开' }} + + + + + + + + + +<#-- 结束循环 --> + + + + + + + + + + + <${entityName}Modal ref="registerModal" @success="handleSuccess"> + <#if bpm_flag==true> + + + + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi new file mode 100644 index 0000000..7944cae --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi @@ -0,0 +1,93 @@ +import { defHttp } from "/@/utils/http/axios"; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/rootList', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + delete${entityName} = '/${entityPackagePath}/${entityName?uncap_first}/delete', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', + loadTreeData = '/${entityPackagePath}/${entityName?uncap_first}/loadTreeRoot', + getChildList = '/${entityPackagePath}/${entityName?uncap_first}/childList', + getChildListBatch = '/${entityPackagePath}/${entityName?uncap_first}/getChildListBatch', +} + +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + * @param params + */ +export const getImportUrl = Api.importExcel; + +/** + * 列表接口 + * @param params + */ +export const list = (params) => defHttp.get({ url: Api.list, params }); + +/** + * 删除 + * @param params + * @param handleSuccess + */ +export const delete${entityName} = (params,handleSuccess) => { + return defHttp.delete({ url: Api.delete${entityName}, params }, { joinParamsToUrl: true }).then(() => { + handleSuccess(); + }); +} + +/** + * 批量删除 + * @param params + * @param handleSuccess + */ +export const batchDelete${entityName} = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({ url: Api.delete${entityName}, data: params }, { joinParamsToUrl: true }).then(() => { + handleSuccess(); + }); + } + }); +} + +/** + * 保存或者更新 + * @param params + * @param isUpdate + */ +export const saveOrUpdateDict = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({ url: url, params },{ isTransformResponse:false }); +} + +/** + * 查询全部树形节点数据 + * @param params + */ +export const loadTreeData = (params) => defHttp.get({ url: Api.loadTreeData,params }); + +/** + * 查询子节点数据 + * @param params + */ +export const getChildList = (params) => defHttp.get({ url: Api.getChildList, params }); + +/** + * 批量查询子节点数据 + * @param params + */ +export const getChildListBatch = (params) => defHttp.get({ url: Api.getChildListBatch, params },{ isTransformResponse:false }); diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi new file mode 100644 index 0000000..e6c3447 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi @@ -0,0 +1,94 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + <#if po.fieldDbName == tableVo.extendParams.textField> + align: 'left', + <#else> + align: 'center', + + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender: render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]); + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}'); + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : ''); + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei new file mode 100644 index 0000000..12dfa60 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei @@ -0,0 +1,316 @@ +<#include "/common/utils.ftl"> + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei new file mode 100644 index 0000000..7242007 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/default/tree/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei @@ -0,0 +1,101 @@ +<#include "/common/utils.ftl"> + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..4c143de --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,403 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import org.jeecg.common.system.query.QueryGenerator; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.jeecg.common.system.query.QueryRuleEnum; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.system.base.controller.JeecgController; +import org.jeecg.common.api.vo.Result; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.ModelAndView; +import java.util.Arrays; +import java.util.HashMap; +import org.jeecg.common.util.oConvertUtils; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.service.I${sub.entityName}Service; + +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.shiro.authz.annotation.RequiresPermissions; +<#assign has_multi_query_field=false> +<#list originalColumns as po> +<#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + <#assign has_multi_query_field=true> + + +<#assign enhanceJavaList=[]> +<#if tableVo.extendParams?? && tableVo.extendParams.enhanceJavaList??> + <#assign enhanceJavaList = tableVo.extendParams.enhanceJavaList?filter(enhance -> enhance??)> + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackagePath}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller extends JeecgController<${entityName}, I${entityName}Service> { + + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + <#list subTables as sub> + + @Autowired + private I${sub.entityName}Service ${sub.entityName?uncap_first}Service; + + + + /*---------------------------------主表处理-begin-------------------------------------*/ + + /** + * 分页列表查询 + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 查询前触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeQuery() + + + + <#if has_multi_query_field> + // 自定义查询规则 + Map customeRuleMap = new HashMap<>(); + // 自定义多选的查询规则为:LIKE_WITH_OR + <#list originalColumns as po> + <#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + customeRuleMap.put("${po.fieldName}", QueryRuleEnum.LIKE_WITH_OR); + + + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap(),customeRuleMap); + <#else> + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 查询后触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterQuery() + + + + return Result.OK(pageList); + } + + /** + * 添加 + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @RequiresPermissions("${entityPackage}:${tableName}:add") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName} ${entityName?uncap_first}) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 新增前的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeAdd() + + + + ${entityName?uncap_first}Service.save(${entityName?uncap_first}); +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 新增后的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterAdd() + + + + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequiresPermissions("${entityPackage}:${tableName}:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName} ${entityName?uncap_first}) { +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 编辑前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeEdit() + + + + ${entityName?uncap_first}Service.updateById(${entityName?uncap_first}); +<#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 编辑后,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterEdit() + + + + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @RequiresPermissions("${entityPackage}:${tableName}:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delMain(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @RequiresPermissions("${entityPackage}:${tableName}:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.delBatchMain(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 导出 + * @return + */ + @RequiresPermissions("${entityPackage}:${tableName}:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='export' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导出前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeExport() + + + + return super.exportXls(request, ${entityName?uncap_first}, ${entityName}.class, "${tableVo.ftlDescription}"); + } + + /** + * 导入 + * @return + */ + @RequiresPermissions("${entityPackage}:${tableName}:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='import' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导入前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeImport() + + + + return super.importExcel(request, response, ${entityName}.class); + } + /*---------------------------------主表处理-end-------------------------------------*/ + + <#list subTables as sub> + + /*--------------------------------子表处理-${sub.ftlDescription}-begin----------------------------------------------*/ + /** + * 通过主表ID查询 + * @return + */ + //@AutoLog(value = "${sub.ftlDescription}-通过主表ID查询") + @Operation(summary="${sub.ftlDescription}-通过主表ID查询") + @GetMapping(value = "/list${sub.entityName}ByMainId") + public Result> list${sub.entityName}ByMainId(${sub.entityName} ${sub.entityName?uncap_first}, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper<${sub.entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${sub.entityName?uncap_first}, req.getParameterMap()); + Page<${sub.entityName}> page = new Page<${sub.entityName}>(pageNo, pageSize); + IPage<${sub.entityName}> pageList = ${sub.entityName?uncap_first}Service.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * @param ${sub.entityName?uncap_first} + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-添加") + @Operation(summary="${sub.ftlDescription}-添加") + @PostMapping(value = "/add${sub.entityName}") + public Result add${sub.entityName}(@RequestBody ${sub.entityName} ${sub.entityName?uncap_first}) { + ${sub.entityName?uncap_first}Service.save(${sub.entityName?uncap_first}); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * @param ${sub.entityName?uncap_first} + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-编辑") + @Operation(summary="${sub.ftlDescription}-编辑") + @RequestMapping(value = "/edit${sub.entityName}", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit${sub.entityName}(@RequestBody ${sub.entityName} ${sub.entityName?uncap_first}) { + ${sub.entityName?uncap_first}Service.updateById(${sub.entityName?uncap_first}); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * @param id + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-通过id删除") + @Operation(summary="${sub.ftlDescription}-通过id删除") + @DeleteMapping(value = "/delete${sub.entityName}") + public Result delete${sub.entityName}(@RequestParam(name="id",required=true) String id) { + ${sub.entityName?uncap_first}Service.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * @param ids + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-批量删除") + @Operation(summary="${sub.ftlDescription}-批量删除") + @DeleteMapping(value = "/deleteBatch${sub.entityName}") + public Result deleteBatch${sub.entityName}(@RequestParam(name="ids",required=true) String ids) { + this.${sub.entityName?uncap_first}Service.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 导出 + * @return + */ + @RequestMapping(value = "/export${sub.entityName}") + public ModelAndView export${sub.entityName}(HttpServletRequest request, ${sub.entityName} ${sub.entityName?uncap_first}) { + // Step.1 组装查询条件 + QueryWrapper<${sub.entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${sub.entityName?uncap_first}, request.getParameterMap()); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + // Step.2 获取导出数据 + List<${sub.entityName}> pageList = ${sub.entityName?uncap_first}Service.list(queryWrapper); + List<${sub.entityName}> exportList = null; + + // 过滤选中数据 + String selections = request.getParameter("selections"); + if (oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + exportList = pageList.stream().filter(item -> selectionList.contains(item.getId())).collect(Collectors.toList()); + } else { + exportList = pageList; + } + + // Step.3 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + //此处设置的filename无效,前端会重更新设置一下 + mv.addObject(NormalExcelConstants.FILE_NAME, "${sub.ftlDescription}"); + mv.addObject(NormalExcelConstants.CLASS, ${sub.entityName}.class); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("${sub.ftlDescription}报表", "导出人:" + sysUser.getRealname(), "${sub.ftlDescription}", ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST, exportList); + return mv; + } + + /** + * 导入 + * @return + */ + @RequestMapping(value = "/import${sub.entityName}/{mainId}") + public Result import${sub.entityName}(HttpServletRequest request, HttpServletResponse response, @PathVariable("mainId") String mainId) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List<${sub.entityName}> list = ExcelImportUtil.importExcel(file.getInputStream(), ${sub.entityName}.class, params); + for (${sub.entityName} temp : list) { + <#list sub.foreignKeys as key> + temp.set${key?cap_first}(mainId); + + } + long start = System.currentTimeMillis(); + ${sub.entityName?uncap_first}Service.saveBatch(list); + log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒"); + return Result.OK("文件导入成功!数据行数:" + list.size()); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Result.error("文件导入失败:" + e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.error("文件导入失败!"); + } + + /*--------------------------------子表处理-${sub.ftlDescription}-end----------------------------------------------*/ + + + + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..f68b1f8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,82 @@ +<#include "/common/utils.ftl"> +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecg.common.aspect.annotation.Dict; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${tableName}") +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + + <#include "/common/blob.ftl"> + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai new file mode 100644 index 0000000..2108b24 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai @@ -0,0 +1,86 @@ +<#include "/common/utils.ftl"> +<#list subTables as subTab> +#segment#${subTab.entityName}.java +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.aspect.annotation.Dict; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import java.util.Date; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.UnsupportedEncodingException; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${subTab.tableName}") +@Schema(description="${subTab.ftlDescription}") +public class ${subTab.entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> +<#list subTab.originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ +<#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) +<#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#elseif !subTab.foreignKeys?seq_contains(po.fieldName?cap_first)> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + <#-- 大字段转换 --> + <#include "/common/blob.ftl"> + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai new file mode 100644 index 0000000..6b6736d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai @@ -0,0 +1,35 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Mapper.java +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${subTab.entityName}Mapper extends BaseMapper<${subTab.entityName}> { + + /** + * 通过主表id删除子表数据 + * + * @param mainId 主表id + * @return boolean + */ + public boolean deleteByMainId(@Param("mainId") String mainId); + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(@Param("mainId") String mainId); + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml new file mode 100644 index 0000000..1dc5802 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml @@ -0,0 +1,28 @@ +<#list subTables as subTab> +<#assign originalForeignKeys = subTab.originalForeignKeys> +#segment#${subTab.entityName}Mapper.xml + + + + + + DELETE + FROM ${subTab.tableName} + WHERE + <#list originalForeignKeys as key> + ${key} = ${r'#'}{mainId} <#rt/> + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..e22b54c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,36 @@ +package ${bussiPackage}.${entityPackage}.service; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import org.springframework.beans.factory.annotation.Autowired; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /** + * 删除一对多 + * + * @param id + */ + public void delMain (String id); + + /** + * 批量删除一对多 + * + * @param idList + */ + public void delBatchMain (Collection idList); + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai new file mode 100644 index 0000000..74ad5a7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai @@ -0,0 +1,25 @@ +<#list subTables as subTab> +#segment#I${subTab.entityName}Service.java +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${subTab.entityName}Service extends IService<${subTab.entityName}> { + + /** + * 通过主表id查询子表数据 + * + * @param mainId + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(String mainId); +} + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..a98cfee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,56 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.mapper.${sub.entityName}Mapper; + +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.io.Serializable; +import java.util.List; +import java.util.Collection; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Autowired + private ${entityName}Mapper ${entityName?uncap_first}Mapper; + <#list subTables as sub> + @Autowired + private ${sub.entityName}Mapper ${sub.entityName?uncap_first}Mapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void delMain(String id) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatchMain(Collection idList) { + for(Serializable id:idList) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id.toString()); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai new file mode 100644 index 0000000..0ce41d3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai @@ -0,0 +1,30 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}ServiceImpl.java +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${subTab.entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${subTab.entityName}Service; +import org.springframework.stereotype.Service; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${subTab.entityName}ServiceImpl extends ServiceImpl<${subTab.entityName}Mapper, ${subTab.entityName}> implements I${subTab.entityName}Service { + + @Autowired + private ${subTab.entityName}Mapper ${subTab.entityName?uncap_first}Mapper; + + @Override + public List<${subTab.entityName}> selectByMainId(String mainId) { + return ${subTab.entityName?uncap_first}Mapper.selectByMainId(mainId); + } +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..9d2cbbb --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,457 @@ +<#include "/common/utils.ftl"> + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/[1-n]List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/[1-n]List.vuei new file mode 100644 index 0000000..dae1445 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/[1-n]List.vuei @@ -0,0 +1,327 @@ +<#list subTables as sub> +#segment#${sub.entityName}List.vue + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..f20d988 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,212 @@ +<#include "/common/utils.ftl"> + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Modal.vuei new file mode 100644 index 0000000..f5e31a9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Modal.vuei @@ -0,0 +1,208 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +#segment#${sub.entityName}Modal.vue + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..a3d1c2b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,473 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..c130074 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,133 @@ +import {defHttp} from '/@/utils/http/axios'; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/list', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackagePath}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackagePath}/${entityName?uncap_first}/list${sub.entityName}ByMainId', + ${sub.entityName?uncap_first}Save='/${entityPackagePath}/${entityName?uncap_first}/add${sub.entityName}', + ${sub.entityName?uncap_first}Edit='/${entityPackagePath}/${entityName?uncap_first}/edit${sub.entityName}', + ${sub.entityName?uncap_first}Delete = '/${entityPackagePath}/${entityName?uncap_first}/delete${sub.entityName}', + ${sub.entityName?uncap_first}DeleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch${sub.entityName}', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const batchDelete = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} +<#list subTables as sub><#rt/> + <#assign myForeignKeys=''> + <#list sub.foreignKeys as key> + <#assign myForeignKeys='${key?uncap_first}'> + +/** + * 列表接口 + * @param params + */ +export const ${sub.entityName?uncap_first}List = (params) => { + if(params['${myForeignKeys}']){ + return defHttp.get({url: Api.${sub.entityName?uncap_first}List, params}); + } + return Promise.resolve({}); +} + + +/** + * 删除单个 + */ +export const ${sub.entityName?uncap_first}Delete = (params,handleSuccess) => { + return defHttp.delete({url: Api.${sub.entityName?uncap_first}Delete, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const ${sub.entityName?uncap_first}DeleteBatch = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.${sub.entityName?uncap_first}DeleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const ${sub.entityName?uncap_first}SaveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.${sub.entityName?uncap_first}Edit : Api.${sub.entityName?uncap_first}Save; + return defHttp.post({url: url, params}); +} +/** + * 导入 + */ +export const ${sub.entityName?uncap_first}ImportUrl = '/${entityPackagePath}/${entityName?uncap_first}/import${sub.entityName}' + +/** + * 导出 + */ +export const ${sub.entityName?uncap_first}ExportXlsUrl = '/${entityPackagePath}/${entityName?uncap_first}/export${sub.entityName}' + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..25b8e2a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,850 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict' || po.classType=='link_table'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: (text, record) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +<#-- 开始循环 --> +<#list columns as po> +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isQuery=='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign query_flag=true> + <#assign query_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictField}"> + +<#if po.queryMode=='single'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${query_field_dictCode}" + }, +<#elseif po.classType=='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, +<#elseif po.classType=='switch'> + component: 'JSwitch', + componentProps:{ + query:true, + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType=='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}", + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}", + + triggerChange: true + }, + <#elseif po.classType=='cat_tree'> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}",//back和事件未添加,暂时有问题 + }, +<#elseif po.classType=='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, +<#elseif po.classType=='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, +<#elseif po.classType=='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, +<#elseif po.classType=='popup'> + <#include "/common/form/vue3popup.ftl"> +<#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, +<#elseif po.classType=='list' || po.classType=='radio' || po.classType=='checkbox'> +<#-- ---------------------------下拉或是单选 判断数据字典是表字典还是普通字典------------------------------- --> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}" + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}" + + }, +<#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', +<#else> + component: 'Input', + + //colProps: {span: 6}, + }, +<#elseif po.queryMode=='like'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", + component: 'JInput', + }, +<#else> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='date'> + component: 'RangePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueType: 'Date', + }, +<#elseif po.classType=='datetime'> + component: 'RangePicker', + componentProps: { + valueType: 'Date', + showTime:true + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'JRangeNumber', +<#-- update-begin---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#elseif po.classType=='time'> + component: 'RangeTime', +<#-- update-end---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#else> + component: 'Input', //TODO 范围查询 + + //colProps: {span: 6}, + }, + + + +<#-- 结束循环 --> +]; + +//表单数据 +export const formSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign id_exists = false> +<#list columns as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName == 'id'> + <#assign id_exists = true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + <#else> + labelKey:'realname', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea',//TODO 注意string转换问题 + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, + <#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern:/^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true, + + }, + + +<#if id_exists == false> + // TODO 主键隐藏字段,目前写死为ID + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; + +<#list subTables as sub> +//子表列表数据 +export const ${sub.entityName?uncap_first}Columns: BasicColumn[] = [ + <#list sub.originalColumns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='link_table'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//子表表单数据 +export const ${sub.entityName?uncap_first}FormSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#list sub.originalColumns as po><#rt/> +<#if po.fieldName == 'id'> + // TODO 子表隐藏字段,目前写死为ID + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + <#else> + labelKey:'realname', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list' || po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, + <#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true, + + }, + + +]; + + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/[1-n]List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/[1-n]List.vuei new file mode 100644 index 0000000..d882ad5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/[1-n]List.vuei @@ -0,0 +1,189 @@ +<#list subTables as sub> +#segment#${sub.entityName}List.vue + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei new file mode 100644 index 0000000..54e87d0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei @@ -0,0 +1,129 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Modal.vuei new file mode 100644 index 0000000..526b182 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Modal.vuei @@ -0,0 +1,114 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +#segment#${sub.entityName}Modal.vue + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei new file mode 100644 index 0000000..09c1a61 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei @@ -0,0 +1,613 @@ +<#include "/common/utils.ftl"> + + +<#if query_flag> + + + + 查询 + 重置 + + {{ toggleSearchStatus ? '收起' : '展开' }} + + + + + + + + + +<#-- 结束循环 --> +

+ + + + + + + + + + + + <#assign sub_seq=1> + <#list subTables as sub> + forceRender> + <${sub.entityName}List /> + + <#assign sub_seq=sub_seq+1> + + +
+ + <${entityName}Modal ref="registerModal" @success="handleSuccess" /> + <#if bpm_flag==true> + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi new file mode 100644 index 0000000..e40d27c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi @@ -0,0 +1,139 @@ +import {defHttp} from '/@/utils/http/axios'; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/list', + save= '/${entityPackagePath}/${entityName?uncap_first}/add', + edit= '/${entityPackagePath}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackagePath}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackagePath}/${entityName?uncap_first}/list${sub.entityName}ByMainId', + ${sub.entityName?uncap_first}Save= '/${entityPackagePath}/${entityName?uncap_first}/add${sub.entityName}', + ${sub.entityName?uncap_first}Edit= '/${entityPackagePath}/${entityName?uncap_first}/edit${sub.entityName}', + ${sub.entityName?uncap_first}Delete = '/${entityPackagePath}/${entityName?uncap_first}/delete${sub.entityName}', + ${sub.entityName?uncap_first}DeleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch${sub.entityName}', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; + +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({ url: Api.list, params }); + +/** + * 删除单个 + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => { + handleSuccess(); + }); +} + +/** + * 批量删除 + * @param params + */ +export const batchDelete = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => { + handleSuccess(); + }); + } + }); +} + +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({ url: url, params },{ isTransformResponse: false }); +} +<#list subTables as sub><#rt/> + <#assign myForeignKeys=''> + <#list sub.foreignKeys as key> + <#assign myForeignKeys='${key?uncap_first}'> + + +/** + * 列表接口 + * @param params + */ +export const ${sub.entityName?uncap_first}List = (params) => { + if(params['${myForeignKeys}']){ + return defHttp.get({ url: Api.${sub.entityName?uncap_first}List, params }); + } + return Promise.resolve({}); +} + +/** + * 删除单个 + */ +export const ${sub.entityName?uncap_first}Delete = (params,handleSuccess) => { + return defHttp.delete({ url: Api.${sub.entityName?uncap_first}Delete, params }, { joinParamsToUrl: true }).then(() => { + handleSuccess(); + }); +} + +/** + * 批量删除 + * @param params + */ +export const ${sub.entityName?uncap_first}DeleteBatch = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({ url: Api.${sub.entityName?uncap_first}DeleteBatch, data: params }, { joinParamsToUrl: true }).then(() => { + handleSuccess(); + }); + } + }); +} + +/** + * 保存或者更新 + * @param params + */ +export const ${sub.entityName?uncap_first}SaveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.${sub.entityName?uncap_first}Edit : Api.${sub.entityName?uncap_first}Save; + return defHttp.post({ url: url, params },{ isTransformResponse: false }); +} + +/** + * 导入 + */ +export const ${sub.entityName?uncap_first}ImportUrl = '/${entityPackagePath}/${entityName?uncap_first}/import${sub.entityName}' + +/** + * 导出 + */ +export const ${sub.entityName?uncap_first}ExportXlsUrl = '/${entityPackagePath}/${entityName?uncap_first}/export${sub.entityName}' + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi new file mode 100644 index 0000000..bd01e27 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi @@ -0,0 +1,155 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: (text, record) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; + +<#list subTables as sub> +//子表列表数据 +export const ${sub.entityName?uncap_first}Columns: BasicColumn[] = [ + <#list sub.originalColumns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + return !text?"":(text.length>10?text.substr(0,10):text) + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; + + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/[1-n]List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/[1-n]List.vuei new file mode 100644 index 0000000..39ea595 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/[1-n]List.vuei @@ -0,0 +1,250 @@ +<#list subTables as sub> +#segment#${sub.entityName}List.vue +<#assign need_pca = false> +<#assign is_like = false> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei new file mode 100644 index 0000000..abd2775 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei @@ -0,0 +1,252 @@ +<#include "/common/utils.ftl"> + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei new file mode 100644 index 0000000..2bc8816 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei @@ -0,0 +1,105 @@ +<#include "/common/utils.ftl"> + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Form.vuei new file mode 100644 index 0000000..87511dc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Form.vuei @@ -0,0 +1,228 @@ +<#list subTables as sub> +#segment#${sub.entityName}Form.vue +<#include "/common/utils.ftl"> +<#assign need_category = false> +<#assign bpm_flag=false> +<#assign need_pca = false> +<#assign need_search = false> +<#assign need_dept_user = false> +<#assign need_switch = false> +<#assign need_dept = false> +<#assign need_multi = false> +<#assign need_popup = false> +<#assign need_popup_dict = false> +<#assign need_select_tag = false> +<#assign need_select_tree = false> +<#assign need_time = false> +<#assign need_markdown = false> +<#assign need_upload = false> +<#assign need_image_upload = false> +<#assign need_editor = false> +<#assign need_checkbox = false> +<#assign need_range_number = false> +<#assign is_like = false> +<#assign form_span = 24> +<#if tableVo.fieldRowNum==2> + <#assign form_span = 12> +<#elseif tableVo.fieldRowNum==3> + <#assign form_span = 8> +<#elseif tableVo.fieldRowNum==4> + <#assign form_span = 6> + + <#assign hasOnlyValidate = false> + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Modal.vuei new file mode 100644 index 0000000..2218e92 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/erp/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Modal.vuei @@ -0,0 +1,79 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +#segment#${sub.entityName}Modal.vue + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..932ba94 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,368 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.stream.Collectors; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecg.common.system.vo.LoginUser; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.system.query.QueryRuleEnum; +import org.jeecg.common.util.oConvertUtils; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.vo.${entityName}Page; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.service.I${sub.entityName}Service; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.apache.shiro.authz.annotation.RequiresPermissions; +<#assign has_multi_query_field=false> +<#list originalColumns as po> +<#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + <#assign has_multi_query_field=true> + + +<#assign enhanceJavaList=[]> +<#if tableVo.extendParams?? && tableVo.extendParams.enhanceJavaList??> + <#assign enhanceJavaList = tableVo.extendParams.enhanceJavaList?filter(enhance -> enhance??)> + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackagePath}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + <#list subTables as sub> + @Autowired + private I${sub.entityName}Service ${sub.entityName?uncap_first}Service; + + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 查询前触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeQuery() + + + + <#if has_multi_query_field> + // 自定义查询规则 + Map customeRuleMap = new HashMap<>(); + // 自定义多选的查询规则为:LIKE_WITH_OR + <#list originalColumns as po> + <#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + customeRuleMap.put("${po.fieldName}", QueryRuleEnum.LIKE_WITH_OR); + + + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap(),customeRuleMap); + <#else> + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 查询后触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterQuery() + + + + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @RequiresPermissions("${entityPackage}:${tableName}:add") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 新增前的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeAdd() + + + + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName?uncap_first}Service.saveMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 新增后的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterAdd() + + + + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequiresPermissions("${entityPackage}:${tableName}:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 编辑前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeEdit() + + + + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName} ${entityName?uncap_first}Entity = ${entityName?uncap_first}Service.getById(${entityName?uncap_first}.getId()); + if(${entityName?uncap_first}Entity==null) { + return Result.error("未找到对应数据"); + } + ${entityName?uncap_first}Service.updateMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 编辑后,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterEdit() + + + + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @RequiresPermissions("${entityPackage}:${tableName}:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delMain(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @RequiresPermissions("${entityPackage}:${tableName}:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.delBatchMain(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result<${entityName}> queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + if(${entityName?uncap_first}==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(${entityName?uncap_first}); + + } + + <#list subTables as sub> + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${sub.ftlDescription}-通过主表ID查询") + @Operation(summary="${sub.ftlDescription}-通过主表ID查询") + @GetMapping(value = "/query${sub.entityName}ByMainId") + public Result> query${sub.entityName}ListByMainId(@RequestParam(name="id",required=true) String id) { + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(id); + <#-- 包裹分页对象,用于翻译注解 --> + IPage <${sub.entityName}> page = new Page<>(); + page.setRecords(${sub.entityName?uncap_first}List); + page.setTotal(${sub.entityName?uncap_first}List.size()); + return Result.OK(page); + } + + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequiresPermissions("${entityPackage}:${tableName}:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='export' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导出前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeExport() + + + + // Step.1 组装查询条件查询数据 + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, request.getParameterMap()); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + //配置选中数据查询条件 + String selections = request.getParameter("selections"); + if(oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + queryWrapper.in("id",selectionList); + } + //Step.2 获取导出数据 + List<${entityName}> ${entityName?uncap_first}List = ${entityName?uncap_first}Service.list(queryWrapper); + + // Step.3 组装pageList + List<${entityName}Page> pageList = new ArrayList<${entityName}Page>(); + for (${entityName} main : ${entityName?uncap_first}List) { + ${entityName}Page vo = new ${entityName}Page(); + BeanUtils.copyProperties(main, vo); + <#list subTables as sub> + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(main.getId()); + vo.set${sub.entityName}List(${sub.entityName?uncap_first}List); + + pageList.add(vo); + } + + // Step.4 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + mv.addObject(NormalExcelConstants.FILE_NAME, "${tableVo.ftlDescription}列表"); + mv.addObject(NormalExcelConstants.CLASS, ${entityName}Page.class); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("${tableVo.ftlDescription}数据", "导出人:"+sysUser.getRealname(), "${tableVo.ftlDescription}", ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("${entityPackage}:${tableName}:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='import' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导入前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeImport() + + + + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List<${entityName}Page> list = ExcelImportUtil.importExcel(file.getInputStream(), ${entityName}Page.class, params); + for (${entityName}Page page : list) { + ${entityName} po = new ${entityName}(); + BeanUtils.copyProperties(page, po); + ${entityName?uncap_first}Service.saveMain(po, <#list subTables as sub>page.get${sub.entityName}List()<#if sub_has_next>,); + } + return Result.OK("文件导入成功!数据行数:" + list.size()); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.OK("文件导入失败!"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..5762ece --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,82 @@ +<#include "/common/utils.ftl"> +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.aspect.annotation.Dict; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${tableVo.ftlDescription}") +@Data +@TableName("${tableName}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + + <#include "/common/blob.ftl"> + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai new file mode 100644 index 0000000..a81400f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai @@ -0,0 +1,86 @@ +<#include "/common/utils.ftl"> +<#list subTables as subTab> +#segment#${subTab.entityName}.java +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import java.util.Date; +import org.jeecg.common.aspect.annotation.Dict; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.UnsupportedEncodingException; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${subTab.ftlDescription}") +@Data +@TableName("${subTab.tableName}") +public class ${subTab.entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list subTab.originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#elseif !subTab.foreignKeys?seq_contains(po.fieldName?cap_first)> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + <#-- 大字段转换 --> + <#include "/common/blob.ftl"> + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai new file mode 100644 index 0000000..10b2764 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai @@ -0,0 +1,34 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Mapper.java +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${subTab.entityName}Mapper extends BaseMapper<${subTab.entityName}> { + + /** + * 通过主表id删除子表数据 + * + * @param mainId 主表id + * @return boolean + */ + public boolean deleteByMainId(@Param("mainId") String mainId); + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(@Param("mainId") String mainId); +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml new file mode 100644 index 0000000..117c9b6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml @@ -0,0 +1,26 @@ +<#list subTables as subTab> +<#assign originalForeignKeys = subTab.originalForeignKeys> +#segment#${subTab.entityName}Mapper.xml + + + + + + DELETE + FROM ${subTab.tableName} + WHERE + <#list originalForeignKeys as key> + ${key} = ${r'#'}{mainId} <#rt/> + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..d80c029 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,54 @@ +package ${bussiPackage}.${entityPackage}.service; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /** + * 添加一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void saveMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) ; + + /** + * 修改一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,); + + /** + * 删除一对多 + * + * @param id + */ + public void delMain (String id); + + /** + * 批量删除一对多 + * + * @param idList + */ + public void delBatchMain (Collection idList); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai new file mode 100644 index 0000000..cbc72ff --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai @@ -0,0 +1,25 @@ +<#list subTables as subTab> +#segment#I${subTab.entityName}Service.java +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${subTab.entityName}Service extends IService<${subTab.entityName}> { + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(String mainId); +} + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..7f99d42 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,105 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.mapper.${sub.entityName}Mapper; + +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.io.Serializable; +import java.util.List; +import java.util.Collection; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Autowired + private ${entityName}Mapper ${entityName?uncap_first}Mapper; + <#list subTables as sub> + @Autowired + private ${sub.entityName}Mapper ${sub.entityName?uncap_first}Mapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMain(${entityName} ${entityName?uncap_first}, <#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.insert(${entityName?uncap_first}); + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.updateById(${entityName?uncap_first}); + + //1.先删除子表数据 + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(${entityName?uncap_first}.getId()); + + + //2.子表数据重新插入 + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delMain(String id) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatchMain(Collection idList) { + for(Serializable id:idList) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id.toString()); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai new file mode 100644 index 0000000..0ce41d3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai @@ -0,0 +1,30 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}ServiceImpl.java +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${subTab.entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${subTab.entityName}Service; +import org.springframework.stereotype.Service; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${subTab.entityName}ServiceImpl extends ServiceImpl<${subTab.entityName}Mapper, ${subTab.entityName}> implements I${subTab.entityName}Service { + + @Autowired + private ${subTab.entityName}Mapper ${subTab.entityName?uncap_first}Mapper; + + @Override + public List<${subTab.entityName}> selectByMainId(String mainId) { + return ${subTab.entityName?uncap_first}Mapper.selectByMainId(mainId); + } +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai new file mode 100644 index 0000000..218e489 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai @@ -0,0 +1,117 @@ +package ${bussiPackage}.${entityPackage}.vo; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecgframework.poi.excel.annotation.ExcelEntity; +import org.jeecgframework.poi.excel.annotation.ExcelCollection; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import java.util.Date; +import org.jeecg.common.aspect.annotation.Dict; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName}Page { + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> +<#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "realname", dicCode = "username"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "depart_name", dicCode = "id"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + <#elseif po.classType=='cat_tree'> + <#assign list_field_dictCode=', dictTable = "sys_category", dicText = "name", dicCode = "id"'> + + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + <#if list_field_dictCode?length gt 1 && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Dict(${list_field_dictCode?substring(2)}) + + + + @Schema(description = "${po.filedComment}") + <#if po.fieldDbType=='Blob'> + private java.lang.String ${po.fieldName}String; + <#elseif po.classType=='pca'> + @Excel(name = "${po.filedComment}", width = 15,exportConvert=true,importConvert = true ) + private ${po.fieldType} ${po.fieldName}; + + public String convertis${po.fieldName?cap_first}() { + return SpringContextUtils.getBean(ProvinceCityArea.class).getText(${po.fieldName}); + } + + public void convertset${po.fieldName?cap_first}(String text) { + this.${po.fieldName} = SpringContextUtils.getBean(ProvinceCityArea.class).getCode(text); + } + <#elseif po.classType=='cat_tree'> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + private ${po.fieldType} ${po.fieldName}; + <#elseif po.classType=='switch'> + <#assign switch_extend_arr=['Y','N']> + <#if po.dictField?default("")?contains("[")> + <#assign switch_extend_arr=po.dictField?eval> + + <#list switch_extend_arr as a> + <#if a_index == 0> + <#assign switch_extend_arr1=a> + <#else> + <#assign switch_extend_arr2=a> + + + @Excel(name = "${po.filedComment}", width = 15,replace = {"是_${switch_extend_arr1}","否_${switch_extend_arr2}"} ) + private ${po.fieldType} ${po.fieldName}; + <#else> + private ${po.fieldType} ${po.fieldName}; + + + + <#list subTables as sub> + @ExcelCollection(name="${sub.ftlDescription}") + @Schema(description = "${sub.ftlDescription}") + private List<${sub.entityName}> ${sub.entityName?uncap_first}List; + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..aa40235 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,509 @@ +<#-- noinspection JSDuplicatedDeclaration,RequiredAttributes,NpmUsedModulesInstalled --> +<#-- ** 引入全局工具方法 ** --> +<#include "/common/utils.ftl"> +<#-- ** 定义全局使用的变量 ** --> +<#-- 是否有查询条件 --> +<#assign query_flag=false> +<#-- 是否有下拉查询条件 --> +<#assign query_field_select=false> +<#-- 是否有日期查询条件 --> +<#assign query_field_date=false> +<#-- 是否有字典 --> +<#assign list_need_dict=false> +<#-- 是否有分类字典 --> +<#assign list_need_category=false> +<#-- 是否有省市区 --> +<#assign list_need_pca=false> +<#-- 是否有用户选择 --> +<#assign query_sel_user=false> +<#-- 是否有部门选择 --> +<#assign query_sel_dep=false> +<#-- 是否有下拉多选框 --> +<#assign query_sel_multi=false> +<#-- 是否有下拉搜索框 --> +<#assign query_sel_search=false> +<#-- 是否有省市区组件 --> +<#assign query_field_pca=false> +<#-- 是否有分类字典树 --> +<#assign query_sel_cat=false> + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei new file mode 100644 index 0000000..d2d6ac6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei @@ -0,0 +1,545 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..5d789e9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,62 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei new file mode 100644 index 0000000..9b697c6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei @@ -0,0 +1,167 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/subTables/[1-n]SubTable.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/subTables/[1-n]SubTable.vuei new file mode 100644 index 0000000..a68776e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue/subTables/[1-n]SubTable.vuei @@ -0,0 +1,146 @@ +<#--noinspection JSDuplicatedDeclaration--> +<#list subTables as sub> +#segment#${sub.entityName}SubTable.vue + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..8b08640 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,470 @@ +<#-- ** 引入全局工具方法 ** --> +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..a22734b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,83 @@ +import {defHttp} from '/@/utils/http/axios'; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/list', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackagePath}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackagePath}/${entityName?uncap_first}/query${sub.entityName}ByMainId', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +<#list subTables as sub><#rt/> +/** + * 子表单查询接口 + * @param params + */ +export const query${sub.entityName} = Api.${sub.entityName?uncap_first}List + +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const batchDelete = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} +<#list subTables as sub><#rt/> +/** + * 子表列表接口 + * @param params + */ +export const ${sub.entityName?uncap_first}List = (params) => + defHttp.get({url: Api.${sub.entityName?uncap_first}List, params},{isTransformResponse:false}); + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..2d744b3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,1060 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types' +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict' || po.classType=='link_table'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +<#-- 开始循环 --> +<#list columns as po> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isQuery=='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign query_flag=true> + <#assign query_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictField}"> + +<#if po.queryMode=='single'> + { + label: "${po.filedComment}", + field: ${autoStringSuffix(po)}, +<#if po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${query_field_dictCode}" + }, +<#elseif po.classType=='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, +<#elseif po.classType=='switch'> + component: 'JSwitch', + componentProps:{ + query:true, + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType=='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}", + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}", + + triggerChange: true + }, + <#elseif po.classType=='cat_tree'> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}",//back和事件未添加,暂时有问题 + }, +<#elseif po.classType=='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, +<#elseif po.classType=='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, +<#elseif po.classType=='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, +<#elseif po.classType=='popup'> + <#include "/common/form/vue3popup.ftl"> +<#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, +<#elseif po.classType=='list' || po.classType=='radio' || po.classType=='checkbox'> +<#-- ---------------------------下拉或是单选 判断数据字典是表字典还是普通字典------------------------------- --> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}" + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}" + + }, +<#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', +<#else> + component: 'Input', + + //colProps: {span: 6}, + }, +<#elseif po.queryMode=='like'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", + component: 'JInput', + }, +<#else> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='date'> + component: 'RangePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueType: 'Date', + }, +<#elseif po.classType=='datetime'> + component: 'RangePicker', + componentProps: { + valueType: 'Date', + showTime:true + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'JRangeNumber', +<#-- update-begin---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#elseif po.classType=='time'> + component: 'RangeTime', +<#-- update-end---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#else> + component: 'Input', //TODO 范围查询 + + //colProps: {span: 6}, + }, + + + +<#-- 结束循环 --> +]; +//表单数据 +export const formSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign id_exists = false> +<#list columns as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName == 'id'> + <#assign id_exists = true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, +<#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern:/^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true + + }, + + +<#if id_exists == false> + // TODO 主键隐藏字段,目前写死为ID + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; +//子表单数据 +<#list subTables as sub> +//子表列表数据 +export const ${sub.entityName?uncap_first}Columns: BasicColumn[] = [ + <#list sub.originalColumns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + return !text?"":(text.length>10?text.substr(0,10):text) + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict' || po.classType=='link_table'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +<#if sub.foreignRelationType =='1'> +export const ${sub.entityName?uncap_first}FormSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign sub_id_exists=false> +<#list sub.colums as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName=='id'> + <#assign sub_id_exists=true> + +<#if po.isShow =='Y'> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, +<#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true + + }, + + +<#if sub_id_exists == false> + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; + + +//子表表格配置 +<#list subTables as sub> +<#if sub.foreignRelationType =='0'> +export const ${sub.entityName?uncap_first}JVxeColumns: JVxeColumn[] = [ +<#assign popupBackFields = ""> + +<#-- 循环子表的列 开始 --> +<#list sub.colums as col><#rt/> +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.isShow =='Y' && col.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.filedComment !='外键' > + { + title: '${col.filedComment}', + key: '${autoStringSuffixForModel(col)}', +<#if col.classType =='date'> + type: JVxeTypes.date, + <#if col.extendParams?exists && col.extendParams.picker?exists> + picker: '${col.extendParams.picker}', + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='datetime'> + type: JVxeTypes.datetime, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='time'> + type: JVxeTypes.time, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='textarea'> + type: JVxeTypes.textarea, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list' || col.classType =='radio'> + type: JVxeTypes.select, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list_multi' || col.classType =='checkbox'> + type: JVxeTypes.selectMultiple, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_search'> + type: JVxeTypes.selectSearch, + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_depart'> + type: JVxeTypes.departSelect, + props:{ + <#if col.extendParams?exists && col.extendParams.text?exists> + labelKey: '${col.extendParams.text}', + + <#if col.extendParams?exists && col.extendParams.store?exists> + rowKey: '${col.extendParams.store}', + + }, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_user'> + type: JVxeTypes.userSelect, + props:{ + <#if col.extendParams?exists && col.extendParams.text?exists> + labelKey: '${col.extendParams.text}', + + <#if col.extendParams?exists && col.extendParams.store?exists> + rowKey: '${col.extendParams.store}', + + }, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='image'> + type: JVxeTypes.image, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='file'> + type: JVxeTypes.file, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='switch'> + type: JVxeTypes.checkbox, + <#if col.dictField == 'is_open'> + customValue: ['Y', 'N'], + <#else> + customValue: ${col.dictField}, + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType=='pca'> + type: JVxeTypes.pca, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='popup'> +<#if popupBackFields?length gt 0> + <#assign popupBackFields = "${popupBackFields}"+","+"${col.dictText}"> +<#else> + <#assign popupBackFields = "${col.dictText}"> + + <#include "/common/form/vue3Jvxepopup.ftl"> +<#-- update-begin-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> +<#-- elseif "int,decimal,double,"?contains(col.classType) --> +<#elseif col.fieldDbType=='int' || col.fieldDbType=='long' || col.fieldDbType=='double' || col.fieldDbType=='BigDecimal'> +<#-- update-end-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> + type: JVxeTypes.inputNumber, + <#if col.readonly=='Y'> + disabled:true, + +<#else> + type: JVxeTypes.input, + <#if col.readonly=='Y'> + disabled:true, + + +<#if col.classType =='list_multi' || col.classType =='checkbox'> + width:"250px", +<#else> + width:"200px", + +<#if col.classType =='file'> + placeholder: '请选择文件', +<#else> + placeholder: '请输入${'$'}{title}', + +<#if col.defaultVal??> +<#if col.fieldDbType=="BigDecimal" || col.fieldDbType=="double" || col.fieldDbType=="int"> + defaultValue:${col.defaultVal}, + <#else> + defaultValue:"${col.defaultVal}", + +<#else> + defaultValue:'', + +<#-- 子表的校验 --> + <#include "/common/validatorRulesTemplate/sub-vue3.ftl"> + }, + + + +<#-- 循环子表的列 结束 --> + ] + + + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + + //子表高级查询 + <#list subTables as sub> + ${sub.entityName?uncap_first}: { + title: '${sub.ftlDescription}', + view: 'table', + fields: { + <#list sub.colums as subCol> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if subCol.isShowList =='Y' && subCol.fieldName !='id' && subCol.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(subCol,subCol_index)}, + + + } + }, + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> + +/** +* 流程表单调用这个方法获取formSchema +* @param param +*/ +export function getBpmFormSchema(_formData): FormSchema[]{ + // 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema + return formSchema; +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei new file mode 100644 index 0000000..b5e8c33 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei @@ -0,0 +1,212 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei new file mode 100644 index 0000000..d25bb5a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei @@ -0,0 +1,278 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei new file mode 100644 index 0000000..a2c7a58 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei @@ -0,0 +1,89 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/subTables/[1-n]SubTable.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/subTables/[1-n]SubTable.vuei new file mode 100644 index 0000000..dc2ed4a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/inner-table/onetomany/java/${bussiPackage}/${entityPackage}/vue3/subTables/[1-n]SubTable.vuei @@ -0,0 +1,80 @@ +<#--noinspection JSDuplicatedDeclaration--> +<#list subTables as sub> +#segment#${sub.entityName}SubTable.vue + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..c466555 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,377 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.HashMap; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecg.common.system.vo.LoginUser; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.system.query.QueryRuleEnum; +import org.jeecg.common.util.oConvertUtils; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.vo.${entityName}Page; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.service.I${sub.entityName}Service; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.apache.shiro.authz.annotation.RequiresPermissions; + +<#assign bpm_flag=false> +<#assign has_multi_query_field=false> +<#list originalColumns as po> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + + + +<#assign has_multi_query_field=false> +<#list originalColumns as po> +<#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + <#assign has_multi_query_field=true> + + +<#assign enhanceJavaList=[]> +<#if tableVo.extendParams?? && tableVo.extendParams.enhanceJavaList??> + <#assign enhanceJavaList = tableVo.extendParams.enhanceJavaList?filter(enhance -> enhance??)> + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackagePath}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + <#list subTables as sub> + @Autowired + private I${sub.entityName}Service ${sub.entityName?uncap_first}Service; + + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 查询前触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeQuery() + + + + <#if has_multi_query_field> + // 自定义查询规则 + Map customeRuleMap = new HashMap<>(); + // 自定义多选的查询规则为:LIKE_WITH_OR + <#list originalColumns as po> + <#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + customeRuleMap.put("${po.fieldName}", QueryRuleEnum.LIKE_WITH_OR); + + + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap(),customeRuleMap); + <#else> + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 查询后触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterQuery() + + + + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @RequiresPermissions("${entityPackage}:${tableName}:add") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 新增前的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeAdd() + + + + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + <#if bpm_flag> + ${entityName?uncap_first}.setBpmStatus("1"); + + ${entityName?uncap_first}Service.saveMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 新增后的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterAdd() + + + + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequiresPermissions("${entityPackage}:${tableName}:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 编辑前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeEdit() + + + + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName} ${entityName?uncap_first}Entity = ${entityName?uncap_first}Service.getById(${entityName?uncap_first}.getId()); + if(${entityName?uncap_first}Entity==null) { + return Result.error("未找到对应数据"); + } + ${entityName?uncap_first}Service.updateMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 编辑后,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterEdit() + + + + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @RequiresPermissions("${entityPackage}:${tableName}:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delMain(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @RequiresPermissions("${entityPackage}:${tableName}:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.delBatchMain(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result<${entityName}> queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + if(${entityName?uncap_first}==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(${entityName?uncap_first}); + + } + + <#list subTables as sub> + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${sub.ftlDescription}通过主表ID查询") + @Operation(summary="${sub.ftlDescription}主表ID查询") + @GetMapping(value = "/query${sub.entityName}ByMainId") + public Result> query${sub.entityName}ListByMainId(@RequestParam(name="id",required=true) String id) { + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(id); + return Result.OK(${sub.entityName?uncap_first}List); + } + + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequiresPermissions("${entityPackage}:${tableName}:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='export' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导出前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeExport() + + + + + // Step.1 组装查询条件查询数据 + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, request.getParameterMap()); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + //配置选中数据查询条件 + String selections = request.getParameter("selections"); + if(oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + queryWrapper.in("id",selectionList); + } + //Step.2 获取导出数据 + List<${entityName}> ${entityName?uncap_first}List = ${entityName?uncap_first}Service.list(queryWrapper); + + // Step.3 组装pageList + List<${entityName}Page> pageList = new ArrayList<${entityName}Page>(); + for (${entityName} main : ${entityName?uncap_first}List) { + ${entityName}Page vo = new ${entityName}Page(); + BeanUtils.copyProperties(main, vo); + <#list subTables as sub> + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(main.getId()); + vo.set${sub.entityName}List(${sub.entityName?uncap_first}List); + + pageList.add(vo); + } + + // Step.4 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + mv.addObject(NormalExcelConstants.FILE_NAME, "${tableVo.ftlDescription}列表"); + mv.addObject(NormalExcelConstants.CLASS, ${entityName}Page.class); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("${tableVo.ftlDescription}数据", "导出人:"+sysUser.getRealname(), "${tableVo.ftlDescription}", ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("${entityPackage}:${tableName}:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='import' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导入前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeImport() + + + + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List<${entityName}Page> list = ExcelImportUtil.importExcel(file.getInputStream(), ${entityName}Page.class, params); + for (${entityName}Page page : list) { + ${entityName} po = new ${entityName}(); + BeanUtils.copyProperties(page, po); + ${entityName?uncap_first}Service.saveMain(po, <#list subTables as sub>page.get${sub.entityName}List()<#if sub_has_next>,); + } + return Result.OK("文件导入成功!数据行数:" + list.size()); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.OK("文件导入失败!"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..385ea9d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,82 @@ +<#include "/common/utils.ftl"> +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.aspect.annotation.Dict; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${tableVo.ftlDescription}") +@Data +@TableName("${tableName}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> +<#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + + <#include "/common/blob.ftl"> + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai new file mode 100644 index 0000000..f9ab598 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai @@ -0,0 +1,81 @@ +<#include "/common/utils.ftl"> +<#list subTables as subTab> +#segment#${subTab.entityName}.java +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import java.util.Date; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.UnsupportedEncodingException; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${subTab.ftlDescription}") +@Data +@TableName("${subTab.tableName}") +public class ${subTab.entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list subTab.originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#elseif !subTab.foreignKeys?seq_contains(po.fieldName?cap_first)> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + + <#-- 大字段转换 --> + <#include "/common/blob.ftl"> + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai new file mode 100644 index 0000000..10b2764 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai @@ -0,0 +1,34 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Mapper.java +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${subTab.entityName}Mapper extends BaseMapper<${subTab.entityName}> { + + /** + * 通过主表id删除子表数据 + * + * @param mainId 主表id + * @return boolean + */ + public boolean deleteByMainId(@Param("mainId") String mainId); + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(@Param("mainId") String mainId); +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml new file mode 100644 index 0000000..117c9b6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml @@ -0,0 +1,26 @@ +<#list subTables as subTab> +<#assign originalForeignKeys = subTab.originalForeignKeys> +#segment#${subTab.entityName}Mapper.xml + + + + + + DELETE + FROM ${subTab.tableName} + WHERE + <#list originalForeignKeys as key> + ${key} = ${r'#'}{mainId} <#rt/> + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..43ef9dc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,54 @@ +package ${bussiPackage}.${entityPackage}.service; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /** + * 添加一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void saveMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) ; + + /** + * 修改一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,); + + /** + * 删除一对多 + * + * @param id + */ + public void delMain (String id); + + /** + * 批量删除一对多 + * + * @param idList + */ + public void delBatchMain (Collection idList); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai new file mode 100644 index 0000000..cbc72ff --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai @@ -0,0 +1,25 @@ +<#list subTables as subTab> +#segment#I${subTab.entityName}Service.java +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${subTab.entityName}Service extends IService<${subTab.entityName}> { + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(String mainId); +} + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..7f99d42 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,105 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.mapper.${sub.entityName}Mapper; + +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.io.Serializable; +import java.util.List; +import java.util.Collection; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Autowired + private ${entityName}Mapper ${entityName?uncap_first}Mapper; + <#list subTables as sub> + @Autowired + private ${sub.entityName}Mapper ${sub.entityName?uncap_first}Mapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMain(${entityName} ${entityName?uncap_first}, <#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.insert(${entityName?uncap_first}); + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.updateById(${entityName?uncap_first}); + + //1.先删除子表数据 + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(${entityName?uncap_first}.getId()); + + + //2.子表数据重新插入 + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delMain(String id) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatchMain(Collection idList) { + for(Serializable id:idList) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id.toString()); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai new file mode 100644 index 0000000..0ce41d3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai @@ -0,0 +1,30 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}ServiceImpl.java +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${subTab.entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${subTab.entityName}Service; +import org.springframework.stereotype.Service; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${subTab.entityName}ServiceImpl extends ServiceImpl<${subTab.entityName}Mapper, ${subTab.entityName}> implements I${subTab.entityName}Service { + + @Autowired + private ${subTab.entityName}Mapper ${subTab.entityName?uncap_first}Mapper; + + @Override + public List<${subTab.entityName}> selectByMainId(String mainId) { + return ${subTab.entityName?uncap_first}Mapper.selectByMainId(mainId); + } +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai new file mode 100644 index 0000000..c34e18b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai @@ -0,0 +1,117 @@ +package ${bussiPackage}.${entityPackage}.vo; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecgframework.poi.excel.annotation.ExcelEntity; +import org.jeecgframework.poi.excel.annotation.ExcelCollection; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import java.util.Date; +import org.jeecg.common.aspect.annotation.Dict; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName}Page { + + <#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "realname", dicCode = "username"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "depart_name", dicCode = "id"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + <#elseif po.classType=='cat_tree'> + <#assign list_field_dictCode=', dictTable = "sys_category", dicText = "name", dicCode = "id"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1 && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Dict(${list_field_dictCode?substring(2)}) + + + @Schema(description = "${po.filedComment}") + <#if po.fieldDbType=='Blob'> + private java.lang.String ${po.fieldName}String; + <#elseif po.classType=='pca'> + @Excel(name = "${po.filedComment}", width = 15,exportConvert=true,importConvert = true ) + private ${po.fieldType} ${po.fieldName}; + + public String convertis${po.fieldName?cap_first}() { + return SpringContextUtils.getBean(ProvinceCityArea.class).getText(${po.fieldName}); + } + + public void convertset${po.fieldName?cap_first}(String text) { + this.${po.fieldName} = SpringContextUtils.getBean(ProvinceCityArea.class).getCode(text); + } + <#elseif po.classType=='cat_tree'> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + private ${po.fieldType} ${po.fieldName}; + <#elseif po.classType=='switch'> + <#assign switch_extend_arr=['Y','N']> + <#if po.dictField?default("")?contains("[")> + <#assign switch_extend_arr=po.dictField?eval> + + <#list switch_extend_arr as a> + <#if a_index == 0> + <#assign switch_extend_arr1=a> + <#else> + <#assign switch_extend_arr2=a> + + + @Excel(name = "${po.filedComment}", width = 15,replace = {"是_${switch_extend_arr1}","否_${switch_extend_arr2}"} ) + private ${po.fieldType} ${po.fieldName}; + <#else> + private ${po.fieldType} ${po.fieldName}; + + + + <#list subTables as sub> + @ExcelCollection(name="${sub.ftlDescription}") + @Schema(description = "${sub.ftlDescription}") + private List<${sub.entityName}> ${sub.entityName?uncap_first}List; + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..1df3dee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,397 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei new file mode 100644 index 0000000..e54b9a8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei @@ -0,0 +1,568 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..8021bb8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,65 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei new file mode 100644 index 0000000..5047287 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei @@ -0,0 +1,196 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..862b931 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,445 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..a3c1bb7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,75 @@ +import {defHttp} from '/@/utils/http/axios'; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/list', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackagePath}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackagePath}/${entityName?uncap_first}/query${sub.entityName}ByMainId', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +<#list subTables as sub><#rt/> +/** + * 查询子表数据 + * @param params + */ +export const ${sub.entityName?uncap_first}List = Api.${sub.entityName?uncap_first}List; + +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const batchDelete = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdate = (params, isUpdate,showTip = true) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params},{ successMessageMode :showTip?'success':'none'}); +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..b3245ad --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,988 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types' +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +<#-- 开始循环 --> +<#list columns as po> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isQuery=='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign query_flag=true> + <#assign query_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictField}"> + +<#if po.queryMode=='single'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${query_field_dictCode}" + }, +<#elseif po.classType=='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, +<#elseif po.classType=='switch'> + component: 'JSwitch', + componentProps:{ + query:true, + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType=='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}", + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}", + + triggerChange: true + }, + <#elseif po.classType=='cat_tree'> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}",//back和事件未添加,暂时有问题 + }, +<#elseif po.classType=='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, +<#elseif po.classType=='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, +<#elseif po.classType=='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, +<#elseif po.classType=='popup'> + <#include "/common/form/vue3popup.ftl"> +<#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, +<#elseif po.classType=='list' || po.classType=='radio' || po.classType=='checkbox'> +<#-- ---------------------------下拉或是单选 判断数据字典是表字典还是普通字典------------------------------- --> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}" + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}" + + }, +<#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', +<#else> + component: 'Input', + + //colProps: {span: 6}, + }, +<#elseif po.queryMode=='like'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", + component: 'JInput', + }, +<#else> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='date'> + component: 'RangePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueType: 'Date', + }, +<#elseif po.classType=='datetime'> + component: 'RangePicker', + componentProps: { + valueType: 'Date', + showTime:true + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'JRangeNumber', +<#-- update-begin---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#elseif po.classType=='time'> + component: 'RangeTime', +<#-- update-end---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#else> + component: 'Input', //TODO 范围查询 + + //colProps: {span: 6}, + }, + + + +<#-- 结束循环 --> +]; +//表单数据 +export const formSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign id_exists = false> +<#list columns as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName == 'id'> + <#assign id_exists = true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件------------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件------------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, + <#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true + + }, + + +<#if id_exists == false> + // TODO 主键隐藏字段,目前写死为ID + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; +//子表单数据 +<#list subTables as sub> +<#if sub.foreignRelationType =='1'> +export const ${sub.entityName?uncap_first}FormSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign sub_id_exists=false> +<#list sub.colums as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName=='id'> + <#assign sub_id_exists=true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件------------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件------------- --> + componentProps:{ + labelKey:'realname', + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, + <#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true + + }, + + +<#if sub_id_exists == false> + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; + + +//子表表格配置 +<#list subTables as sub> +<#if sub.foreignRelationType =='0'> +export const ${sub.entityName?uncap_first}Columns: JVxeColumn[] = [ +<#assign popupBackFields = ""> + +<#-- 循环子表的列 开始 --> +<#list sub.colums as col><#rt/> +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.isShow =='Y' && col.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.filedComment !='外键' > + { + title: '${col.filedComment}', + key: '${autoStringSuffixForModel(col)}', +<#if col.classType =='date'> + type: JVxeTypes.date, + <#if col.extendParams?exists && col.extendParams.picker?exists> + picker: '${col.extendParams.picker}', + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='datetime'> + type: JVxeTypes.datetime, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='time'> + type: JVxeTypes.time, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='textarea'> + type: JVxeTypes.textarea, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list' || col.classType =='radio'> + type: JVxeTypes.select, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list_multi' || col.classType =='checkbox'> + type: JVxeTypes.selectMultiple, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_search'> + type: JVxeTypes.selectSearch, + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_depart'> + type: JVxeTypes.departSelect, + props:{ + <#if col.extendParams?exists && col.extendParams.text?exists> + labelKey: '${col.extendParams.text}', + + <#if col.extendParams?exists && col.extendParams.store?exists> + rowKey: '${col.extendParams.store}', + + }, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_user'> + type: JVxeTypes.userSelect, + props:{ + <#if col.extendParams?exists && col.extendParams.text?exists> + labelKey: '${col.extendParams.text}', + + <#if col.extendParams?exists && col.extendParams.store?exists> + rowKey: '${col.extendParams.store}', + + }, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='image'> + type: JVxeTypes.image, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='file'> + type: JVxeTypes.file, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='switch'> + type: JVxeTypes.checkbox, + <#if col.dictField == 'is_open'> + customValue: ['Y', 'N'], + <#else> + customValue: ${col.dictField}, + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType=='pca'> + type: JVxeTypes.pca, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='popup'> +<#if popupBackFields?length gt 0> + <#assign popupBackFields = "${popupBackFields}"+","+"${col.dictText}"> +<#else> + <#assign popupBackFields = "${col.dictText}"> + + <#include "/common/form/vue3Jvxepopup.ftl"> +<#-- update-begin-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> +<#-- elseif "int,decimal,double,"?contains(col.classType) --> +<#elseif col.fieldDbType=='int' || col.fieldDbType=='long' || col.fieldDbType=='double' || col.fieldDbType=='BigDecimal'> +<#-- update-end-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> + type: JVxeTypes.inputNumber, + <#if col.readonly=='Y'> + disabled:true, + +<#else> + type: JVxeTypes.input, + <#if col.readonly=='Y'> + disabled:true, + + +<#if col.classType =='list_multi' || col.classType =='checkbox'> + width:"250px", +<#else> + width:"200px", + +<#if col.classType =='file'> + placeholder: '请选择文件', +<#else> + placeholder: '请输入${'$'}{title}', + +<#if col.defaultVal??> +<#if col.fieldDbType=="BigDecimal" || col.fieldDbType=="double" || col.fieldDbType=="int"> + defaultValue:${col.defaultVal}, + <#else> + defaultValue:"${col.defaultVal}", + +<#else> + defaultValue:'', + +<#-- 子表的校验 --> + <#include "/common/validatorRulesTemplate/sub-vue3.ftl"> + }, + + + +<#-- 循环子表的列 结束 --> + ] + + + + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + + //子表高级查询 + <#list subTables as sub> + ${sub.entityName?uncap_first}: { + title: '${sub.ftlDescription}', + view: 'table', + fields: { + <#list sub.colums as subCol> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if subCol.isShowList =='Y' && subCol.fieldName !='id' && subCol.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(subCol,subCol_index)}, + + + } + }, + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> + +/** +* 流程表单调用这个方法获取formSchema +* @param param +*/ +export function getBpmFormSchema(_formData): FormSchema[]{ +// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema + return formSchema; +} \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..7292339 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei new file mode 100644 index 0000000..eff0ab3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei @@ -0,0 +1,209 @@ +<#include "/common/utils.ftl"> + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei new file mode 100644 index 0000000..a3698d0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei @@ -0,0 +1,278 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei new file mode 100644 index 0000000..9bf73fa --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei @@ -0,0 +1,87 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei new file mode 100644 index 0000000..95ff8e5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}List.vuei @@ -0,0 +1,582 @@ +<#include "/common/utils.ftl"> + + +<#if query_flag> + + + + 查询 + 重置 + + {{ toggleSearchStatus ? '收起' : '展开' }} + + + + + + + + + +<#-- 结束循环 --> + + + + + + + + + + + <${entityName}Modal @register="registerModal" @success="handleSuccess"> + <#if bpm_flag==true> + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi new file mode 100644 index 0000000..6762659 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__api.tsi @@ -0,0 +1,85 @@ +import {defHttp} from '/@/utils/http/axios'; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/list', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackagePath}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', + queryDataById = '/${entityPackagePath}/${entityName?uncap_first}/queryById', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackagePath}/${entityName?uncap_first}/query${sub.entityName}ByMainId', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; + +<#list subTables as sub><#rt/> +/** + * 查询子表数据 + * @param params + */ +export const query${sub.entityName}ListByMainId = (id) => defHttp.get({url: Api.${sub.entityName?uncap_first}List, params:{ id }}); + + +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const batchDelete = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} + +/** +* 根据id查询数据 +* @param params +*/ +export const queryDataById = (id) => defHttp.get({url: Api.queryDataById, params:{ id }}); + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi new file mode 100644 index 0000000..89ec48c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/${entityName}__data.tsi @@ -0,0 +1,273 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types' +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; + +//子表表格配置 +<#list subTables as sub> +<#if sub.foreignRelationType =='0'> +export const ${sub.entityName?uncap_first}Columns: JVxeColumn[] = [ +<#assign popupBackFields = ""> + +<#-- 循环子表的列 开始 --> +<#list sub.colums as col><#rt/> +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.isShow =='Y' && col.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.filedComment !='外键' > + { + title: '${col.filedComment}', + key: '${autoStringSuffixForModel(col)}', +<#if col.classType =='date'> + type: JVxeTypes.date, + <#if col.extendParams?exists && col.extendParams.picker?exists> + picker: '${col.extendParams.picker}', + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='datetime'> + type: JVxeTypes.datetime, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='time'> + type: JVxeTypes.time, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='textarea'> + type: JVxeTypes.textarea, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list' || col.classType =='radio'> + type: JVxeTypes.select, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list_multi' || col.classType =='checkbox'> + type: JVxeTypes.selectMultiple, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_search'> + type: JVxeTypes.selectSearch, + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_depart'> + type: JVxeTypes.departSelect, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_user'> + type: JVxeTypes.userSelect, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='image'> + type: JVxeTypes.image, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='file'> + type: JVxeTypes.file, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='switch'> + type: JVxeTypes.checkbox, + <#if col.dictField == 'is_open'> + customValue: ['Y', 'N'], + <#else> + customValue: ${col.dictField}, + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType=='pca'> + type: JVxeTypes.pca, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='popup'> +<#if popupBackFields?length gt 0> + <#assign popupBackFields = "${popupBackFields}"+","+"${col.dictText}"> +<#else> + <#assign popupBackFields = "${col.dictText}"> + + <#include "/common/form/vue3Jvxepopup.ftl"> +<#-- update-begin-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> +<#-- elseif "int,decimal,double,"?contains(col.classType) --> +<#elseif col.fieldDbType=='int' || col.fieldDbType=='long' || col.fieldDbType=='double' || col.fieldDbType=='BigDecimal'> +<#-- update-end-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> + type: JVxeTypes.inputNumber, + <#if col.readonly=='Y'> + disabled:true, + +<#else> + type: JVxeTypes.input, + <#if col.readonly=='Y'> + disabled:true, + + +<#if col.classType =='list_multi' || col.classType =='checkbox'> + width:"250px", +<#else> + width:"200px", + +<#if col.classType =='file'> + placeholder: '请选择文件', +<#else> + placeholder: '请输入${'$'}{title}', + +<#if col.defaultVal??> +<#if col.fieldDbType=="BigDecimal" || col.fieldDbType=="double" || col.fieldDbType=="int"> + defaultValue:${col.defaultVal}, + <#else> + defaultValue:"${col.defaultVal}", + +<#else> + defaultValue:'', + +<#-- 子表的校验 --> + <#include "/common/validatorRulesTemplate/sub-vue3.ftl"> + }, + + + +<#-- 循环子表的列 结束 --> + ] + + + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + + //子表高级查询 + <#list subTables as sub> + ${sub.entityName?uncap_first}: { + title: '${sub.ftlDescription}', + view: 'table', + fields: { + <#list sub.colums as subCol> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if subCol.isShowList =='Y' && subCol.fieldName !='id' && subCol.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(subCol,subCol_index)}, + + + } + }, + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei new file mode 100644 index 0000000..24cbc01 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Form.vuei @@ -0,0 +1,412 @@ +<#include "/common/utils.ftl"> +<#assign need_category = false> +<#assign bpm_flag=false> +<#assign need_pca = false> +<#assign need_search = false> +<#assign need_dept_user = false> +<#assign need_switch = false> +<#assign need_dept = false> +<#assign need_multi = false> +<#assign need_popup = false> +<#assign need_popup_dict = false> +<#assign need_select_tag = false> +<#assign need_select_tree = false> +<#assign need_time = false> +<#assign need_markdown = false> +<#assign need_upload = false> +<#assign need_image_upload = false> +<#assign need_editor = false> +<#assign need_checkbox = false> +<#assign need_range_number = false> +<#assign is_like = false> +<#assign form_span = 24> +<#if tableVo.fieldRowNum==2> + <#assign form_span = 12> +<#elseif tableVo.fieldRowNum==3> + <#assign form_span = 8> +<#elseif tableVo.fieldRowNum==4> + <#assign form_span = 6> + +<#assign hasOne2manyTable = false> +<#assign subTabActiveKey = ''> +<#assign subMainFieldMap={}> +<#assign subTableColumnsKey=[]> +<#assign hasOnlyValidate = false> + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei new file mode 100644 index 0000000..1a3a8dd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/${entityName}Modal.vuei @@ -0,0 +1,97 @@ +<#include "/common/utils.ftl"> + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Form.vuei new file mode 100644 index 0000000..4a2e369 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/jvxe/onetomany/java/${bussiPackage}/${entityPackage}/vue3Native/components/[1-n]Form.vuei @@ -0,0 +1,185 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue +<#include "/common/utils.ftl"> +<#assign need_category = false> +<#assign bpm_flag=false> +<#assign need_pca = false> +<#assign need_search = false> +<#assign need_dept_user = false> +<#assign need_switch = false> +<#assign need_dept = false> +<#assign need_multi = false> +<#assign need_popup = false> +<#assign need_popup_dict = false> +<#assign need_select_tag = false> +<#assign need_select_tree = false> +<#assign need_time = false> +<#assign need_markdown = false> +<#assign need_upload = false> +<#assign need_image_upload = false> +<#assign need_editor = false> +<#assign need_checkbox = false> +<#assign need_range_number = false> +<#assign is_like = false> +<#assign form_span = 24> +<#if tableVo.fieldRowNum==2> + <#assign form_span = 12> +<#elseif tableVo.fieldRowNum==3> + <#assign form_span = 8> +<#elseif tableVo.fieldRowNum==4> + <#assign form_span = 6> + + <#assign hasOnlyValidate = false> + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..3c513ee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,365 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.HashMap; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecg.common.system.vo.LoginUser; +import org.apache.shiro.SecurityUtils; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.system.query.QueryRuleEnum; +import org.jeecg.common.util.oConvertUtils; +import org.jeecgframework.poi.excel.entity.enmus.ExcelType; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.vo.${entityName}Page; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.service.I${sub.entityName}Service; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.apache.shiro.authz.annotation.RequiresPermissions; +<#assign has_multi_query_field=false> +<#list originalColumns as po> +<#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + <#assign has_multi_query_field=true> + + +<#assign enhanceJavaList=[]> +<#if tableVo.extendParams?? && tableVo.extendParams.enhanceJavaList??> + <#assign enhanceJavaList = tableVo.extendParams.enhanceJavaList?filter(enhance -> enhance??)> + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackagePath}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + <#list subTables as sub> + @Autowired + private I${sub.entityName}Service ${sub.entityName?uncap_first}Service; + + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 查询前触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeQuery() + + + + <#if has_multi_query_field> + // 自定义查询规则 + Map customeRuleMap = new HashMap<>(); + // 自定义多选的查询规则为:LIKE_WITH_OR + <#list originalColumns as po> + <#if po.isQuery=='Y' && (po.classType=='list' || po.classType=='list_multi' || po.classType=='radio' || po.classType=='checkbox')> + customeRuleMap.put("${po.fieldName}", QueryRuleEnum.LIKE_WITH_OR); + + + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap(),customeRuleMap); + <#else> + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='query' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 查询后触发的方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterQuery() + + + + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @RequiresPermissions("${entityPackage}:${tableName}:add") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 新增前的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeAdd() + + + + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName?uncap_first}Service.saveMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='add' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 新增后的处理方法,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterAdd() + + + + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequiresPermissions("${entityPackage}:${tableName}:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 编辑前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeEdit() + + + + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName} ${entityName?uncap_first}Entity = ${entityName?uncap_first}Service.getById(${entityName?uncap_first}.getId()); + if(${entityName?uncap_first}Entity==null) { + return Result.error("未找到对应数据"); + } + ${entityName?uncap_first}Service.updateMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='edit' && enhanceJava.event=='end' && enhanceJava.activeStatus=='1'> + //TODO 编辑后,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.afterEdit() + + + + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @RequiresPermissions("${entityPackage}:${tableName}:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delMain(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @RequiresPermissions("${entityPackage}:${tableName}:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.delBatchMain(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result<${entityName}> queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + if(${entityName?uncap_first}==null) { + return Result.error("未找到对应数据"); + } + return Result.OK(${entityName?uncap_first}); + + } + + <#list subTables as sub> + /** + * 通过id查询 + * + * @param id + * @return + */ + //@AutoLog(value = "${sub.ftlDescription}通过主表ID查询") + @Operation(summary="${sub.ftlDescription}主表ID查询") + @GetMapping(value = "/query${sub.entityName}ByMainId") + public Result> query${sub.entityName}ListByMainId(@RequestParam(name="id",required=true) String id) { + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(id); + return Result.OK(${sub.entityName?uncap_first}List); + } + + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequiresPermissions("${entityPackage}:${tableName}:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='export' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导出前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeExport() + + + + + // Step.1 组装查询条件查询数据 + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, request.getParameterMap()); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + //配置选中数据查询条件 + String selections = request.getParameter("selections"); + if(oConvertUtils.isNotEmpty(selections)) { + List selectionList = Arrays.asList(selections.split(",")); + queryWrapper.in("id",selectionList); + } + //Step.2 获取导出数据 + List<${entityName}> ${entityName?uncap_first}List = ${entityName?uncap_first}Service.list(queryWrapper); + + // Step.3 组装pageList + List<${entityName}Page> pageList = new ArrayList<${entityName}Page>(); + for (${entityName} main : ${entityName?uncap_first}List) { + ${entityName}Page vo = new ${entityName}Page(); + BeanUtils.copyProperties(main, vo); + <#list subTables as sub> + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(main.getId()); + vo.set${sub.entityName}List(${sub.entityName?uncap_first}List); + + pageList.add(vo); + } + + // Step.4 AutoPoi 导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + mv.addObject(NormalExcelConstants.FILE_NAME, "${tableVo.ftlDescription}列表"); + mv.addObject(NormalExcelConstants.CLASS, ${entityName}Page.class); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("${tableVo.ftlDescription}数据", "导出人:"+sysUser.getRealname(), "${tableVo.ftlDescription}", ExcelType.XSSF)); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequiresPermissions("${entityPackage}:${tableName}:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + <#if enhanceJavaList?size gt 0> + <#list enhanceJavaList as enhanceJava> + <#if enhanceJava.buttonCode=='import' && enhanceJava.event=='start' && enhanceJava.activeStatus=='1'> + //TODO 导入前,代码生成后,请手工实现增强类逻辑; + //${entityName?uncap_first}Service.beforeImport() + + + + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + // 获取上传文件对象 + MultipartFile file = entity.getValue(); + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List<${entityName}Page> list = ExcelImportUtil.importExcel(file.getInputStream(), ${entityName}Page.class, params); + for (${entityName}Page page : list) { + ${entityName} po = new ${entityName}(); + BeanUtils.copyProperties(page, po); + ${entityName?uncap_first}Service.saveMain(po, <#list subTables as sub>page.get${sub.entityName}List()<#if sub_has_next>,); + } + return Result.OK("文件导入成功!数据行数:" + list.size()); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.OK("文件导入失败!"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..b45dbe9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,82 @@ +<#include "/common/utils.ftl"> +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecg.common.aspect.annotation.Dict; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${tableVo.ftlDescription}") +@Data +@TableName("${tableName}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1> + @Dict(${list_field_dictCode?substring(2)}) + + + <#include "/common/blob.ftl"> + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai new file mode 100644 index 0000000..f85ed30 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai @@ -0,0 +1,82 @@ +<#include "/common/utils.ftl"> +<#list subTables as subTab> +#segment#${subTab.entityName}.java +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import java.util.Date; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.UnsupportedEncodingException; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Schema(description="${subTab.ftlDescription}") +@Data +@TableName("${subTab.tableName}") +public class ${subTab.entityName} implements Serializable { + private static final long serialVersionUID = 1L; + +<#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list subTab.originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "${camelToDashed(po.extendParams.text?default(\"realname\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"username\")?trim)}"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "${camelToDashed(po.extendParams.text?default(\"depart_name\")?trim)}", dicCode = "${camelToDashed(po.extendParams.store?default(\"id\")?trim)}"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + <#elseif po.classType=='sel_tree'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText?split(",")[2]}", dicCode = "${po.dictText?split(",")[0]}"'> + + <#elseif po.classType=='link_table'> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicCode = "${po.dictField}", dicText = "${po.dictText?split(",")[0]}"'> + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#elseif !subTab.foreignKeys?seq_contains(po.fieldName?cap_first)> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + + <#-- 大字段转换 --> + <#include "/common/blob.ftl"> + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai new file mode 100644 index 0000000..10b2764 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai @@ -0,0 +1,34 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Mapper.java +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${subTab.entityName}Mapper extends BaseMapper<${subTab.entityName}> { + + /** + * 通过主表id删除子表数据 + * + * @param mainId 主表id + * @return boolean + */ + public boolean deleteByMainId(@Param("mainId") String mainId); + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(@Param("mainId") String mainId); +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml new file mode 100644 index 0000000..117c9b6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml @@ -0,0 +1,26 @@ +<#list subTables as subTab> +<#assign originalForeignKeys = subTab.originalForeignKeys> +#segment#${subTab.entityName}Mapper.xml + + + + + + DELETE + FROM ${subTab.tableName} + WHERE + <#list originalForeignKeys as key> + ${key} = ${r'#'}{mainId} <#rt/> + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..d80c029 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,54 @@ +package ${bussiPackage}.${entityPackage}.service; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /** + * 添加一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void saveMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) ; + + /** + * 修改一对多 + * + * @param ${entityName?uncap_first} + <#list subTables as sub> + * @param ${sub.entityName?uncap_first}List + + */ + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,); + + /** + * 删除一对多 + * + * @param id + */ + public void delMain (String id); + + /** + * 批量删除一对多 + * + * @param idList + */ + public void delBatchMain (Collection idList); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai new file mode 100644 index 0000000..cbc72ff --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai @@ -0,0 +1,25 @@ +<#list subTables as subTab> +#segment#I${subTab.entityName}Service.java +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${subTab.entityName}Service extends IService<${subTab.entityName}> { + + /** + * 通过主表id查询子表数据 + * + * @param mainId 主表id + * @return List<${subTab.entityName}> + */ + public List<${subTab.entityName}> selectByMainId(String mainId); +} + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..7f99d42 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,105 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.mapper.${sub.entityName}Mapper; + +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.io.Serializable; +import java.util.List; +import java.util.Collection; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Autowired + private ${entityName}Mapper ${entityName?uncap_first}Mapper; + <#list subTables as sub> + @Autowired + private ${sub.entityName}Mapper ${sub.entityName?uncap_first}Mapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMain(${entityName} ${entityName?uncap_first}, <#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.insert(${entityName?uncap_first}); + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.updateById(${entityName?uncap_first}); + + //1.先删除子表数据 + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(${entityName?uncap_first}.getId()); + + + //2.子表数据重新插入 + <#list subTables as sub> + if(${sub.entityName?uncap_first}List!=null && ${sub.entityName?uncap_first}List.size()>0) { + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delMain(String id) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatchMain(Collection idList) { + for(Serializable id:idList) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id.toString()); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai new file mode 100644 index 0000000..0ce41d3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai @@ -0,0 +1,30 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}ServiceImpl.java +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${subTab.entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${subTab.entityName}Service; +import org.springframework.stereotype.Service; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${subTab.entityName}ServiceImpl extends ServiceImpl<${subTab.entityName}Mapper, ${subTab.entityName}> implements I${subTab.entityName}Service { + + @Autowired + private ${subTab.entityName}Mapper ${subTab.entityName?uncap_first}Mapper; + + @Override + public List<${subTab.entityName}> selectByMainId(String mainId) { + return ${subTab.entityName?uncap_first}Mapper.selectByMainId(mainId); + } +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai new file mode 100644 index 0000000..c8550f8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai @@ -0,0 +1,117 @@ +package ${bussiPackage}.${entityPackage}.vo; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecgframework.poi.excel.annotation.ExcelEntity; +import org.jeecgframework.poi.excel.annotation.ExcelCollection; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import java.util.Date; +import org.jeecg.common.aspect.annotation.Dict; +import org.jeecg.common.constant.ProvinceCityArea; +import org.jeecg.common.util.SpringContextUtils; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName}Page { + + <#assign excel_ignore_arr=['createBy','createTime','updateBy','updateTime','sysOrgCode']> + <#assign excel_ignore_classType_arr=['pca','switch','cat_tree']> + <#list originalColumns as po> + <#-- 生成字典Code --> + <#assign list_field_dictCode=""> + <#if po.classType='sel_user'> + <#assign list_field_dictCode=', dictTable = "sys_user", dicText = "realname", dicCode = "username"'> + <#elseif po.classType='sel_depart'> + <#assign list_field_dictCode=', dictTable = "sys_depart", dicText = "depart_name", dicCode = "id"'> + <#elseif po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox'> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dictTable = "${po.dictTable}", dicText = "${po.dictText}", dicCode = "${po.dictField}"'> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign list_field_dictCode=', dicCode = "${po.dictField}"'> + <#elseif po.classType=='cat_tree'> + <#assign list_field_dictCode=', dictTable = "sys_category", dicText = "name", dicCode = "id"'> + + + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + <#else> + <#if po.fieldDbType =='Date' || po.fieldDbType =='Datetime'> + <#if po.classType=='date'> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}")> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !excel_ignore_arr?seq_contains("${po.fieldName}") && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + + + <#if list_field_dictCode?length gt 1 && !excel_ignore_classType_arr?seq_contains("${po.classType}")> + @Dict(${list_field_dictCode?substring(2)}) + + + @Schema(description = "${po.filedComment}") + <#if po.fieldDbType=='Blob'> + private java.lang.String ${po.fieldName}String; + <#elseif po.classType=='pca'> + @Excel(name = "${po.filedComment}", width = 15,exportConvert=true,importConvert = true ) + private ${po.fieldType} ${po.fieldName}; + + public String convertis${po.fieldName?cap_first}() { + return SpringContextUtils.getBean(ProvinceCityArea.class).getText(${po.fieldName}); + } + + public void convertset${po.fieldName?cap_first}(String text) { + this.${po.fieldName} = SpringContextUtils.getBean(ProvinceCityArea.class).getCode(text); + } + <#elseif po.classType=='cat_tree'> + @Excel(name = "${po.filedComment}", width = 15${list_field_dictCode}) + private ${po.fieldType} ${po.fieldName}; + <#elseif po.classType=='switch'> + <#assign switch_extend_arr=['Y','N']> + <#if po.dictField?default("")?contains("[")> + <#assign switch_extend_arr=po.dictField?eval> + + <#list switch_extend_arr as a> + <#if a_index == 0> + <#assign switch_extend_arr1=a> + <#else> + <#assign switch_extend_arr2=a> + + + @Excel(name = "${po.filedComment}", width = 15,replace = {"是_${switch_extend_arr1}","否_${switch_extend_arr2}"} ) + private ${po.fieldType} ${po.fieldName}; + <#else> + private ${po.fieldType} ${po.fieldName}; + + + + <#list subTables as sub> + @ExcelCollection(name="${sub.ftlDescription}") + @Schema(description = "${sub.ftlDescription}") + private List<${sub.entityName}> ${sub.entityName?uncap_first}List; + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..e440c05 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,351 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei new file mode 100644 index 0000000..1e84cd7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei @@ -0,0 +1,509 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..c666aa4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,62 @@ +<#include "/common/utils.ftl"> + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei new file mode 100644 index 0000000..7b69d6d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Form.vuei @@ -0,0 +1,199 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..14c7d9f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,444 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..ffe92f5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,75 @@ +import {defHttp} from '/@/utils/http/axios'; +import { useMessage } from "/@/hooks/web/useMessage"; + +const { createConfirm } = useMessage(); + +enum Api { + list = '/${entityPackagePath}/${entityName?uncap_first}/list', + save='/${entityPackagePath}/${entityName?uncap_first}/add', + edit='/${entityPackagePath}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackagePath}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackagePath}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackagePath}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackagePath}/${entityName?uncap_first}/exportXls', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackagePath}/${entityName?uncap_first}/query${sub.entityName}ByMainId', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +<#list subTables as sub><#rt/> +/** + * 查询子表数据 + * @param params + */ +export const ${sub.entityName?uncap_first}List = Api.${sub.entityName?uncap_first}List; + +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const batchDelete = (params, handleSuccess) => { + createConfirm({ + iconType: 'warning', + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..e6a50f9 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,991 @@ +<#include "/common/utils.ftl"> +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types' +import { getWeekMonthQuarterYear } from '/@/utils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + { + title: '${po.filedComment}', + align:"center", + <#if po.sort=='Y'> + sorter: true, + + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text); + <#if po.extendParams?exists && po.extendParams.picker?exists> + if(text) { + return getWeekMonthQuarterYear(text)['${po.extendParams.picker}']; + } else { + return text; + } + <#else> + return text; + + }, + <#elseif po.fieldDbType=='Blob'> + dataIndex: '${po.fieldName}String' + <#elseif po.classType=='umeditor'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='pca'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='file'> + dataIndex: '${po.fieldName}', + <#elseif po.classType=='image'> + dataIndex: '${po.fieldName}', + customRender:render.renderImage, + <#elseif po.classType=='switch'> + dataIndex: '${po.fieldName}', +<#assign switch_extend_arr=['Y','N']> +<#if po.dictField?default("")?contains("[")> +<#assign switch_extend_arr=po.dictField?eval> + +<#list switch_extend_arr as a> +<#if a_index == 0> +<#assign switch_extend_arr1=a> +<#else> +<#assign switch_extend_arr2=a> + + + customRender:({text}) => { + return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}]) + }, + <#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict' || po.classType=='link_table'> + dataIndex: '${po.fieldName}_dictText' + <#elseif po.classType=='cat_tree'> + dataIndex: '${po.fieldName}', + <#if po.dictText?default("")?trim?length == 0> + customRender:({text}) => { + return render.renderCategoryTree(text,'${po.dictField?default("")}') + }, + <#else> + customRender: ({text, record}) => (text ? record['${po.dictText}'] : '') + + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +<#-- 开始循环 --> +<#list columns as po> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isQuery=='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign query_flag=true> + <#assign query_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign query_field_dictCode="${po.dictField}"> + +<#if po.queryMode=='single'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${query_field_dictCode}" + }, +<#elseif po.classType=='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, +<#elseif po.classType=='switch'> + component: 'JSwitch', + componentProps:{ + query:true, + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType=='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}", + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}", + + triggerChange: true + }, + <#elseif po.classType=='cat_tree'> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}",//back和事件未添加,暂时有问题 + }, +<#elseif po.classType=='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, +<#elseif po.classType=='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, +<#elseif po.classType=='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, +<#elseif po.classType=='popup'> + <#include "/common/form/vue3popup.ftl"> +<#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, +<#elseif po.classType=='list' || po.classType=='radio' || po.classType=='checkbox'> +<#-- ---------------------------下拉或是单选 判断数据字典是表字典还是普通字典------------------------------- --> + component: 'JSelectMultiple', + componentProps:{ + <#if po.dictTable?default("")?trim?length gt 1> + dictCode:"${po.dictTable},${po.dictText},${po.dictField}" + <#elseif po.dictField?default("")?trim?length gt 1> + dictCode:"${po.dictField}" + + }, +<#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', +<#else> + component: 'Input', + + //colProps: {span: 6}, + }, +<#elseif po.queryMode=='like'> + { + label: "${po.filedComment}", + field: "${po.fieldName}", + component: 'JInput', + }, +<#else> + { + label: "${po.filedComment}", + field: "${po.fieldName}", +<#if po.classType=='date'> + component: 'RangePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueType: 'Date', + }, +<#elseif po.classType=='datetime'> + component: 'RangePicker', + componentProps: { + valueType: 'Date', + showTime:true + }, +<#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'JRangeNumber', +<#-- update-begin---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#elseif po.classType=='time'> + component: 'RangeTime', +<#-- update-end---author:chenrui ---date:20240527 for:[TV360X-388]时间范围查询控件---------- --> +<#else> + component: 'Input', //TODO 范围查询 + + //colProps: {span: 6}, + }, + + + +<#-- 结束循环 --> +]; +//表单数据 +export const formSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign id_exists = false> +<#list columns as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName == 'id'> + <#assign id_exists = true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, + <#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true + + }, + + +<#if id_exists == false> + // TODO 主键隐藏字段,目前写死为ID + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; +//子表单数据 +<#list subTables as sub> +<#if sub.foreignRelationType =='1'> +export const ${sub.entityName?uncap_first}FormSchema: FormSchema[] = [ +<#assign form_cat_tree = false> +<#assign form_cat_back = ""> +<#assign bpm_flag=false> +<#assign sub_id_exists = false> +<#list sub.colums as po><#rt/> +<#if po.fieldDbName=='bpm_status'> + <#assign bpm_flag=true> + +<#if po.fieldDbName == 'id'> + <#assign sub_id_exists = true> + +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if po.isShow =='Y' && po.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#assign form_field_dictCode=""> + <#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}"> + <#elseif po.dictField?default("")?trim?length gt 1> + <#assign form_field_dictCode="${po.dictField}"> + + { + label: '${po.filedComment}', + field: ${autoStringSuffix(po)}, +<#-- update-begin-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.defaultVal??> + <#if po.fieldDbType=="BigDecimal" || po.fieldDbType=="double" || po.fieldDbType=="int"> + defaultValue: ${po.defaultVal}, + <#else> + defaultValue: "${po.defaultVal}", + + +<#-- update-end-author:taoyan date:2022-6-24 for: VUEN-1190【代码生成】默认值未生成 --> + <#if po.classType =='date'> + component: 'DatePicker', + componentProps: { + <#if po.extendParams?exists && po.extendParams.picker?exists> + picker: '${po.extendParams.picker}', + + valueFormat: 'YYYY-MM-DD' + }, + <#elseif po.classType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime:true, + valueFormat: 'YYYY-MM-DD HH:mm:ss' + }, + <#elseif po.classType =='time'> + component: 'TimePicker', + componentProps: { + valueFormat: 'HH:mm:ss' + }, + <#elseif po.classType =='popup'> + <#include "/common/form/vue3popup.ftl"> + <#elseif po.classType=='popup_dict'> + component: 'JPopupDict', + componentProps: { + placeholder: '请选择${po.filedComment}', + dictCode: '${po.dictTable},${po.dictText},${po.dictField}', + multi: ${po.extendParams.popupMulti?c} + }, + <#elseif po.classType =='sel_depart'> + component: 'JSelectDept', + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='switch'> + component: 'JSwitch', + componentProps:{ + <#if po.dictField != 'is_open'> + options:${po.dictField} + + }, + <#elseif po.classType =='pca'> + component: 'JAreaLinkage', + componentProps: { + saveCode: 'region', + }, + <#elseif po.classType =='markdown'> + component: 'JMarkdownEditor',//注意string转换问题 + <#elseif po.classType =='password'> + component: 'InputPassword', + <#elseif po.classType =='sel_user'> +<#-- update-begin---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + component: 'JSelectUser', +<#-- update-end---author:chenrui ---date:20240102 for:[issue/#5711]修复用户选择组件在生成代码后变成部门用户选择组件---------- --> + componentProps:{ + <#if po.extendParams?exists && po.extendParams.text?exists> + labelKey: '${po.extendParams.text}', + + <#if po.extendParams?exists && po.extendParams.store?exists> + rowKey: '${po.extendParams.store}', + + }, + <#elseif po.classType =='textarea'> + component: 'InputTextArea', + <#elseif po.classType=='list'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#elseif po.classType=='radio'> + component: 'JDictSelectTag', + componentProps:{ + dictCode:"${form_field_dictCode}", + type: "radio", + <#if po.fieldDbType=='int'> + stringToNumber: true + + }, + <#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选---------- --> + <#elseif po.classType=='list_multi'> + component: 'JSelectMultiple', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#elseif po.classType=='checkbox'> + component: 'JCheckbox', + componentProps:{ + dictCode:"${form_field_dictCode}" + }, + <#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7583] Vue3风格表单页面多选控件渲染成了下拉多选----------- --> + <#elseif po.classType=='sel_search'> + component: 'JSearchSelect', + componentProps:{ + dict:"${form_field_dictCode}" + }, +<#elseif po.classType=='cat_tree'> + <#assign form_cat_tree = true> + component: 'JCategorySelect', + componentProps:{ + pcode:"${po.dictField?default("")}", //TODO back和事件未添加,暂时有问题 + }, + <#if po.dictText?default("")?trim?length gt 1> + <#assign form_cat_back = "${po.dictText}"> + + <#elseif po.fieldDbType=='int' || po.fieldDbType=='long' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'> + component: 'InputNumber', + <#elseif po.classType=='file'> + component: 'JUpload', + componentProps:{ + <#if po.uploadnum??> + maxCount:${po.uploadnum} + + }, + <#elseif po.classType=='image'> + component: 'JImageUpload', + componentProps:{ + <#if po.uploadnum??> + fileMax:${po.uploadnum} + <#else> + fileMax: 0 + + }, + <#elseif po.classType=='umeditor'> + component: 'JEditor', + <#elseif po.classType == 'sel_tree'> + component: 'JTreeSelect', + componentProps:{ + <#if po.dictText??> + <#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??> + dict:"${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}", + <#elseif po.dictText?split(',')[1]??> + pidField:"${po.dictText?split(',')[1]}", + <#elseif po.dictText?split(',')[3]??> + hasChildField:"${po.dictText?split(',')[3]}", + + + pidValue:"${po.dictField}", + }, + <#elseif po.classType=='link_table'> + component: 'JLinkTableCard', + componentProps: { + valueField: '${po.dictField}', + textField: '${po.dictText}', + tableName: '${po.dictTable}', + multi: <#if (po.queryMode!"") == "multi">true<#else>false + }, + <#else> + component: 'Input', + + <#include "/common/utils.ftl"> + <#if po.isShow == 'Y' && poHasCheck(po)> + dynamicRules: ({model,schema}) => { + <#if po.fieldName != 'id'> + <#assign fieldValidType = po.fieldValidType!''> + return [ + <#-- 非空校验 --> + <#if po.nullable == 'N' || fieldValidType == '*'> + { required: true, message: '请输入${po.filedComment}!'}, + <#elseif fieldValidType!=''> + { required: false}, + + <#-- 唯一校验 --> + <#if fieldValidType == 'only'> + {...rules.duplicateCheckRule(<#if sub?default("")?trim?length gt 1>'${sub.tableName}'<#else>'${tableName}', '${po.fieldDbName}',model,schema)[0]}, + <#-- 6到16位数字 --> + <#elseif fieldValidType == 'n6-16'> + { pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'}, + <#-- 6到16位任意字符 --> + <#elseif fieldValidType == '*6-16'> + { pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'}, + <#-- 6到18位字母 --> + <#elseif fieldValidType == 's6-18'> + { pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'}, + <#-- 网址 --> + <#elseif fieldValidType == 'url'> + { pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'}, + <#-- 电子邮件 --> + <#elseif fieldValidType == 'e'> + { pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'}, + <#-- 手机号码 --> + <#elseif fieldValidType == 'm'> + { pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'}, + <#-- 邮政编码 --> + <#elseif fieldValidType == 'p'> + { pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'}, + <#-- 字母 --> + <#elseif fieldValidType == 's'> + { pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'}, + <#-- 数字 --> + <#elseif fieldValidType == 'n'> + { pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'}, + <#-- 整数 --> + <#elseif fieldValidType == 'z'> + { pattern: /^-?\d+$/, message: '请输入整数!'}, + <#-- 金额 --> + <#elseif fieldValidType == 'money'> + { pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'}, + <#-- 正则校验 --> + <#elseif fieldValidType != '' && fieldValidType != '*'> + { pattern: '${fieldValidType}', message: '不符合校验规则!'}, + <#-- 无校验 --> + <#else> + <#t> + + ]; + + }, + + <#if po.readonly=='Y'> + dynamicDisabled:true + + }, + + +<#if sub_id_exists == false> + { + label: '', + field: 'id', + component: 'Input', + show: false + }, + +]; + + +//子表表格配置 +<#list subTables as sub> +<#if sub.foreignRelationType =='0'> +export const ${sub.entityName?uncap_first}Columns: JVxeColumn[] = [ +<#assign popupBackFields = ""> + +<#-- 循环子表的列 开始 --> +<#list sub.colums as col><#rt/> +<#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.isShow =='Y' && col.fieldName !='delFlag'> +<#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> +<#if col.filedComment !='外键' > + { + title: '${col.filedComment}', + key: '${autoStringSuffixForModel(col)}', +<#if col.classType =='date'> + type: JVxeTypes.date, + <#if col.extendParams?exists && col.extendParams.picker?exists> + picker: '${col.extendParams.picker}', + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='datetime'> + type: JVxeTypes.datetime, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='time'> + type: JVxeTypes.time, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='textarea'> + type: JVxeTypes.textarea, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list' || col.classType =='radio'> + type: JVxeTypes.select, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='list_multi' || col.classType =='checkbox'> + type: JVxeTypes.selectMultiple, + options:[], + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_search'> + type: JVxeTypes.selectSearch, + <#if col.dictTable?default("")?trim?length gt 1> + dictCode:"${col.dictTable},${col.dictText},${col.dictField}", + <#else> + dictCode:"${col.dictField}", + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_depart'> + type: JVxeTypes.departSelect, + props:{ + <#if col.extendParams?exists && col.extendParams.text?exists> + labelKey: '${col.extendParams.text}', + + <#if col.extendParams?exists && col.extendParams.store?exists> + rowKey: '${col.extendParams.store}', + + }, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='sel_user'> + type: JVxeTypes.userSelect, + props:{ + <#if col.extendParams?exists && col.extendParams.text?exists> + labelKey: '${col.extendParams.text}', + + <#if col.extendParams?exists && col.extendParams.store?exists> + rowKey: '${col.extendParams.store}', + + }, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='image'> + type: JVxeTypes.image, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='file'> + type: JVxeTypes.file, + token:true, + responseName:"message", + <#if col.readonly=='Y'> + disabled:true, + + <#if col.uploadnum??> + number: ${col.uploadnum}, + +<#elseif col.classType =='switch'> + type: JVxeTypes.checkbox, + <#if col.dictField == 'is_open'> + customValue: ['Y', 'N'], + <#else> + customValue: ${col.dictField}, + + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType=='pca'> + type: JVxeTypes.pca, + <#if col.readonly=='Y'> + disabled:true, + +<#elseif col.classType =='popup'> +<#if popupBackFields?length gt 0> + <#assign popupBackFields = "${popupBackFields}"+","+"${col.dictText}"> +<#else> + <#assign popupBackFields = "${col.dictText}"> + + <#include "/common/form/vue3Jvxepopup.ftl"> +<#-- update-begin-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> +<#-- elseif "int,decimal,double,"?contains(col.classType) --> +<#elseif col.fieldDbType=='int' || col.fieldDbType=='long' || col.fieldDbType=='double' || col.fieldDbType=='BigDecimal'> +<#-- update-end-author:taoyan date:20220523 for: VUEN-1084 【vue3】online表单测试发现的新问题 20、一对多列字段类型生成的不对,数字或者金额类型 --> + type: JVxeTypes.inputNumber, + <#if col.readonly=='Y'> + disabled:true, + +<#else> + type: JVxeTypes.input, + <#if col.readonly=='Y'> + disabled:true, + + +<#if col.classType =='list_multi' || col.classType =='checkbox'> + width:"250px", +<#else> + width:"200px", + +<#if col.classType =='file'> + placeholder: '请选择文件', +<#else> + placeholder: '请输入${'$'}{title}', + +<#if col.defaultVal??> +<#if col.fieldDbType=="BigDecimal" || col.fieldDbType=="double" || col.fieldDbType=="int"> + defaultValue:${col.defaultVal}, + <#else> + defaultValue:"${col.defaultVal}", + +<#else> + defaultValue:'', + +<#-- 子表的校验 --> + <#include "/common/validatorRulesTemplate/sub-vue3.ftl"> + }, + + + +<#-- 循环子表的列 结束 --> + ] + + + +<#-- update-begin---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> +// 高级查询数据 +export const superQuerySchema = { + <#list columns as po> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(po,po_index)}, + + + //子表高级查询 + <#list subTables as sub> + ${sub.entityName?uncap_first}: { + title: '${sub.ftlDescription}', + view: 'table', + fields: { + <#list sub.colums as subCol> + <#-- update-begin---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + <#if subCol.isShowList =='Y' && subCol.fieldName !='id' && subCol.fieldName !='delFlag'> + <#-- update-end---author:chenrui ---date:20240108 for:[issues/5755]vue代码不加入逻辑删除字段---------- --> + ${superQueryFieldListForVue3(subCol,subCol_index)}, + + + } + }, + +}; +<#-- update-end---author:chenrui ---date:20231228 for:[QQYUN-7527]vue3代码生成默认带上高级查询---------- --> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql new file mode 100644 index 0000000..5396d5d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/V${currentDate}_1__menu_insert_${entityName}.sql @@ -0,0 +1 @@ +<#include "/common/sql/menu_insert.ftl"> \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei new file mode 100644 index 0000000..b3080bd --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Form.vuei @@ -0,0 +1,251 @@ +<#include "/common/utils.ftl"> + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei new file mode 100644 index 0000000..34a5ac1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/${entityName}Modal.vuei @@ -0,0 +1,366 @@ +<#include "/common/utils.ftl"> + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei new file mode 100644 index 0000000..c02ad27 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template-online/tab/onetomany/java/${bussiPackage}/${entityPackage}/vue3/components/[1-n]Form.vuei @@ -0,0 +1,87 @@ +<#include "/common/utils.ftl"> +<#list subTables as sub> +<#if sub.foreignRelationType=='1'> +#segment#${sub.entityName}Form.vue + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..529985f --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,167 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.jeecg.common.util.oConvertUtils; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import java.util.Date; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.system.base.controller.JeecgController; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Slf4j +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackage}/${entityName?uncap_first}") +public class ${entityName}Controller extends JeecgController<${entityName}, I${entityName}Service> { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName} ${entityName?uncap_first}) { + ${entityName?uncap_first}Service.save(${entityName?uncap_first}); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName} ${entityName?uncap_first}) { + ${entityName?uncap_first}Service.updateById(${entityName?uncap_first}); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + return Result.OK(${entityName?uncap_first}); + } + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + return super.exportXls(request, ${entityName?uncap_first}, ${entityName}.class, "${tableVo.ftlDescription}"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, ${entityName}.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..84997b5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,52 @@ +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${tableName}") +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName} { + + <#list originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + @Excel(name = "${po.filedComment}", width = 15) + + + @Schema(description = "${po.filedComment}") + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..f00240a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,14 @@ +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..6326220 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,19 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/uniapp/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/uniapp/${entityName}Form.vuei new file mode 100644 index 0000000..e72b4af --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/uniapp/${entityName}Form.vuei @@ -0,0 +1,93 @@ + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/uniapp/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/uniapp/${entityName}List.vuei new file mode 100644 index 0000000..4e499af --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/uniapp/${entityName}List.vuei @@ -0,0 +1,44 @@ + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..23e3122 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,173 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..fa7484e --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,130 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal__Style#Drawer.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal__Style#Drawer.vuei new file mode 100644 index 0000000..9a90774 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal__Style#Drawer.vuei @@ -0,0 +1,150 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..68e72ee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,152 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..9bfdb0c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,65 @@ +import {defHttp} from '/@/utils/http/axios'; +import {Modal} from 'ant-design-vue'; + +enum Api { + list = '/${entityPackage}/${entityName?uncap_first}/list', + save='/${entityPackage}/${entityName?uncap_first}/add', + edit='/${entityPackage}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackage}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackage}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackage}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackage}/${entityName?uncap_first}/exportXls', +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + * @param params + * @param handleSuccess + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + * @param handleSuccess + */ +export const batchDelete = (params, handleSuccess) => { + Modal.confirm({ + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + * @param isUpdate 是否是更新数据 + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..3515c49 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,62 @@ +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; + +export const columns: BasicColumn[] = [ + <#list columns as po> + <#if po.fieldName !='id'> + { + title: '${po.filedComment}', + dataIndex: '${po.fieldName}' + }, + + +]; + +export const searchFormSchema: FormSchema[] = [ +<#list columns as po> +<#if po.fieldName !='id' && po_index<= tableVo.searchFieldNum> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'TimePicker' + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber' + <#else> + component: 'Input' + + }, + + +]; + +export const formSchema: FormSchema[] = [ + // TODO 主键隐藏字段,目前写死为ID + {label: '', field: 'id', component: 'Input', show: false}, +<#list columns as po><#rt/> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime: true, + valueFormat: 'YYYY-MM-DD hh:mm:ss', + }, + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber', + <#else> + component: 'Input', + + <#if po.fieldName =='id'><#rt/> + show:false, + + }, + +]; diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..990c3d1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/controller/${entityPackage}/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/controller/${entityPackage}/${entityName}Controller.javai new file mode 100644 index 0000000..cddb8c8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/controller/${entityPackage}/${entityName}Controller.javai @@ -0,0 +1,170 @@ +package ${bussiPackage}.controller.${entityPackage}; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.util.oConvertUtils; +import ${bussiPackage}.entity.${entityPackage}.${entityName}; +import ${bussiPackage}.service.${entityPackage}.I${entityName}Service; +import org.jeecg.common.system.base.controller.JeecgController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; + +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.extern.slf4j.Slf4j; + +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackage}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller extends JeecgController<${entityName}, I${entityName}Service> { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + return Result.OK(pageList); + + } + + /** + * 添加 + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName} ${entityName?uncap_first}) { + ${entityName?uncap_first}Service.save(${entityName?uncap_first}); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first} + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName} ${entityName?uncap_first}) { + ${entityName?uncap_first}Service.updateById(${entityName?uncap_first}); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.removeById(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + return Result.OK(${entityName?uncap_first}); + } + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + return super.exportXls(request, ${entityName?uncap_first}, ${entityName}.class, "${tableVo.ftlDescription}"); + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, ${entityName}.class); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/entity/${entityPackage}/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/entity/${entityPackage}/${entityName}.javai new file mode 100644 index 0000000..484f6a4 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/entity/${entityPackage}/${entityName}.javai @@ -0,0 +1,48 @@ +package ${bussiPackage}.entity.${entityPackage}; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${tableName}") +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#list originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + @Excel(name = "${po.filedComment}", width = 15) + + + @Schema(description = "${po.filedComment}") + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/mapper/${entityPackage}/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/mapper/${entityPackage}/${entityName}Mapper.javai new file mode 100644 index 0000000..4705b6c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/mapper/${entityPackage}/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.mapper.${entityPackage}; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.entity.${entityPackage}.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/mapper/${entityPackage}/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/mapper/${entityPackage}/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..fb6f712 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/mapper/${entityPackage}/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/service/${entityPackage}/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/service/${entityPackage}/I${entityName}Service.javai new file mode 100644 index 0000000..8fdad41 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/service/${entityPackage}/I${entityName}Service.javai @@ -0,0 +1,14 @@ +package ${bussiPackage}.service.${entityPackage}; + +import ${bussiPackage}.entity.${entityPackage}.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/service/${entityPackage}/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/service/${entityPackage}/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..4b09157 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/service/${entityPackage}/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,19 @@ +package ${bussiPackage}.service.${entityPackage}.impl; + +import ${bussiPackage}.entity.${entityPackage}.${entityName}; +import ${bussiPackage}.mapper.${entityPackage}.${entityName}Mapper; +import ${bussiPackage}.service.${entityPackage}.I${entityName}Service; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/${entityName}List.vuei new file mode 100644 index 0000000..106943b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/${entityName}List.vuei @@ -0,0 +1,173 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..ff934f1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/modules/${entityName}Modal.vuei @@ -0,0 +1,156 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/modules/${entityName}Modal__Style#Drawer.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/modules/${entityName}Modal__Style#Drawer.vuei new file mode 100644 index 0000000..4446ba6 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue/${entityPackage}/modules/${entityName}Modal__Style#Drawer.vuei @@ -0,0 +1,162 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}List.vuei new file mode 100644 index 0000000..68e72ee --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}List.vuei @@ -0,0 +1,152 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}__api.tsi new file mode 100644 index 0000000..9bfdb0c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}__api.tsi @@ -0,0 +1,65 @@ +import {defHttp} from '/@/utils/http/axios'; +import {Modal} from 'ant-design-vue'; + +enum Api { + list = '/${entityPackage}/${entityName?uncap_first}/list', + save='/${entityPackage}/${entityName?uncap_first}/add', + edit='/${entityPackage}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackage}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackage}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackage}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackage}/${entityName?uncap_first}/exportXls', +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + * @param params + * @param handleSuccess + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + * @param handleSuccess + */ +export const batchDelete = (params, handleSuccess) => { + Modal.confirm({ + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + * @param isUpdate 是否是更新数据 + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}__data.tsi new file mode 100644 index 0000000..3515c49 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/${entityName}__data.tsi @@ -0,0 +1,62 @@ +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; + +export const columns: BasicColumn[] = [ + <#list columns as po> + <#if po.fieldName !='id'> + { + title: '${po.filedComment}', + dataIndex: '${po.fieldName}' + }, + + +]; + +export const searchFormSchema: FormSchema[] = [ +<#list columns as po> +<#if po.fieldName !='id' && po_index<= tableVo.searchFieldNum> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'TimePicker' + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber' + <#else> + component: 'Input' + + }, + + +]; + +export const formSchema: FormSchema[] = [ + // TODO 主键隐藏字段,目前写死为ID + {label: '', field: 'id', component: 'Input', show: false}, +<#list columns as po><#rt/> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime: true, + valueFormat: 'YYYY-MM-DD hh:mm:ss', + }, + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber', + <#else> + component: 'Input', + + <#if po.fieldName =='id'><#rt/> + show:false, + + }, + +]; diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..990c3d1 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/one2/java/${bussiPackage}/vue3/${entityPackage}/modules/${entityName}Modal.vuei @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..35a3d9a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,250 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jeecg.common.system.vo.LoginUser; +import org.apache.shiro.SecurityUtils; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; + +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.util.oConvertUtils; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.vo.${entityName}Page; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.service.I${sub.entityName}Service; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackage}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + <#list subTables as sub> + @Autowired + private I${sub.entityName}Service ${sub.entityName?uncap_first}Service; + + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName?uncap_first}Service.saveMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-编辑") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName?uncap_first}Service.updateMain(${entityName?uncap_first}, <#list subTables as sub>${entityName?uncap_first}Page.get${sub.entityName}List()<#if sub_has_next>,); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delMain(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.delBatchMain(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + return Result.OK(${entityName?uncap_first}); + } + + <#list subTables as sub> + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-通过主表ID查询") + @Operation(summary="${sub.ftlDescription}-通过主表ID查询") + @GetMapping(value = "/query${sub.entityName}ByMainId") + public Result query${sub.entityName}ListByMainId(@RequestParam(name="id",required=true) String id) { + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(id); + return Result.OK(${sub.entityName?uncap_first}List); + } + + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + // Step.1 组装查询条件 + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, request.getParameterMap()); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + //Step.2 获取导出数据 + List<${entityName}Page> pageList = new ArrayList<${entityName}Page>(); + List<${entityName}> ${entityName?uncap_first}List = ${entityName?uncap_first}Service.list(queryWrapper); + for (${entityName} temp : ${entityName?uncap_first}List) { + ${entityName}Page vo = new ${entityName}Page(); + BeanUtils.copyProperties(temp, vo); + <#list subTables as sub> + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(temp.getId()); + vo.set${sub.entityName}List(${sub.entityName?uncap_first}List); + + pageList.add(vo); + } + //Step.3 调用AutoPoi导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + mv.addObject(NormalExcelConstants.FILE_NAME, "${tableVo.ftlDescription}"); + mv.addObject(NormalExcelConstants.CLASS, ${entityName}Page.class); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("${tableVo.ftlDescription}数据", "导出人:"+sysUser.getRealname(), "${tableVo.ftlDescription}")); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + MultipartFile file = entity.getValue();// 获取上传文件对象 + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List<${entityName}Page> list = ExcelImportUtil.importExcel(file.getInputStream(), ${entityName}Page.class, params); + for (${entityName}Page page : list) { + ${entityName} po = new ${entityName}(); + BeanUtils.copyProperties(page, po); + ${entityName?uncap_first}Service.saveMain(po, <#list subTables as sub>page.get${sub.entityName}List()<#if sub_has_next>,); + } + return Result.OK("文件导入成功!数据行数:" + list.size()); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.OK("文件导入失败!"); + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..4677085 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,42 @@ +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${tableName}") +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#list originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + + @Schema(description = "${po.filedComment}") + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai new file mode 100644 index 0000000..5a13e9b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai @@ -0,0 +1,53 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}.java +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import java.util.Date; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${subTab.tableName}") +@Schema(description="${tableVo.ftlDescription}") +public class ${subTab.entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#list subTab.originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !subTab.foreignKeys?seq_contains(po.fieldName?cap_first)> + @Excel(name = "${po.filedComment}", width = 15) + + + + @Schema(description = "${po.filedComment}") + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai new file mode 100644 index 0000000..9585548 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai @@ -0,0 +1,21 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Mapper.java +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${subTab.entityName}Mapper extends BaseMapper<${subTab.entityName}> { + + public boolean deleteByMainId(String mainId); + + public List<${subTab.entityName}> selectByMainId(String mainId); +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml new file mode 100644 index 0000000..708ae0b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml @@ -0,0 +1,36 @@ +<#list subTables as subTab> +<#assign originalForeignKeys = subTab.originalForeignKeys> +#segment#${subTab.entityName}Mapper.xml + + + + + + DELETE + FROM ${subTab.tableName} + WHERE + <#list originalForeignKeys as key> + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + ${key} = ${r'#'}{${primaryKeyField}} <#rt/> + <#else> + ${key} = ${r'#'}{${key}} <#rt/> + + <#if key_has_next>AND + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..e7d9914 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,42 @@ +package ${bussiPackage}.${entityPackage}.service; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /** + * 添加一对多 + * + */ + public void saveMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) ; + + /** + * 修改一对多 + * + */ + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,); + + /** + * 删除一对多 + */ + public void delMain (String id); + + /** + * 批量删除一对多 + */ + public void delBatchMain (Collection idList); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai new file mode 100644 index 0000000..0f85cb3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai @@ -0,0 +1,19 @@ +<#list subTables as subTab> +#segment#I${subTab.entityName}Service.java +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${subTab.entityName}Service extends IService<${subTab.entityName}> { + + public List<${subTab.entityName}> selectByMainId(String mainId); +} + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..1fd8198 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,101 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.mapper.${sub.entityName}Mapper; + +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.io.Serializable; +import java.util.List; +import java.util.Collection; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Autowired + private ${entityName}Mapper ${entityName?uncap_first}Mapper; + <#list subTables as sub> + @Autowired + private ${sub.entityName}Mapper ${sub.entityName?uncap_first}Mapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMain(${entityName} ${entityName?uncap_first}, <#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.insert(${entityName?uncap_first}); + <#list subTables as sub> + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.updateById(${entityName?uncap_first}); + + //1.先删除子表数据 + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(${entityName?uncap_first}.getId()); + + + //2.子表数据重新插入 + <#list subTables as sub> + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delMain(String id) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatchMain(Collection idList) { + for(Serializable id:idList) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id.toString()); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai new file mode 100644 index 0000000..0ce41d3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai @@ -0,0 +1,30 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}ServiceImpl.java +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${subTab.entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${subTab.entityName}Service; +import org.springframework.stereotype.Service; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${subTab.entityName}ServiceImpl extends ServiceImpl<${subTab.entityName}Mapper, ${subTab.entityName}> implements I${subTab.entityName}Service { + + @Autowired + private ${subTab.entityName}Mapper ${subTab.entityName?uncap_first}Mapper; + + @Override + public List<${subTab.entityName}> selectByMainId(String mainId) { + return ${subTab.entityName?uncap_first}Mapper.selectByMainId(mainId); + } +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai new file mode 100644 index 0000000..3768768 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai @@ -0,0 +1,53 @@ +package ${bussiPackage}.${entityPackage}.vo; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecgframework.poi.excel.annotation.ExcelCollection; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import java.util.Date; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName}Page { + + <#list originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + <#else> + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + @Excel(name = "${po.filedComment}", width = 15) + + + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + + + <#list subTables as sub> + @ExcelCollection(name="${sub.ftlDescription}") + @Schema(description = "${sub.ftlDescription}") + private List<${sub.entityName}> ${sub.entityName?uncap_first}List; + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..d6d1ea0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,162 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei new file mode 100644 index 0000000..76e6bdc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Form.vuei @@ -0,0 +1,146 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..2b46b40 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..9459166 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,151 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..5ab6f83 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,76 @@ +import {defHttp} from '/@/utils/http/axios'; +import {Modal} from 'ant-design-vue'; + +enum Api { + list = '/${entityPackage}/${entityName?uncap_first}/list', + save='/${entityPackage}/${entityName?uncap_first}/add', + edit='/${entityPackage}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackage}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackage}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackage}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackage}/${entityName?uncap_first}/exportXls', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackage}/${entityName?uncap_first}/query${sub.entityName}ByMainId', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; +<#list subTables as sub><#rt/> +/** + * 查询子表数据 + * @param params + */ +export const ${sub.entityName?uncap_first}List = Api.${sub.entityName?uncap_first}List; + +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + * @param params + * @param handleSuccess + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + * @param handleSuccess + */ +export const batchDelete = (params, handleSuccess) => { + Modal.confirm({ + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + * @param isUpdate 是否是更新数据 + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..79b8d13 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,102 @@ +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types' +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#if po.fieldName !='id'> + { + title: '${po.filedComment}', + align:"center", + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + return !text?"":(text.length>10?text.substr(0,10):text) + }, + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +<#list columns as po> +<#if po.fieldName !='id' && po_index<= tableVo.searchFieldNum> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'TimePicker' + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber' + <#else> + component: 'Input' + + }, + + +]; + +export const formSchema: FormSchema[] = [ + // TODO 主键隐藏字段,目前写死为ID + {label: '', field: 'id', component: 'Input', show: false}, +<#list columns as po><#rt/> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime: true, + valueFormat: 'YYYY-MM-DD hh:mm:ss', + }, + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber', + <#else> + component: 'Input', + + <#if po.fieldName =='id'><#rt/> + show:false, + + }, + +]; +//子表表格配置 +<#list subTables as sub> +export const ${sub.entityName?uncap_first}Columns: JVxeColumn[] = [ +<#-- 循环子表的列 开始 --> +<#list sub.colums as col><#rt/> +<#if col.filedComment !='外键' > + { + title: '${col.filedComment}', + key: '${col.fieldName}', +<#if col.fieldType =='date'> + type: JVxeTypes.date, +<#elseif col.fieldType =='datetime'> + type: JVxeTypes.datetime, +<#elseif "int,decimal,double,"?contains(col.fieldType)> + type: JVxeTypes.inputNumber, +<#else> + type: JVxeTypes.input, + + width:"200px", + placeholder: '请输入${'$'}{title}', + defaultValue: '', +<#-- 子表的校验 --> +<#if col.nullable =='N'> + validateRules: [{ required: true, message: '${'$'}{title}不能为空' }], + + }, + + +<#-- 循环子表的列 结束 --> + ] + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..9085782 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei @@ -0,0 +1,119 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai new file mode 100644 index 0000000..cebf518 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/controller/${entityName}Controller.javai @@ -0,0 +1,337 @@ +package ${bussiPackage}.${entityPackage}.controller; + +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.shiro.SecurityUtils; +import org.jeecgframework.poi.excel.ExcelImportUtil; +import org.jeecgframework.poi.excel.def.NormalExcelConstants; +import org.jeecgframework.poi.excel.entity.ExportParams; +import org.jeecgframework.poi.excel.entity.ImportParams; +import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.util.oConvertUtils; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import ${bussiPackage}.${entityPackage}.vo.${entityName}Page; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.service.I${sub.entityName}Service; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.common.aspect.annotation.AutoLog; + + /** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Tag(name="${tableVo.ftlDescription}") +@RestController +@RequestMapping("/${entityPackage}/${entityName?uncap_first}") +@Slf4j +public class ${entityName}Controller { + @Autowired + private I${entityName}Service ${entityName?uncap_first}Service; + <#list subTables as sub> + @Autowired + private I${sub.entityName}Service ${sub.entityName?uncap_first}Service; + + + /** + * 分页列表查询 + * + * @param ${entityName?uncap_first} + * @param pageNo + * @param pageSize + * @param req + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-分页列表查询") + @Operation(summary="${tableVo.ftlDescription}-分页列表查询") + @GetMapping(value = "/list") + public Result queryPageList(${entityName} ${entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, req.getParameterMap()); + Page<${entityName}> page = new Page<${entityName}>(pageNo, pageSize); + IPage<${entityName}> pageList = ${entityName?uncap_first}Service.page(page, queryWrapper); + return Result.OK(pageList); + } + + /** + * 添加 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-添加") + @Operation(summary="${tableVo.ftlDescription}-添加") + @PostMapping(value = "/add") + public Result add(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName?uncap_first}Service.save(${entityName?uncap_first}); + return Result.OK("添加成功!"); + } + + /** + * 编辑 + * + * @param ${entityName?uncap_first}Page + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-编辑") + @Operation(summary="${tableVo.ftlDescription}-") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit(@RequestBody ${entityName}Page ${entityName?uncap_first}Page) { + ${entityName} ${entityName?uncap_first} = new ${entityName}(); + BeanUtils.copyProperties(${entityName?uncap_first}Page, ${entityName?uncap_first}); + ${entityName?uncap_first}Service.updateById(${entityName?uncap_first}); + return Result.OK("编辑成功!"); + } + + /** + * 通过id删除 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id删除") + @Operation(summary="${tableVo.ftlDescription}-通过id删除") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name="id",required=true) String id) { + ${entityName?uncap_first}Service.delMain(id); + return Result.OK("删除成功!"); + } + + /** + * 批量删除 + * + * @param ids + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-批量删除") + @Operation(summary="${tableVo.ftlDescription}-批量删除") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { + this.${entityName?uncap_first}Service.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } + + /** + * 通过id查询 + * + * @param id + * @return + */ + @AutoLog(value = "${tableVo.ftlDescription}-通过id查询") + @Operation(summary="${tableVo.ftlDescription}-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name="id",required=true) String id) { + ${entityName} ${entityName?uncap_first} = ${entityName?uncap_first}Service.getById(id); + return Result.OK(${entityName?uncap_first}); + } + + //===========================以下是子表信息操作相关API==================================== + + <#list subTables as sub> + /** + * 通过主表id查询${sub.ftlDescription} + * + * @param ${sub.entityName?uncap_first} + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-通过主表id查询") + @Operation(summary="${sub.ftlDescription}-通过主表id查询") + <#-- update-begin--Author:kangxiaolin Date:20190905 for:[442]主子表分开维护,生成的代码子表的分页改为真实的分页-------------------- --> + @GetMapping(value = "/list${sub.entityName}ByMainId") + public Result list${sub.entityName}ByMainId(${sub.entityName} ${sub.entityName?uncap_first}, + @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, + @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, + HttpServletRequest req) { + if(<#list sub.foreignKeys as key><#rt/> + <#if key?lower_case?index_of("${primaryKeyField}")!=-1><#rt/> + <#if key_index == 0><#rt/> +${sub.entityName?uncap_first}.get${key?cap_first}()!=null<#rt/> + <#else><#rt/> +|| ${sub.entityName?uncap_first}.get${key?cap_first}()!=null<#rt/> + <#rt/> + <#else><#rt/> + <#if key_index == 0><#rt/> +${sub.entityName?uncap_first}.get${key}()!=null<#rt/> + <#else><#rt/> +|| ${sub.entityName?uncap_first}.get${key}()!=null<#rt/> + <#rt/> + + <#rt/> +) { + QueryWrapper<${sub.entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${sub.entityName?uncap_first}, req.getParameterMap()); + Page<${sub.entityName}> page = new Page<${sub.entityName}>(pageNo, pageSize); + IPage<${sub.entityName}> pageList = ${sub.entityName?uncap_first}Service.page(page, queryWrapper); + return Result.OK(pageList); + }else{ + return Result.OK(); + } + } + <#-- update-end--Author:kangxiaolin Date:20190905 for:[442]主子表分开维护,生成的代码子表的分页改为真实的分页-------------------- --> + + /** + * 添加${sub.ftlDescription} + * + * @param ${sub.entityName?uncap_first} + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-添加") + @Operation(summary="${sub.ftlDescription}-添加") + @PostMapping(value = "/add${sub.entityName}") + public Result add${sub.entityName}(@RequestBody ${sub.entityName} ${sub.entityName?uncap_first}) { + ${sub.entityName?uncap_first}Service.save(${sub.entityName?uncap_first}); + return Result.OK("添加${sub.ftlDescription}成功!"); + } + + /** + * 编辑${sub.ftlDescription} + * + * @param ${sub.entityName?uncap_first} + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-编辑") + @Operation(summary="${sub.ftlDescription}-编辑") + @RequestMapping(value = "/edit${sub.entityName}", method = {RequestMethod.PUT,RequestMethod.POST}) + public Result edit${sub.entityName}(@RequestBody ${sub.entityName} ${sub.entityName?uncap_first}) { + ${sub.entityName?uncap_first}Service.updateById(${sub.entityName?uncap_first}); + return Result.OK("编辑${sub.ftlDescription}成功!"); + } + + /** + * 通过id删除${sub.ftlDescription} + * + * @param id + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-通过id删除") + @Operation(summary="${sub.ftlDescription}-通过id删除") + @DeleteMapping(value = "/delete${sub.entityName}") + public Result delete${sub.entityName}(@RequestParam(name = "id", required = true) String id) { + ${sub.entityName?uncap_first}Service.removeById(id); + return Result.OK("删除${sub.ftlDescription}成功!"); + } + + /** + * 批量删除${sub.ftlDescription} + * + * @param ids + * @return + */ + @AutoLog(value = "${sub.ftlDescription}-批量删除") + @Operation(summary="${sub.ftlDescription}-批量删除") + @DeleteMapping(value = "/deleteBatch${sub.entityName}") + public Result deleteBatch${sub.entityName}(@RequestParam(name = "ids", required = true) String ids) { + if (ids == null || "".equals(ids.trim())) { + return Result.error("参数不识别!"); + } + this.${sub.entityName?uncap_first}Service.removeByIds(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + + } + + + + /** + * 导出excel + * + * @param request + * @param ${entityName?uncap_first} + */ + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, ${entityName} ${entityName?uncap_first}) { + // Step.1 组装查询条件 + QueryWrapper<${entityName}> queryWrapper = QueryGenerator.initQueryWrapper(${entityName?uncap_first}, request.getParameterMap()); + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + + //Step.2 获取导出数据 + List<${entityName}Page> pageList = new ArrayList<${entityName}Page>(); + List<${entityName}> ${entityName?uncap_first}List = ${entityName?uncap_first}Service.list(queryWrapper); + for (${entityName} temp : ${entityName?uncap_first}List) { + ${entityName}Page vo = new ${entityName}Page(); + BeanUtils.copyProperties(temp, vo); + <#list subTables as sub> + List<${sub.entityName}> ${sub.entityName?uncap_first}List = ${sub.entityName?uncap_first}Service.selectByMainId(temp.getId()); + vo.set${sub.entityName}List(${sub.entityName?uncap_first}List); + + pageList.add(vo); + } + //Step.3 调用AutoPoi导出Excel + ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); + mv.addObject(NormalExcelConstants.FILE_NAME, "${tableVo.ftlDescription}"); + mv.addObject(NormalExcelConstants.CLASS, ${entityName}Page.class); + mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("${tableVo.ftlDescription}数据", "导出人:"+sysUser.getRealname(), "${tableVo.ftlDescription}")); + mv.addObject(NormalExcelConstants.DATA_LIST, pageList); + return mv; + } + + /** + * 通过excel导入数据 + * + * @param request + * @param response + * @return + */ + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; + Map fileMap = multipartRequest.getFileMap(); + for (Map.Entry entity : fileMap.entrySet()) { + MultipartFile file = entity.getValue();// 获取上传文件对象 + ImportParams params = new ImportParams(); + params.setTitleRows(2); + params.setHeadRows(1); + params.setNeedSave(true); + try { + List<${entityName}Page> list = ExcelImportUtil.importExcel(file.getInputStream(), ${entityName}Page.class, params); + for (${entityName}Page page : list) { + ${entityName} po = new ${entityName}(); + BeanUtils.copyProperties(page, po); + ${entityName?uncap_first}Service.saveMain(po, <#list subTables as sub>page.get${sub.entityName}List()<#if sub_has_next>,); + } + return Result.OK("文件导入成功!数据行数:" + list.size()); + } catch (Exception e) { + log.error(e.getMessage(),e); + return Result.error("文件导入失败:"+e.getMessage()); + } finally { + try { + file.getInputStream().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return Result.OK("文件导入失败!"); + } +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai new file mode 100644 index 0000000..4677085 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/${entityName}.javai @@ -0,0 +1,42 @@ +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import java.util.Date; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${tableName}") +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#list originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + + @Schema(description = "${po.filedComment}") + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai new file mode 100644 index 0000000..64c0c6b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/entity/[1-n]Entity.javai @@ -0,0 +1,52 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}.java +package ${bussiPackage}.${entityPackage}.entity; + +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import org.jeecgframework.poi.excel.annotation.Excel; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@TableName("${subTab.tableName}") +@Schema(description="${tableVo.ftlDescription}") +public class ${subTab.entityName} implements Serializable { + private static final long serialVersionUID = 1L; + + <#list subTab.originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + @TableId(type = IdType.ASSIGN_ID) + <#else> + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + <#if !subTab.foreignKeys?seq_contains(po.fieldName?cap_first)> + @Excel(name = "${po.filedComment}", width = 15) + + + + @Schema(description = "${po.filedComment}") + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai new file mode 100644 index 0000000..c31b9bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/${entityName}Mapper.javai @@ -0,0 +1,17 @@ +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${entityName}Mapper extends BaseMapper<${entityName}> { + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai new file mode 100644 index 0000000..9585548 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/[1-n]Mapper.javai @@ -0,0 +1,21 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Mapper.java +package ${bussiPackage}.${entityPackage}.mapper; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface ${subTab.entityName}Mapper extends BaseMapper<${subTab.entityName}> { + + public boolean deleteByMainId(String mainId); + + public List<${subTab.entityName}> selectByMainId(String mainId); +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml new file mode 100644 index 0000000..16f3d65 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/xml/${entityName}Mapper.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml new file mode 100644 index 0000000..708ae0b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/mapper/xml/[1-n]Mapper.xml @@ -0,0 +1,36 @@ +<#list subTables as subTab> +<#assign originalForeignKeys = subTab.originalForeignKeys> +#segment#${subTab.entityName}Mapper.xml + + + + + + DELETE + FROM ${subTab.tableName} + WHERE + <#list originalForeignKeys as key> + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + ${key} = ${r'#'}{${primaryKeyField}} <#rt/> + <#else> + ${key} = ${r'#'}{${key}} <#rt/> + + <#if key_has_next>AND + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai new file mode 100644 index 0000000..e7d9914 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/I${entityName}Service.javai @@ -0,0 +1,42 @@ +package ${bussiPackage}.${entityPackage}.service; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${entityName}Service extends IService<${entityName}> { + + /** + * 添加一对多 + * + */ + public void saveMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) ; + + /** + * 修改一对多 + * + */ + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,); + + /** + * 删除一对多 + */ + public void delMain (String id); + + /** + * 批量删除一对多 + */ + public void delBatchMain (Collection idList); + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai new file mode 100644 index 0000000..0f85cb3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/[1-n]Service.javai @@ -0,0 +1,19 @@ +<#list subTables as subTab> +#segment#I${subTab.entityName}Service.java +package ${bussiPackage}.${entityPackage}.service; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import com.baomidou.mybatisplus.extension.service.IService; +import java.util.List; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +public interface I${subTab.entityName}Service extends IService<${subTab.entityName}> { + + public List<${subTab.entityName}> selectByMainId(String mainId); +} + diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai new file mode 100644 index 0000000..1fd8198 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/impl/${entityName}ServiceImpl.javai @@ -0,0 +1,101 @@ +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.mapper.${sub.entityName}Mapper; + +import ${bussiPackage}.${entityPackage}.mapper.${entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${entityName}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.io.Serializable; +import java.util.List; +import java.util.Collection; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Mapper, ${entityName}> implements I${entityName}Service { + + @Autowired + private ${entityName}Mapper ${entityName?uncap_first}Mapper; + <#list subTables as sub> + @Autowired + private ${sub.entityName}Mapper ${sub.entityName?uncap_first}Mapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMain(${entityName} ${entityName?uncap_first}, <#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.insert(${entityName?uncap_first}); + <#list subTables as sub> + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMain(${entityName} ${entityName?uncap_first},<#list subTables as sub>List<${sub.entityName}> ${sub.entityName?uncap_first}List<#if sub_has_next>,) { + ${entityName?uncap_first}Mapper.updateById(${entityName?uncap_first}); + + //1.先删除子表数据 + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(${entityName?uncap_first}.getId()); + + + //2.子表数据重新插入 + <#list subTables as sub> + for(${sub.entityName} entity:${sub.entityName?uncap_first}List) { + <#list sub.foreignKeys as key> + //外键设置 + <#if key?lower_case?index_of("${primaryKeyField}")!=-1> + entity.set${key?cap_first}(${entityName?uncap_first}.get${primaryKeyField?cap_first}()); + <#else> + entity.set${key?cap_first}(${entityName?uncap_first}.get${key}()); + + + ${sub.entityName?uncap_first}Mapper.insert(entity); + } + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delMain(String id) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delBatchMain(Collection idList) { + for(Serializable id:idList) { + <#list subTables as sub> + ${sub.entityName?uncap_first}Mapper.deleteByMainId(id.toString()); + + ${entityName?uncap_first}Mapper.deleteById(id); + } + } + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai new file mode 100644 index 0000000..0ce41d3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/service/impl/[1-n]ServiceImpl.javai @@ -0,0 +1,30 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}ServiceImpl.java +package ${bussiPackage}.${entityPackage}.service.impl; + +import ${bussiPackage}.${entityPackage}.entity.${subTab.entityName}; +import ${bussiPackage}.${entityPackage}.mapper.${subTab.entityName}Mapper; +import ${bussiPackage}.${entityPackage}.service.I${subTab.entityName}Service; +import org.springframework.stereotype.Service; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @Description: ${subTab.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Service +public class ${subTab.entityName}ServiceImpl extends ServiceImpl<${subTab.entityName}Mapper, ${subTab.entityName}> implements I${subTab.entityName}Service { + + @Autowired + private ${subTab.entityName}Mapper ${subTab.entityName?uncap_first}Mapper; + + @Override + public List<${subTab.entityName}> selectByMainId(String mainId) { + return ${subTab.entityName?uncap_first}Mapper.selectByMainId(mainId); + } +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai new file mode 100644 index 0000000..546b1f8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vo/${entityName}Page.javai @@ -0,0 +1,52 @@ +package ${bussiPackage}.${entityPackage}.vo; + +import java.util.List; +import ${bussiPackage}.${entityPackage}.entity.${entityName}; +<#list subTables as sub> +import ${bussiPackage}.${entityPackage}.entity.${sub.entityName}; + +import lombok.Data; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.jeecgframework.poi.excel.annotation.ExcelCollection; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.springframework.format.annotation.DateTimeFormat; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Description: ${tableVo.ftlDescription} + * @Author: jeecg-boot + * @Date: ${.now?string["yyyy-MM-dd"]} + * @Version: V1.0 + */ +@Data +@Schema(description="${tableVo.ftlDescription}") +public class ${entityName}Page { + + <#list originalColumns as po> + /**${po.filedComment}*/ + <#if po.fieldName == primaryKeyField> + <#else> + <#if po.fieldType =='java.util.Date'> + <#if po.fieldDbType =='date'> + @Excel(name = "${po.filedComment}", width = 15, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern="yyyy-MM-dd") + <#elseif po.fieldDbType =='datetime'> + @Excel(name = "${po.filedComment}", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") + + <#else> + @Excel(name = "${po.filedComment}", width = 15) + + + private <#if po.fieldType=='java.sql.Blob'>byte[]<#else>${po.fieldType} ${po.fieldName}; + + + <#list subTables as sub> + @ExcelCollection(name="${sub.ftlDescription}") + @Schema(description = "${sub.ftlDescription}") + private List<${sub.entityName}> ${sub.entityName?uncap_first}List; + + +} diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei new file mode 100644 index 0000000..a07b236 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/${entityName}List.vuei @@ -0,0 +1,225 @@ + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/[1-n]List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/[1-n]List.vuei new file mode 100644 index 0000000..f4b829c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/[1-n]List.vuei @@ -0,0 +1,144 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}List.vue + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..1069111 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/modules/${entityName}Modal.vuei @@ -0,0 +1,168 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Modal.vuei new file mode 100644 index 0000000..5970466 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue/modules/[1-n]Modal.vuei @@ -0,0 +1,184 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Modal.vue + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei new file mode 100644 index 0000000..730def8 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}List.vuei @@ -0,0 +1,170 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi new file mode 100644 index 0000000..c8511d0 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__api.tsi @@ -0,0 +1,117 @@ +import {defHttp} from '/@/utils/http/axios'; +import {Modal} from 'ant-design-vue'; + +enum Api { + list = '/${entityPackage}/${entityName?uncap_first}/list', + save='/${entityPackage}/${entityName?uncap_first}/add', + edit='/${entityPackage}/${entityName?uncap_first}/edit', + deleteOne = '/${entityPackage}/${entityName?uncap_first}/delete', + deleteBatch = '/${entityPackage}/${entityName?uncap_first}/deleteBatch', + importExcel = '/${entityPackage}/${entityName?uncap_first}/importExcel', + exportXls = '/${entityPackage}/${entityName?uncap_first}/exportXls', +<#list subTables as sub><#rt/> + ${sub.entityName?uncap_first}List = '/${entityPackage}/${entityName?uncap_first}/list${sub.entityName}ByMainId', + ${sub.entityName?uncap_first}Save='/${entityPackage}/${entityName?uncap_first}/add${sub.entityName}', + ${sub.entityName?uncap_first}Edit='/${entityPackage}/${entityName?uncap_first}/edit${sub.entityName}', + ${sub.entityName?uncap_first}Delete = '/${entityPackage}/${entityName?uncap_first}/delete${sub.entityName}', + ${sub.entityName?uncap_first}DeleteBatch = '/${entityPackage}/${entityName?uncap_first}/deleteBatch${sub.entityName}', + +} +/** + * 导出api + * @param params + */ +export const getExportUrl = Api.exportXls; + +/** + * 导入api + */ +export const getImportUrl = Api.importExcel; + +/** + * 列表接口 + * @param params + */ +export const list = (params) => + defHttp.get({url: Api.list, params}); + +/** + * 删除单个 + * @param params + * @param handleSuccess + */ +export const deleteOne = (params,handleSuccess) => { + return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + * @param handleSuccess + */ +export const batchDelete = (params, handleSuccess) => { + Modal.confirm({ + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + * @param isUpdate 是否是更新数据 + */ +export const saveOrUpdate = (params, isUpdate) => { + let url = isUpdate ? Api.edit : Api.save; + return defHttp.post({url: url, params}); +} + +<#list subTables as sub><#rt/> +/** + * 列表接口 + * @param params + */ +export const ${sub.entityName?uncap_first}List = (params) => + defHttp.get({url: Api.${sub.entityName?uncap_first}List, params}); + +/** + * 删除单个 + */ +export const ${sub.entityName?uncap_first}Delete = (params,handleSuccess) => { + return defHttp.delete({url: Api.${sub.entityName?uncap_first}Delete, params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); +} +/** + * 批量删除 + * @param params + */ +export const ${sub.entityName?uncap_first}DeleteBatch = (params, handleSuccess) => { + Modal.confirm({ + title: '确认删除', + content: '是否删除选中数据', + okText: '确认', + cancelText: '取消', + onOk: () => { + return defHttp.delete({url: Api. ${sub.entityName?uncap_first}DeleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { + handleSuccess(); + }); + } + }); +} +/** + * 保存或者更新 + * @param params + */ +export const ${sub.entityName?uncap_first}Save = (params, isUpdate) => { + let url = isUpdate ? Api.${sub.entityName?uncap_first}Edit : Api.${sub.entityName?uncap_first}Save; + return defHttp.post({url: url, params}); +} + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi new file mode 100644 index 0000000..8d8ac1c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/${entityName}__data.tsi @@ -0,0 +1,124 @@ +import {BasicColumn} from '/@/components/Table'; +import {FormSchema} from '/@/components/Table'; +import { rules} from '/@/utils/helper/validator'; +import { render } from '/@/utils/common/renderUtils'; +//列表数据 +export const columns: BasicColumn[] = [ + <#list columns as po> + <#if po.fieldName !='id'> + { + title: '${po.filedComment}', + align:"center", + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + return !text?"":(text.length>10?text.substr(0,10):text) + }, + <#else> + dataIndex: '${po.fieldName}' + + }, + + +]; +//查询数据 +export const searchFormSchema: FormSchema[] = [ +<#list columns as po> +<#if po.fieldName !='id' && po_index<= tableVo.searchFieldNum> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'TimePicker' + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber' + <#else> + component: 'Input' + + }, + + +]; + +export const formSchema: FormSchema[] = [ + // TODO 主键隐藏字段,目前写死为ID + {label: '', field: 'id', component: 'Input', show: false}, +<#list columns as po><#rt/> + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker' + <#elseif po.fieldType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime: true, + valueFormat: 'YYYY-MM-DD hh:mm:ss', + }, + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber', + <#else> + component: 'Input', + + <#if po.fieldName =='id'><#rt/> + show:false, + + }, + +]; + +//子表表格配置 +<#list subTables as sub> +//列表数据 +export const ${sub.entityName?uncap_first}Columns: BasicColumn[] = [ + <#list sub.colums as po><#rt/> + <#if po.fieldName !='id' && sub.foreignKeys[0]?uncap_first != po.fieldName> + { + title: '${po.filedComment}', + align:"center", + <#if po.classType=='date'> + dataIndex: '${po.fieldName}', + customRender:({text}) =>{ + return !text?"":(text.length>10?text.substr(0,10):text) + }, + <#else> + dataIndex: '${po.fieldName}', + + }, + + +]; + +export const ${sub.entityName?uncap_first}FormSchema: FormSchema[] = [ + // TODO 主键隐藏字段,目前写死为ID + {label: '', field: 'id', component: 'Input', show: false}, +<#-- 循环子表的列 开始 --> +<#list sub.colums as po><#rt/> +<#if po.filedComment !='外键' > + { + label: '${po.filedComment}', + field: '${po.fieldName}', + <#if po.fieldType =='date'> + component: 'DatePicker', + <#elseif po.fieldType =='datetime'> + component: 'DatePicker', + componentProps: { + showTime: true, + valueFormat: 'YYYY-MM-DD hh:mm:ss', + }, + <#elseif "int,decimal,double,"?contains(po.fieldType)> + component: 'InputNumber', + <#else> + component: 'Input', + + <#if po.fieldName =='id'><#rt/> + show:false, + + }, + + +<#-- 循环子表的列 结束 --> + ] + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/[1-n]List.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/[1-n]List.vuei new file mode 100644 index 0000000..180d619 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/[1-n]List.vuei @@ -0,0 +1,147 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}List.vue + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei new file mode 100644 index 0000000..668567a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/modules/${entityName}Modal.vuei @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/modules/[1-n]Modal.vuei b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/modules/[1-n]Modal.vuei new file mode 100644 index 0000000..da7668d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/jeecg/code-template/onetomany2/java/${bussiPackage}/${entityPackage}/vue3/modules/[1-n]Modal.vuei @@ -0,0 +1,64 @@ +<#list subTables as subTab> +#segment#${subTab.entityName}Modal.vue + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/static/demo1.html b/test-module-system/test-system-biz/src/main/resources/static/demo1.html new file mode 100644 index 0000000..f984869 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/demo1.html @@ -0,0 +1 @@ +demo1 \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/LICENSE b/test-module-system/test-system-biz/src/main/resources/static/generic/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/build/pdf.js b/test-module-system/test-system-biz/src/main/resources/static/generic/build/pdf.js new file mode 100644 index 0000000..42fafd3 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/build/pdf.js @@ -0,0 +1,8021 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jshint globalstrict: false */ +/* globals PDFJS */ + +// Initializing PDFJS global object (if still undefined) +if (typeof PDFJS === 'undefined') { + (typeof window !== 'undefined' ? window : this).PDFJS = {}; +} + +PDFJS.version = '1.1.159'; +PDFJS.build = '82536f8'; + +(function pdfjsWrapper() { + // Use strict in our context only - users might not want it + 'use strict'; + +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* globals Cmd, ColorSpace, Dict, MozBlobBuilder, Name, PDFJS, Ref, URL, + Promise */ + +'use strict'; + +var globalScope = (typeof window === 'undefined') ? this : window; + +var isWorker = (typeof window === 'undefined'); + +var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; + +var TextRenderingMode = { + FILL: 0, + STROKE: 1, + FILL_STROKE: 2, + INVISIBLE: 3, + FILL_ADD_TO_PATH: 4, + STROKE_ADD_TO_PATH: 5, + FILL_STROKE_ADD_TO_PATH: 6, + ADD_TO_PATH: 7, + FILL_STROKE_MASK: 3, + ADD_TO_PATH_FLAG: 4 +}; + +var ImageKind = { + GRAYSCALE_1BPP: 1, + RGB_24BPP: 2, + RGBA_32BPP: 3 +}; + +var AnnotationType = { + WIDGET: 1, + TEXT: 2, + LINK: 3 +}; + +var StreamType = { + UNKNOWN: 0, + FLATE: 1, + LZW: 2, + DCT: 3, + JPX: 4, + JBIG: 5, + A85: 6, + AHX: 7, + CCF: 8, + RL: 9 +}; + +var FontType = { + UNKNOWN: 0, + TYPE1: 1, + TYPE1C: 2, + CIDFONTTYPE0: 3, + CIDFONTTYPE0C: 4, + TRUETYPE: 5, + CIDFONTTYPE2: 6, + TYPE3: 7, + OPENTYPE: 8, + TYPE0: 9, + MMTYPE1: 10 +}; + +// The global PDFJS object exposes the API +// In production, it will be declared outside a global wrapper +// In development, it will be declared here +if (!globalScope.PDFJS) { + globalScope.PDFJS = {}; +} + +globalScope.PDFJS.pdfBug = false; + +PDFJS.VERBOSITY_LEVELS = { + errors: 0, + warnings: 1, + infos: 5 +}; + +// All the possible operations for an operator list. +var OPS = PDFJS.OPS = { + // Intentionally start from 1 so it is easy to spot bad operators that will be + // 0's. + dependency: 1, + setLineWidth: 2, + setLineCap: 3, + setLineJoin: 4, + setMiterLimit: 5, + setDash: 6, + setRenderingIntent: 7, + setFlatness: 8, + setGState: 9, + save: 10, + restore: 11, + transform: 12, + moveTo: 13, + lineTo: 14, + curveTo: 15, + curveTo2: 16, + curveTo3: 17, + closePath: 18, + rectangle: 19, + stroke: 20, + closeStroke: 21, + fill: 22, + eoFill: 23, + fillStroke: 24, + eoFillStroke: 25, + closeFillStroke: 26, + closeEOFillStroke: 27, + endPath: 28, + clip: 29, + eoClip: 30, + beginText: 31, + endText: 32, + setCharSpacing: 33, + setWordSpacing: 34, + setHScale: 35, + setLeading: 36, + setFont: 37, + setTextRenderingMode: 38, + setTextRise: 39, + moveText: 40, + setLeadingMoveText: 41, + setTextMatrix: 42, + nextLine: 43, + showText: 44, + showSpacedText: 45, + nextLineShowText: 46, + nextLineSetSpacingShowText: 47, + setCharWidth: 48, + setCharWidthAndBounds: 49, + setStrokeColorSpace: 50, + setFillColorSpace: 51, + setStrokeColor: 52, + setStrokeColorN: 53, + setFillColor: 54, + setFillColorN: 55, + setStrokeGray: 56, + setFillGray: 57, + setStrokeRGBColor: 58, + setFillRGBColor: 59, + setStrokeCMYKColor: 60, + setFillCMYKColor: 61, + shadingFill: 62, + beginInlineImage: 63, + beginImageData: 64, + endInlineImage: 65, + paintXObject: 66, + markPoint: 67, + markPointProps: 68, + beginMarkedContent: 69, + beginMarkedContentProps: 70, + endMarkedContent: 71, + beginCompat: 72, + endCompat: 73, + paintFormXObjectBegin: 74, + paintFormXObjectEnd: 75, + beginGroup: 76, + endGroup: 77, + beginAnnotations: 78, + endAnnotations: 79, + beginAnnotation: 80, + endAnnotation: 81, + paintJpegXObject: 82, + paintImageMaskXObject: 83, + paintImageMaskXObjectGroup: 84, + paintImageXObject: 85, + paintInlineImageXObject: 86, + paintInlineImageXObjectGroup: 87, + paintImageXObjectRepeat: 88, + paintImageMaskXObjectRepeat: 89, + paintSolidColorImageMask: 90, + constructPath: 91 +}; + +// A notice for devs. These are good for things that are helpful to devs, such +// as warning that Workers were disabled, which is important to devs but not +// end users. +function info(msg) { + if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.infos) { + console.log('Info: ' + msg); + } +} + +// Non-fatal warnings. +function warn(msg) { + if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.warnings) { + console.log('Warning: ' + msg); + } +} + +// Fatal errors that should trigger the fallback UI and halt execution by +// throwing an exception. +function error(msg) { + if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.errors) { + console.log('Error: ' + msg); + console.log(backtrace()); + } + UnsupportedManager.notify(UNSUPPORTED_FEATURES.unknown); + throw new Error(msg); +} + +function backtrace() { + try { + throw new Error(); + } catch (e) { + return e.stack ? e.stack.split('\n').slice(2).join('\n') : ''; + } +} + +function assert(cond, msg) { + if (!cond) { + error(msg); + } +} + +var UNSUPPORTED_FEATURES = PDFJS.UNSUPPORTED_FEATURES = { + unknown: 'unknown', + forms: 'forms', + javaScript: 'javaScript', + smask: 'smask', + shadingPattern: 'shadingPattern', + font: 'font' +}; + +var UnsupportedManager = PDFJS.UnsupportedManager = + (function UnsupportedManagerClosure() { + var listeners = []; + return { + listen: function (cb) { + listeners.push(cb); + }, + notify: function (featureId) { + warn('Unsupported feature "' + featureId + '"'); + for (var i = 0, ii = listeners.length; i < ii; i++) { + listeners[i](featureId); + } + } + }; +})(); + +// Combines two URLs. The baseUrl shall be absolute URL. If the url is an +// absolute URL, it will be returned as is. +function combineUrl(baseUrl, url) { + if (!url) { + return baseUrl; + } + if (/^[a-z][a-z0-9+\-.]*:/i.test(url)) { + return url; + } + var i; + if (url.charAt(0) === '/') { + // absolute path + i = baseUrl.indexOf('://'); + if (url.charAt(1) === '/') { + ++i; + } else { + i = baseUrl.indexOf('/', i + 3); + } + return baseUrl.substring(0, i) + url; + } else { + // relative path + var pathLength = baseUrl.length; + i = baseUrl.lastIndexOf('#'); + pathLength = i >= 0 ? i : pathLength; + i = baseUrl.lastIndexOf('?', pathLength); + pathLength = i >= 0 ? i : pathLength; + var prefixLength = baseUrl.lastIndexOf('/', pathLength); + return baseUrl.substring(0, prefixLength + 1) + url; + } +} + +// Validates if URL is safe and allowed, e.g. to avoid XSS. +function isValidUrl(url, allowRelative) { + if (!url) { + return false; + } + // RFC 3986 (http://tools.ietf.org/html/rfc3986#section-3.1) + // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + var protocol = /^[a-z][a-z0-9+\-.]*(?=:)/i.exec(url); + if (!protocol) { + return allowRelative; + } + protocol = protocol[0].toLowerCase(); + switch (protocol) { + case 'http': + case 'https': + case 'ftp': + case 'mailto': + case 'tel': + return true; + default: + return false; + } +} +PDFJS.isValidUrl = isValidUrl; + +function shadow(obj, prop, value) { + Object.defineProperty(obj, prop, { value: value, + enumerable: true, + configurable: true, + writable: false }); + return value; +} +PDFJS.shadow = shadow; + +var PasswordResponses = PDFJS.PasswordResponses = { + NEED_PASSWORD: 1, + INCORRECT_PASSWORD: 2 +}; + +var PasswordException = (function PasswordExceptionClosure() { + function PasswordException(msg, code) { + this.name = 'PasswordException'; + this.message = msg; + this.code = code; + } + + PasswordException.prototype = new Error(); + PasswordException.constructor = PasswordException; + + return PasswordException; +})(); +PDFJS.PasswordException = PasswordException; + +var UnknownErrorException = (function UnknownErrorExceptionClosure() { + function UnknownErrorException(msg, details) { + this.name = 'UnknownErrorException'; + this.message = msg; + this.details = details; + } + + UnknownErrorException.prototype = new Error(); + UnknownErrorException.constructor = UnknownErrorException; + + return UnknownErrorException; +})(); +PDFJS.UnknownErrorException = UnknownErrorException; + +var InvalidPDFException = (function InvalidPDFExceptionClosure() { + function InvalidPDFException(msg) { + this.name = 'InvalidPDFException'; + this.message = msg; + } + + InvalidPDFException.prototype = new Error(); + InvalidPDFException.constructor = InvalidPDFException; + + return InvalidPDFException; +})(); +PDFJS.InvalidPDFException = InvalidPDFException; + +var MissingPDFException = (function MissingPDFExceptionClosure() { + function MissingPDFException(msg) { + this.name = 'MissingPDFException'; + this.message = msg; + } + + MissingPDFException.prototype = new Error(); + MissingPDFException.constructor = MissingPDFException; + + return MissingPDFException; +})(); +PDFJS.MissingPDFException = MissingPDFException; + +var UnexpectedResponseException = + (function UnexpectedResponseExceptionClosure() { + function UnexpectedResponseException(msg, status) { + this.name = 'UnexpectedResponseException'; + this.message = msg; + this.status = status; + } + + UnexpectedResponseException.prototype = new Error(); + UnexpectedResponseException.constructor = UnexpectedResponseException; + + return UnexpectedResponseException; +})(); +PDFJS.UnexpectedResponseException = UnexpectedResponseException; + +var NotImplementedException = (function NotImplementedExceptionClosure() { + function NotImplementedException(msg) { + this.message = msg; + } + + NotImplementedException.prototype = new Error(); + NotImplementedException.prototype.name = 'NotImplementedException'; + NotImplementedException.constructor = NotImplementedException; + + return NotImplementedException; +})(); + +var MissingDataException = (function MissingDataExceptionClosure() { + function MissingDataException(begin, end) { + this.begin = begin; + this.end = end; + this.message = 'Missing data [' + begin + ', ' + end + ')'; + } + + MissingDataException.prototype = new Error(); + MissingDataException.prototype.name = 'MissingDataException'; + MissingDataException.constructor = MissingDataException; + + return MissingDataException; +})(); + +var XRefParseException = (function XRefParseExceptionClosure() { + function XRefParseException(msg) { + this.message = msg; + } + + XRefParseException.prototype = new Error(); + XRefParseException.prototype.name = 'XRefParseException'; + XRefParseException.constructor = XRefParseException; + + return XRefParseException; +})(); + + +function bytesToString(bytes) { + assert(bytes !== null && typeof bytes === 'object' && + bytes.length !== undefined, 'Invalid argument for bytesToString'); + var length = bytes.length; + var MAX_ARGUMENT_COUNT = 8192; + if (length < MAX_ARGUMENT_COUNT) { + return String.fromCharCode.apply(null, bytes); + } + var strBuf = []; + for (var i = 0; i < length; i += MAX_ARGUMENT_COUNT) { + var chunkEnd = Math.min(i + MAX_ARGUMENT_COUNT, length); + var chunk = bytes.subarray(i, chunkEnd); + strBuf.push(String.fromCharCode.apply(null, chunk)); + } + return strBuf.join(''); +} + +function stringToBytes(str) { + assert(typeof str === 'string', 'Invalid argument for stringToBytes'); + var length = str.length; + var bytes = new Uint8Array(length); + for (var i = 0; i < length; ++i) { + bytes[i] = str.charCodeAt(i) & 0xFF; + } + return bytes; +} + +function string32(value) { + return String.fromCharCode((value >> 24) & 0xff, (value >> 16) & 0xff, + (value >> 8) & 0xff, value & 0xff); +} + +function log2(x) { + var n = 1, i = 0; + while (x > n) { + n <<= 1; + i++; + } + return i; +} + +function readInt8(data, start) { + return (data[start] << 24) >> 24; +} + +function readUint16(data, offset) { + return (data[offset] << 8) | data[offset + 1]; +} + +function readUint32(data, offset) { + return ((data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]) >>> 0; +} + +// Lazy test the endianness of the platform +// NOTE: This will be 'true' for simulated TypedArrays +function isLittleEndian() { + var buffer8 = new Uint8Array(2); + buffer8[0] = 1; + var buffer16 = new Uint16Array(buffer8.buffer); + return (buffer16[0] === 1); +} + +Object.defineProperty(PDFJS, 'isLittleEndian', { + configurable: true, + get: function PDFJS_isLittleEndian() { + return shadow(PDFJS, 'isLittleEndian', isLittleEndian()); + } +}); + + // Lazy test if the userAgant support CanvasTypedArrays +function hasCanvasTypedArrays() { + var canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + var ctx = canvas.getContext('2d'); + var imageData = ctx.createImageData(1, 1); + return (typeof imageData.data.buffer !== 'undefined'); +} + +Object.defineProperty(PDFJS, 'hasCanvasTypedArrays', { + configurable: true, + get: function PDFJS_hasCanvasTypedArrays() { + return shadow(PDFJS, 'hasCanvasTypedArrays', hasCanvasTypedArrays()); + } +}); + +var Uint32ArrayView = (function Uint32ArrayViewClosure() { + + function Uint32ArrayView(buffer, length) { + this.buffer = buffer; + this.byteLength = buffer.length; + this.length = length === undefined ? (this.byteLength >> 2) : length; + ensureUint32ArrayViewProps(this.length); + } + Uint32ArrayView.prototype = Object.create(null); + + var uint32ArrayViewSetters = 0; + function createUint32ArrayProp(index) { + return { + get: function () { + var buffer = this.buffer, offset = index << 2; + return (buffer[offset] | (buffer[offset + 1] << 8) | + (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)) >>> 0; + }, + set: function (value) { + var buffer = this.buffer, offset = index << 2; + buffer[offset] = value & 255; + buffer[offset + 1] = (value >> 8) & 255; + buffer[offset + 2] = (value >> 16) & 255; + buffer[offset + 3] = (value >>> 24) & 255; + } + }; + } + + function ensureUint32ArrayViewProps(length) { + while (uint32ArrayViewSetters < length) { + Object.defineProperty(Uint32ArrayView.prototype, + uint32ArrayViewSetters, + createUint32ArrayProp(uint32ArrayViewSetters)); + uint32ArrayViewSetters++; + } + } + + return Uint32ArrayView; +})(); + +var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; + +var Util = PDFJS.Util = (function UtilClosure() { + function Util() {} + + var rgbBuf = ['rgb(', 0, ',', 0, ',', 0, ')']; + + // makeCssRgb() can be called thousands of times. Using |rgbBuf| avoids + // creating many intermediate strings. + Util.makeCssRgb = function Util_makeCssRgb(r, g, b) { + rgbBuf[1] = r; + rgbBuf[3] = g; + rgbBuf[5] = b; + return rgbBuf.join(''); + }; + + // Concatenates two transformation matrices together and returns the result. + Util.transform = function Util_transform(m1, m2) { + return [ + m1[0] * m2[0] + m1[2] * m2[1], + m1[1] * m2[0] + m1[3] * m2[1], + m1[0] * m2[2] + m1[2] * m2[3], + m1[1] * m2[2] + m1[3] * m2[3], + m1[0] * m2[4] + m1[2] * m2[5] + m1[4], + m1[1] * m2[4] + m1[3] * m2[5] + m1[5] + ]; + }; + + // For 2d affine transforms + Util.applyTransform = function Util_applyTransform(p, m) { + var xt = p[0] * m[0] + p[1] * m[2] + m[4]; + var yt = p[0] * m[1] + p[1] * m[3] + m[5]; + return [xt, yt]; + }; + + Util.applyInverseTransform = function Util_applyInverseTransform(p, m) { + var d = m[0] * m[3] - m[1] * m[2]; + var xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d; + var yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d; + return [xt, yt]; + }; + + // Applies the transform to the rectangle and finds the minimum axially + // aligned bounding box. + Util.getAxialAlignedBoundingBox = + function Util_getAxialAlignedBoundingBox(r, m) { + + var p1 = Util.applyTransform(r, m); + var p2 = Util.applyTransform(r.slice(2, 4), m); + var p3 = Util.applyTransform([r[0], r[3]], m); + var p4 = Util.applyTransform([r[2], r[1]], m); + return [ + Math.min(p1[0], p2[0], p3[0], p4[0]), + Math.min(p1[1], p2[1], p3[1], p4[1]), + Math.max(p1[0], p2[0], p3[0], p4[0]), + Math.max(p1[1], p2[1], p3[1], p4[1]) + ]; + }; + + Util.inverseTransform = function Util_inverseTransform(m) { + var d = m[0] * m[3] - m[1] * m[2]; + return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d, + (m[2] * m[5] - m[4] * m[3]) / d, (m[4] * m[1] - m[5] * m[0]) / d]; + }; + + // Apply a generic 3d matrix M on a 3-vector v: + // | a b c | | X | + // | d e f | x | Y | + // | g h i | | Z | + // M is assumed to be serialized as [a,b,c,d,e,f,g,h,i], + // with v as [X,Y,Z] + Util.apply3dTransform = function Util_apply3dTransform(m, v) { + return [ + m[0] * v[0] + m[1] * v[1] + m[2] * v[2], + m[3] * v[0] + m[4] * v[1] + m[5] * v[2], + m[6] * v[0] + m[7] * v[1] + m[8] * v[2] + ]; + }; + + // This calculation uses Singular Value Decomposition. + // The SVD can be represented with formula A = USV. We are interested in the + // matrix S here because it represents the scale values. + Util.singularValueDecompose2dScale = + function Util_singularValueDecompose2dScale(m) { + + var transpose = [m[0], m[2], m[1], m[3]]; + + // Multiply matrix m with its transpose. + var a = m[0] * transpose[0] + m[1] * transpose[2]; + var b = m[0] * transpose[1] + m[1] * transpose[3]; + var c = m[2] * transpose[0] + m[3] * transpose[2]; + var d = m[2] * transpose[1] + m[3] * transpose[3]; + + // Solve the second degree polynomial to get roots. + var first = (a + d) / 2; + var second = Math.sqrt((a + d) * (a + d) - 4 * (a * d - c * b)) / 2; + var sx = first + second || 1; + var sy = first - second || 1; + + // Scale values are the square roots of the eigenvalues. + return [Math.sqrt(sx), Math.sqrt(sy)]; + }; + + // Normalize rectangle rect=[x1, y1, x2, y2] so that (x1,y1) < (x2,y2) + // For coordinate systems whose origin lies in the bottom-left, this + // means normalization to (BL,TR) ordering. For systems with origin in the + // top-left, this means (TL,BR) ordering. + Util.normalizeRect = function Util_normalizeRect(rect) { + var r = rect.slice(0); // clone rect + if (rect[0] > rect[2]) { + r[0] = rect[2]; + r[2] = rect[0]; + } + if (rect[1] > rect[3]) { + r[1] = rect[3]; + r[3] = rect[1]; + } + return r; + }; + + // Returns a rectangle [x1, y1, x2, y2] corresponding to the + // intersection of rect1 and rect2. If no intersection, returns 'false' + // The rectangle coordinates of rect1, rect2 should be [x1, y1, x2, y2] + Util.intersect = function Util_intersect(rect1, rect2) { + function compare(a, b) { + return a - b; + } + + // Order points along the axes + var orderedX = [rect1[0], rect1[2], rect2[0], rect2[2]].sort(compare), + orderedY = [rect1[1], rect1[3], rect2[1], rect2[3]].sort(compare), + result = []; + + rect1 = Util.normalizeRect(rect1); + rect2 = Util.normalizeRect(rect2); + + // X: first and second points belong to different rectangles? + if ((orderedX[0] === rect1[0] && orderedX[1] === rect2[0]) || + (orderedX[0] === rect2[0] && orderedX[1] === rect1[0])) { + // Intersection must be between second and third points + result[0] = orderedX[1]; + result[2] = orderedX[2]; + } else { + return false; + } + + // Y: first and second points belong to different rectangles? + if ((orderedY[0] === rect1[1] && orderedY[1] === rect2[1]) || + (orderedY[0] === rect2[1] && orderedY[1] === rect1[1])) { + // Intersection must be between second and third points + result[1] = orderedY[1]; + result[3] = orderedY[2]; + } else { + return false; + } + + return result; + }; + + Util.sign = function Util_sign(num) { + return num < 0 ? -1 : 1; + }; + + Util.appendToArray = function Util_appendToArray(arr1, arr2) { + Array.prototype.push.apply(arr1, arr2); + }; + + Util.prependToArray = function Util_prependToArray(arr1, arr2) { + Array.prototype.unshift.apply(arr1, arr2); + }; + + Util.extendObj = function extendObj(obj1, obj2) { + for (var key in obj2) { + obj1[key] = obj2[key]; + } + }; + + Util.getInheritableProperty = function Util_getInheritableProperty(dict, + name) { + while (dict && !dict.has(name)) { + dict = dict.get('Parent'); + } + if (!dict) { + return null; + } + return dict.get(name); + }; + + Util.inherit = function Util_inherit(sub, base, prototype) { + sub.prototype = Object.create(base.prototype); + sub.prototype.constructor = sub; + for (var prop in prototype) { + sub.prototype[prop] = prototype[prop]; + } + }; + + Util.loadScript = function Util_loadScript(src, callback) { + var script = document.createElement('script'); + var loaded = false; + script.setAttribute('src', src); + if (callback) { + script.onload = function() { + if (!loaded) { + callback(); + } + loaded = true; + }; + } + document.getElementsByTagName('head')[0].appendChild(script); + }; + + return Util; +})(); + +/** + * PDF page viewport created based on scale, rotation and offset. + * @class + * @alias PDFJS.PageViewport + */ +var PageViewport = PDFJS.PageViewport = (function PageViewportClosure() { + /** + * @constructor + * @private + * @param viewBox {Array} xMin, yMin, xMax and yMax coordinates. + * @param scale {number} scale of the viewport. + * @param rotation {number} rotations of the viewport in degrees. + * @param offsetX {number} offset X + * @param offsetY {number} offset Y + * @param dontFlip {boolean} if true, axis Y will not be flipped. + */ + function PageViewport(viewBox, scale, rotation, offsetX, offsetY, dontFlip) { + this.viewBox = viewBox; + this.scale = scale; + this.rotation = rotation; + this.offsetX = offsetX; + this.offsetY = offsetY; + + // creating transform to convert pdf coordinate system to the normal + // canvas like coordinates taking in account scale and rotation + var centerX = (viewBox[2] + viewBox[0]) / 2; + var centerY = (viewBox[3] + viewBox[1]) / 2; + var rotateA, rotateB, rotateC, rotateD; + rotation = rotation % 360; + rotation = rotation < 0 ? rotation + 360 : rotation; + switch (rotation) { + case 180: + rotateA = -1; rotateB = 0; rotateC = 0; rotateD = 1; + break; + case 90: + rotateA = 0; rotateB = 1; rotateC = 1; rotateD = 0; + break; + case 270: + rotateA = 0; rotateB = -1; rotateC = -1; rotateD = 0; + break; + //case 0: + default: + rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1; + break; + } + + if (dontFlip) { + rotateC = -rotateC; rotateD = -rotateD; + } + + var offsetCanvasX, offsetCanvasY; + var width, height; + if (rotateA === 0) { + offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX; + offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY; + width = Math.abs(viewBox[3] - viewBox[1]) * scale; + height = Math.abs(viewBox[2] - viewBox[0]) * scale; + } else { + offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; + offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; + width = Math.abs(viewBox[2] - viewBox[0]) * scale; + height = Math.abs(viewBox[3] - viewBox[1]) * scale; + } + // creating transform for the following operations: + // translate(-centerX, -centerY), rotate and flip vertically, + // scale, and translate(offsetCanvasX, offsetCanvasY) + this.transform = [ + rotateA * scale, + rotateB * scale, + rotateC * scale, + rotateD * scale, + offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, + offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY + ]; + + this.width = width; + this.height = height; + this.fontScale = scale; + } + PageViewport.prototype = /** @lends PDFJS.PageViewport.prototype */ { + /** + * Clones viewport with additional properties. + * @param args {Object} (optional) If specified, may contain the 'scale' or + * 'rotation' properties to override the corresponding properties in + * the cloned viewport. + * @returns {PDFJS.PageViewport} Cloned viewport. + */ + clone: function PageViewPort_clone(args) { + args = args || {}; + var scale = 'scale' in args ? args.scale : this.scale; + var rotation = 'rotation' in args ? args.rotation : this.rotation; + return new PageViewport(this.viewBox.slice(), scale, rotation, + this.offsetX, this.offsetY, args.dontFlip); + }, + /** + * Converts PDF point to the viewport coordinates. For examples, useful for + * converting PDF location into canvas pixel coordinates. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the viewport coordinate space. + * @see {@link convertToPdfPoint} + * @see {@link convertToViewportRectangle} + */ + convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) { + return Util.applyTransform([x, y], this.transform); + }, + /** + * Converts PDF rectangle to the viewport coordinates. + * @param rect {Array} xMin, yMin, xMax and yMax coordinates. + * @returns {Array} Contains corresponding coordinates of the rectangle + * in the viewport coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToViewportRectangle: + function PageViewport_convertToViewportRectangle(rect) { + var tl = Util.applyTransform([rect[0], rect[1]], this.transform); + var br = Util.applyTransform([rect[2], rect[3]], this.transform); + return [tl[0], tl[1], br[0], br[1]]; + }, + /** + * Converts viewport coordinates to the PDF location. For examples, useful + * for converting canvas pixel location into PDF one. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the PDF coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) { + return Util.applyInverseTransform([x, y], this.transform); + } + }; + return PageViewport; +})(); + +var PDFStringTranslateTable = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, + 0x2013, 0x192, 0x2044, 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C, + 0x201D, 0x2018, 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x141, 0x152, 0x160, + 0x178, 0x17D, 0x131, 0x142, 0x153, 0x161, 0x17E, 0, 0x20AC +]; + +function stringToPDFString(str) { + var i, n = str.length, strBuf = []; + if (str[0] === '\xFE' && str[1] === '\xFF') { + // UTF16BE BOM + for (i = 2; i < n; i += 2) { + strBuf.push(String.fromCharCode( + (str.charCodeAt(i) << 8) | str.charCodeAt(i + 1))); + } + } else { + for (i = 0; i < n; ++i) { + var code = PDFStringTranslateTable[str.charCodeAt(i)]; + strBuf.push(code ? String.fromCharCode(code) : str.charAt(i)); + } + } + return strBuf.join(''); +} + +function stringToUTF8String(str) { + return decodeURIComponent(escape(str)); +} + +function utf8StringToString(str) { + return unescape(encodeURIComponent(str)); +} + +function isEmptyObj(obj) { + for (var key in obj) { + return false; + } + return true; +} + +function isBool(v) { + return typeof v === 'boolean'; +} + +function isInt(v) { + return typeof v === 'number' && ((v | 0) === v); +} + +function isNum(v) { + return typeof v === 'number'; +} + +function isString(v) { + return typeof v === 'string'; +} + +function isName(v) { + return v instanceof Name; +} + +function isCmd(v, cmd) { + return v instanceof Cmd && (cmd === undefined || v.cmd === cmd); +} + +function isDict(v, type) { + if (!(v instanceof Dict)) { + return false; + } + if (!type) { + return true; + } + var dictType = v.get('Type'); + return isName(dictType) && dictType.name === type; +} + +function isArray(v) { + return v instanceof Array; +} + +function isStream(v) { + return typeof v === 'object' && v !== null && v.getBytes !== undefined; +} + +function isArrayBuffer(v) { + return typeof v === 'object' && v !== null && v.byteLength !== undefined; +} + +function isRef(v) { + return v instanceof Ref; +} + +/** + * Promise Capability object. + * + * @typedef {Object} PromiseCapability + * @property {Promise} promise - A promise object. + * @property {function} resolve - Fullfills the promise. + * @property {function} reject - Rejects the promise. + */ + +/** + * Creates a promise capability object. + * @alias PDFJS.createPromiseCapability + * + * @return {PromiseCapability} A capability object contains: + * - a Promise, resolve and reject methods. + */ +function createPromiseCapability() { + var capability = {}; + capability.promise = new Promise(function (resolve, reject) { + capability.resolve = resolve; + capability.reject = reject; + }); + return capability; +} + +PDFJS.createPromiseCapability = createPromiseCapability; + +/** + * Polyfill for Promises: + * The following promise implementation tries to generally implement the + * Promise/A+ spec. Some notable differences from other promise libaries are: + * - There currently isn't a seperate deferred and promise object. + * - Unhandled rejections eventually show an error if they aren't handled. + * + * Based off of the work in: + * https://bugzilla.mozilla.org/show_bug.cgi?id=810490 + */ +(function PromiseClosure() { + if (globalScope.Promise) { + // Promises existing in the DOM/Worker, checking presence of all/resolve + if (typeof globalScope.Promise.all !== 'function') { + globalScope.Promise.all = function (iterable) { + var count = 0, results = [], resolve, reject; + var promise = new globalScope.Promise(function (resolve_, reject_) { + resolve = resolve_; + reject = reject_; + }); + iterable.forEach(function (p, i) { + count++; + p.then(function (result) { + results[i] = result; + count--; + if (count === 0) { + resolve(results); + } + }, reject); + }); + if (count === 0) { + resolve(results); + } + return promise; + }; + } + if (typeof globalScope.Promise.resolve !== 'function') { + globalScope.Promise.resolve = function (value) { + return new globalScope.Promise(function (resolve) { resolve(value); }); + }; + } + if (typeof globalScope.Promise.reject !== 'function') { + globalScope.Promise.reject = function (reason) { + return new globalScope.Promise(function (resolve, reject) { + reject(reason); + }); + }; + } + if (typeof globalScope.Promise.prototype.catch !== 'function') { + globalScope.Promise.prototype.catch = function (onReject) { + return globalScope.Promise.prototype.then(undefined, onReject); + }; + } + return; + } + var STATUS_PENDING = 0; + var STATUS_RESOLVED = 1; + var STATUS_REJECTED = 2; + + // In an attempt to avoid silent exceptions, unhandled rejections are + // tracked and if they aren't handled in a certain amount of time an + // error is logged. + var REJECTION_TIMEOUT = 500; + + var HandlerManager = { + handlers: [], + running: false, + unhandledRejections: [], + pendingRejectionCheck: false, + + scheduleHandlers: function scheduleHandlers(promise) { + if (promise._status === STATUS_PENDING) { + return; + } + + this.handlers = this.handlers.concat(promise._handlers); + promise._handlers = []; + + if (this.running) { + return; + } + this.running = true; + + setTimeout(this.runHandlers.bind(this), 0); + }, + + runHandlers: function runHandlers() { + var RUN_TIMEOUT = 1; // ms + var timeoutAt = Date.now() + RUN_TIMEOUT; + while (this.handlers.length > 0) { + var handler = this.handlers.shift(); + + var nextStatus = handler.thisPromise._status; + var nextValue = handler.thisPromise._value; + + try { + if (nextStatus === STATUS_RESOLVED) { + if (typeof handler.onResolve === 'function') { + nextValue = handler.onResolve(nextValue); + } + } else if (typeof handler.onReject === 'function') { + nextValue = handler.onReject(nextValue); + nextStatus = STATUS_RESOLVED; + + if (handler.thisPromise._unhandledRejection) { + this.removeUnhandeledRejection(handler.thisPromise); + } + } + } catch (ex) { + nextStatus = STATUS_REJECTED; + nextValue = ex; + } + + handler.nextPromise._updateStatus(nextStatus, nextValue); + if (Date.now() >= timeoutAt) { + break; + } + } + + if (this.handlers.length > 0) { + setTimeout(this.runHandlers.bind(this), 0); + return; + } + + this.running = false; + }, + + addUnhandledRejection: function addUnhandledRejection(promise) { + this.unhandledRejections.push({ + promise: promise, + time: Date.now() + }); + this.scheduleRejectionCheck(); + }, + + removeUnhandeledRejection: function removeUnhandeledRejection(promise) { + promise._unhandledRejection = false; + for (var i = 0; i < this.unhandledRejections.length; i++) { + if (this.unhandledRejections[i].promise === promise) { + this.unhandledRejections.splice(i); + i--; + } + } + }, + + scheduleRejectionCheck: function scheduleRejectionCheck() { + if (this.pendingRejectionCheck) { + return; + } + this.pendingRejectionCheck = true; + setTimeout(function rejectionCheck() { + this.pendingRejectionCheck = false; + var now = Date.now(); + for (var i = 0; i < this.unhandledRejections.length; i++) { + if (now - this.unhandledRejections[i].time > REJECTION_TIMEOUT) { + var unhandled = this.unhandledRejections[i].promise._value; + var msg = 'Unhandled rejection: ' + unhandled; + if (unhandled.stack) { + msg += '\n' + unhandled.stack; + } + warn(msg); + this.unhandledRejections.splice(i); + i--; + } + } + if (this.unhandledRejections.length) { + this.scheduleRejectionCheck(); + } + }.bind(this), REJECTION_TIMEOUT); + } + }; + + function Promise(resolver) { + this._status = STATUS_PENDING; + this._handlers = []; + try { + resolver.call(this, this._resolve.bind(this), this._reject.bind(this)); + } catch (e) { + this._reject(e); + } + } + /** + * Builds a promise that is resolved when all the passed in promises are + * resolved. + * @param {array} array of data and/or promises to wait for. + * @return {Promise} New dependant promise. + */ + Promise.all = function Promise_all(promises) { + var resolveAll, rejectAll; + var deferred = new Promise(function (resolve, reject) { + resolveAll = resolve; + rejectAll = reject; + }); + var unresolved = promises.length; + var results = []; + if (unresolved === 0) { + resolveAll(results); + return deferred; + } + function reject(reason) { + if (deferred._status === STATUS_REJECTED) { + return; + } + results = []; + rejectAll(reason); + } + for (var i = 0, ii = promises.length; i < ii; ++i) { + var promise = promises[i]; + var resolve = (function(i) { + return function(value) { + if (deferred._status === STATUS_REJECTED) { + return; + } + results[i] = value; + unresolved--; + if (unresolved === 0) { + resolveAll(results); + } + }; + })(i); + if (Promise.isPromise(promise)) { + promise.then(resolve, reject); + } else { + resolve(promise); + } + } + return deferred; + }; + + /** + * Checks if the value is likely a promise (has a 'then' function). + * @return {boolean} true if value is thenable + */ + Promise.isPromise = function Promise_isPromise(value) { + return value && typeof value.then === 'function'; + }; + + /** + * Creates resolved promise + * @param value resolve value + * @returns {Promise} + */ + Promise.resolve = function Promise_resolve(value) { + return new Promise(function (resolve) { resolve(value); }); + }; + + /** + * Creates rejected promise + * @param reason rejection value + * @returns {Promise} + */ + Promise.reject = function Promise_reject(reason) { + return new Promise(function (resolve, reject) { reject(reason); }); + }; + + Promise.prototype = { + _status: null, + _value: null, + _handlers: null, + _unhandledRejection: null, + + _updateStatus: function Promise__updateStatus(status, value) { + if (this._status === STATUS_RESOLVED || + this._status === STATUS_REJECTED) { + return; + } + + if (status === STATUS_RESOLVED && + Promise.isPromise(value)) { + value.then(this._updateStatus.bind(this, STATUS_RESOLVED), + this._updateStatus.bind(this, STATUS_REJECTED)); + return; + } + + this._status = status; + this._value = value; + + if (status === STATUS_REJECTED && this._handlers.length === 0) { + this._unhandledRejection = true; + HandlerManager.addUnhandledRejection(this); + } + + HandlerManager.scheduleHandlers(this); + }, + + _resolve: function Promise_resolve(value) { + this._updateStatus(STATUS_RESOLVED, value); + }, + + _reject: function Promise_reject(reason) { + this._updateStatus(STATUS_REJECTED, reason); + }, + + then: function Promise_then(onResolve, onReject) { + var nextPromise = new Promise(function (resolve, reject) { + this.resolve = resolve; + this.reject = reject; + }); + this._handlers.push({ + thisPromise: this, + onResolve: onResolve, + onReject: onReject, + nextPromise: nextPromise + }); + HandlerManager.scheduleHandlers(this); + return nextPromise; + }, + + catch: function Promise_catch(onReject) { + return this.then(undefined, onReject); + } + }; + + globalScope.Promise = Promise; +})(); + +var StatTimer = (function StatTimerClosure() { + function rpad(str, pad, length) { + while (str.length < length) { + str += pad; + } + return str; + } + function StatTimer() { + this.started = {}; + this.times = []; + this.enabled = true; + } + StatTimer.prototype = { + time: function StatTimer_time(name) { + if (!this.enabled) { + return; + } + if (name in this.started) { + warn('Timer is already running for ' + name); + } + this.started[name] = Date.now(); + }, + timeEnd: function StatTimer_timeEnd(name) { + if (!this.enabled) { + return; + } + if (!(name in this.started)) { + warn('Timer has not been started for ' + name); + } + this.times.push({ + 'name': name, + 'start': this.started[name], + 'end': Date.now() + }); + // Remove timer from started so it can be called again. + delete this.started[name]; + }, + toString: function StatTimer_toString() { + var i, ii; + var times = this.times; + var out = ''; + // Find the longest name for padding purposes. + var longest = 0; + for (i = 0, ii = times.length; i < ii; ++i) { + var name = times[i]['name']; + if (name.length > longest) { + longest = name.length; + } + } + for (i = 0, ii = times.length; i < ii; ++i) { + var span = times[i]; + var duration = span.end - span.start; + out += rpad(span['name'], ' ', longest) + ' ' + duration + 'ms\n'; + } + return out; + } + }; + return StatTimer; +})(); + +PDFJS.createBlob = function createBlob(data, contentType) { + if (typeof Blob !== 'undefined') { + return new Blob([data], { type: contentType }); + } + // Blob builder is deprecated in FF14 and removed in FF18. + var bb = new MozBlobBuilder(); + bb.append(data); + return bb.getBlob(contentType); +}; + +PDFJS.createObjectURL = (function createObjectURLClosure() { + // Blob/createObjectURL is not available, falling back to data schema. + var digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + return function createObjectURL(data, contentType) { + if (!PDFJS.disableCreateObjectURL && + typeof URL !== 'undefined' && URL.createObjectURL) { + var blob = PDFJS.createBlob(data, contentType); + return URL.createObjectURL(blob); + } + + var buffer = 'data:' + contentType + ';base64,'; + for (var i = 0, ii = data.length; i < ii; i += 3) { + var b1 = data[i] & 0xFF; + var b2 = data[i + 1] & 0xFF; + var b3 = data[i + 2] & 0xFF; + var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); + var d3 = i + 1 < ii ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; + var d4 = i + 2 < ii ? (b3 & 0x3F) : 64; + buffer += digits[d1] + digits[d2] + digits[d3] + digits[d4]; + } + return buffer; + }; +})(); + +function MessageHandler(name, comObj) { + this.name = name; + this.comObj = comObj; + this.callbackIndex = 1; + this.postMessageTransfers = true; + var callbacksCapabilities = this.callbacksCapabilities = {}; + var ah = this.actionHandler = {}; + + ah['console_log'] = [function ahConsoleLog(data) { + console.log.apply(console, data); + }]; + ah['console_error'] = [function ahConsoleError(data) { + console.error.apply(console, data); + }]; + ah['_unsupported_feature'] = [function ah_unsupportedFeature(data) { + UnsupportedManager.notify(data); + }]; + + comObj.onmessage = function messageHandlerComObjOnMessage(event) { + var data = event.data; + if (data.isReply) { + var callbackId = data.callbackId; + if (data.callbackId in callbacksCapabilities) { + var callback = callbacksCapabilities[callbackId]; + delete callbacksCapabilities[callbackId]; + if ('error' in data) { + callback.reject(data.error); + } else { + callback.resolve(data.data); + } + } else { + error('Cannot resolve callback ' + callbackId); + } + } else if (data.action in ah) { + var action = ah[data.action]; + if (data.callbackId) { + Promise.resolve().then(function () { + return action[0].call(action[1], data.data); + }).then(function (result) { + comObj.postMessage({ + isReply: true, + callbackId: data.callbackId, + data: result + }); + }, function (reason) { + comObj.postMessage({ + isReply: true, + callbackId: data.callbackId, + error: reason + }); + }); + } else { + action[0].call(action[1], data.data); + } + } else { + error('Unknown action from worker: ' + data.action); + } + }; +} + +MessageHandler.prototype = { + on: function messageHandlerOn(actionName, handler, scope) { + var ah = this.actionHandler; + if (ah[actionName]) { + error('There is already an actionName called "' + actionName + '"'); + } + ah[actionName] = [handler, scope]; + }, + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * @param {String} actionName Action to call. + * @param {JSON} data JSON data to send. + * @param {Array} [transfers] Optional list of transfers/ArrayBuffers + */ + send: function messageHandlerSend(actionName, data, transfers) { + var message = { + action: actionName, + data: data + }; + this.postMessage(message, transfers); + }, + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * Expects that other side will callback with the response. + * @param {String} actionName Action to call. + * @param {JSON} data JSON data to send. + * @param {Array} [transfers] Optional list of transfers/ArrayBuffers. + * @returns {Promise} Promise to be resolved with response data. + */ + sendWithPromise: + function messageHandlerSendWithPromise(actionName, data, transfers) { + var callbackId = this.callbackIndex++; + var message = { + action: actionName, + data: data, + callbackId: callbackId + }; + var capability = createPromiseCapability(); + this.callbacksCapabilities[callbackId] = capability; + try { + this.postMessage(message, transfers); + } catch (e) { + capability.reject(e); + } + return capability.promise; + }, + /** + * Sends raw message to the comObj. + * @private + * @param message {Object} Raw message. + * @param transfers List of transfers/ArrayBuffers, or undefined. + */ + postMessage: function (message, transfers) { + if (transfers && this.postMessageTransfers) { + this.comObj.postMessage(message, transfers); + } else { + this.comObj.postMessage(message); + } + } +}; + +function loadJpegStream(id, imageUrl, objs) { + var img = new Image(); + img.onload = (function loadJpegStream_onloadClosure() { + objs.resolve(id, img); + }); + img.onerror = (function loadJpegStream_onerrorClosure() { + objs.resolve(id, null); + warn('Error during JPEG image loading'); + }); + img.src = imageUrl; +} + + +/** + * The maximum allowed image size in total pixels e.g. width * height. Images + * above this value will not be drawn. Use -1 for no limit. + * @var {number} + */ +PDFJS.maxImageSize = (PDFJS.maxImageSize === undefined ? + -1 : PDFJS.maxImageSize); + +/** + * The url of where the predefined Adobe CMaps are located. Include trailing + * slash. + * @var {string} + */ +PDFJS.cMapUrl = (PDFJS.cMapUrl === undefined ? null : PDFJS.cMapUrl); + +/** + * Specifies if CMaps are binary packed. + * @var {boolean} + */ +PDFJS.cMapPacked = PDFJS.cMapPacked === undefined ? false : PDFJS.cMapPacked; + +/** + * By default fonts are converted to OpenType fonts and loaded via font face + * rules. If disabled, the font will be rendered using a built in font renderer + * that constructs the glyphs with primitive path commands. + * @var {boolean} + */ +PDFJS.disableFontFace = (PDFJS.disableFontFace === undefined ? + false : PDFJS.disableFontFace); + +/** + * Path for image resources, mainly for annotation icons. Include trailing + * slash. + * @var {string} + */ +PDFJS.imageResourcesPath = (PDFJS.imageResourcesPath === undefined ? + '' : PDFJS.imageResourcesPath); + +/** + * Disable the web worker and run all code on the main thread. This will happen + * automatically if the browser doesn't support workers or sending typed arrays + * to workers. + * @var {boolean} + */ +PDFJS.disableWorker = (PDFJS.disableWorker === undefined ? + false : PDFJS.disableWorker); + +/** + * Path and filename of the worker file. Required when the worker is enabled in + * development mode. If unspecified in the production build, the worker will be + * loaded based on the location of the pdf.js file. + * @var {string} + */ +PDFJS.workerSrc = (PDFJS.workerSrc === undefined ? null : PDFJS.workerSrc); + +/** + * Disable range request loading of PDF files. When enabled and if the server + * supports partial content requests then the PDF will be fetched in chunks. + * Enabled (false) by default. + * @var {boolean} + */ +PDFJS.disableRange = (PDFJS.disableRange === undefined ? + false : PDFJS.disableRange); + +/** + * Disable streaming of PDF file data. By default PDF.js attempts to load PDF + * in chunks. This default behavior can be disabled. + * @var {boolean} + */ +PDFJS.disableStream = (PDFJS.disableStream === undefined ? + false : PDFJS.disableStream); + +/** + * Disable pre-fetching of PDF file data. When range requests are enabled PDF.js + * will automatically keep fetching more data even if it isn't needed to display + * the current page. This default behavior can be disabled. + * + * NOTE: It is also necessary to disable streaming, see above, + * in order for disabling of pre-fetching to work correctly. + * @var {boolean} + */ +PDFJS.disableAutoFetch = (PDFJS.disableAutoFetch === undefined ? + false : PDFJS.disableAutoFetch); + +/** + * Enables special hooks for debugging PDF.js. + * @var {boolean} + */ +PDFJS.pdfBug = (PDFJS.pdfBug === undefined ? false : PDFJS.pdfBug); + +/** + * Enables transfer usage in postMessage for ArrayBuffers. + * @var {boolean} + */ +PDFJS.postMessageTransfers = (PDFJS.postMessageTransfers === undefined ? + true : PDFJS.postMessageTransfers); + +/** + * Disables URL.createObjectURL usage. + * @var {boolean} + */ +PDFJS.disableCreateObjectURL = (PDFJS.disableCreateObjectURL === undefined ? + false : PDFJS.disableCreateObjectURL); + +/** + * Disables WebGL usage. + * @var {boolean} + */ +PDFJS.disableWebGL = (PDFJS.disableWebGL === undefined ? + true : PDFJS.disableWebGL); + +/** + * Disables fullscreen support, and by extension Presentation Mode, + * in browsers which support the fullscreen API. + * @var {boolean} + */ +PDFJS.disableFullscreen = (PDFJS.disableFullscreen === undefined ? + false : PDFJS.disableFullscreen); + +/** + * Enables CSS only zooming. + * @var {boolean} + */ +PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ? + false : PDFJS.useOnlyCssZoom); + +/** + * Controls the logging level. + * The constants from PDFJS.VERBOSITY_LEVELS should be used: + * - errors + * - warnings [default] + * - infos + * @var {number} + */ +PDFJS.verbosity = (PDFJS.verbosity === undefined ? + PDFJS.VERBOSITY_LEVELS.warnings : PDFJS.verbosity); + +/** + * The maximum supported canvas size in total pixels e.g. width * height. + * The default value is 4096 * 4096. Use -1 for no limit. + * @var {number} + */ +PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ? + 16777216 : PDFJS.maxCanvasPixels); + +/** + * Opens external links in a new window if enabled. The default behavior opens + * external links in the PDF.js window. + * @var {boolean} + */ +PDFJS.openExternalLinksInNewWindow = ( + PDFJS.openExternalLinksInNewWindow === undefined ? + false : PDFJS.openExternalLinksInNewWindow); + +/** + * Document initialization / loading parameters object. + * + * @typedef {Object} DocumentInitParameters + * @property {string} url - The URL of the PDF. + * @property {TypedArray|Array|string} data - Binary PDF data. Use typed arrays + * (Uint8Array) to improve the memory usage. If PDF data is BASE64-encoded, + * use atob() to convert it to a binary string first. + * @property {Object} httpHeaders - Basic authentication headers. + * @property {boolean} withCredentials - Indicates whether or not cross-site + * Access-Control requests should be made using credentials such as cookies + * or authorization headers. The default is false. + * @property {string} password - For decrypting password-protected PDFs. + * @property {TypedArray} initialData - A typed array with the first portion or + * all of the pdf data. Used by the extension since some data is already + * loaded before the switch to range requests. + * @property {number} length - The PDF file length. It's used for progress + * reports and range requests operations. + * @property {PDFDataRangeTransport} range + */ + +/** + * @typedef {Object} PDFDocumentStats + * @property {Array} streamTypes - Used stream types in the document (an item + * is set to true if specific stream ID was used in the document). + * @property {Array} fontTypes - Used font type in the document (an item is set + * to true if specific font ID was used in the document). + */ + +/** + * This is the main entry point for loading a PDF and interacting with it. + * NOTE: If a URL is used to fetch the PDF data a standard XMLHttpRequest(XHR) + * is used, which means it must follow the same origin rules that any XHR does + * e.g. No cross domain requests without CORS. + * + * @param {string|TypedArray|DocumentInitParameters|PDFDataRangeTransport} src + * Can be a url to where a PDF is located, a typed array (Uint8Array) + * already populated with data or parameter object. + * + * @param {PDFDataRangeTransport} pdfDataRangeTransport (deprecated) It is used + * if you want to manually serve range requests for data in the PDF. + * + * @param {function} passwordCallback (deprecated) It is used to request a + * password if wrong or no password was provided. The callback receives two + * parameters: function that needs to be called with new password and reason + * (see {PasswordResponses}). + * + * @param {function} progressCallback (deprecated) It is used to be able to + * monitor the loading progress of the PDF file (necessary to implement e.g. + * a loading bar). The callback receives an {Object} with the properties: + * {number} loaded and {number} total. + * + * @return {PDFDocumentLoadingTask} + */ +PDFJS.getDocument = function getDocument(src, + pdfDataRangeTransport, + passwordCallback, + progressCallback) { + var task = new PDFDocumentLoadingTask(); + + // Support of the obsolete arguments (for compatibility with API v1.0) + if (pdfDataRangeTransport) { + if (!(pdfDataRangeTransport instanceof PDFDataRangeTransport)) { + // Not a PDFDataRangeTransport instance, trying to add missing properties. + pdfDataRangeTransport = Object.create(pdfDataRangeTransport); + pdfDataRangeTransport.length = src.length; + pdfDataRangeTransport.initialData = src.initialData; + } + src = Object.create(src); + src.range = pdfDataRangeTransport; + } + task.onPassword = passwordCallback || null; + task.onProgress = progressCallback || null; + + var workerInitializedCapability, transport; + var source; + if (typeof src === 'string') { + source = { url: src }; + } else if (isArrayBuffer(src)) { + source = { data: src }; + } else if (src instanceof PDFDataRangeTransport) { + source = { range: src }; + } else { + if (typeof src !== 'object') { + error('Invalid parameter in getDocument, need either Uint8Array, ' + + 'string or a parameter object'); + } + if (!src.url && !src.data && !src.range) { + error('Invalid parameter object: need either .data, .range or .url'); + } + + source = src; + } + + var params = {}; + for (var key in source) { + if (key === 'url' && typeof window !== 'undefined') { + // The full path is required in the 'url' field. + params[key] = combineUrl(window.location.href, source[key]); + continue; + } else if (key === 'range') { + continue; + } else if (key === 'data' && !(source[key] instanceof Uint8Array)) { + // Converting string or array-like data to Uint8Array. + var pdfBytes = source[key]; + if (typeof pdfBytes === 'string') { + params[key] = stringToBytes(pdfBytes); + } else if (typeof pdfBytes === 'object' && pdfBytes !== null && + !isNaN(pdfBytes.length)) { + params[key] = new Uint8Array(pdfBytes); + } else { + error('Invalid PDF binary data: either typed array, string or ' + + 'array-like object is expected in the data property.'); + } + continue; + } + params[key] = source[key]; + } + + workerInitializedCapability = createPromiseCapability(); + transport = new WorkerTransport(workerInitializedCapability, source.range); + workerInitializedCapability.promise.then(function transportInitialized() { + transport.fetchDocument(task, params); + }); + + return task; +}; + +/** + * PDF document loading operation. + * @class + */ +var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() { + /** @constructs PDFDocumentLoadingTask */ + function PDFDocumentLoadingTask() { + this._capability = createPromiseCapability(); + + /** + * Callback to request a password if wrong or no password was provided. + * The callback receives two parameters: function that needs to be called + * with new password and reason (see {PasswordResponses}). + */ + this.onPassword = null; + + /** + * Callback to be able to monitor the loading progress of the PDF file + * (necessary to implement e.g. a loading bar). The callback receives + * an {Object} with the properties: {number} loaded and {number} total. + */ + this.onProgress = null; + } + + PDFDocumentLoadingTask.prototype = + /** @lends PDFDocumentLoadingTask.prototype */ { + /** + * @return {Promise} + */ + get promise() { + return this._capability.promise; + }, + + // TODO add cancel or abort method + + /** + * Registers callbacks to indicate the document loading completion. + * + * @param {function} onFulfilled The callback for the loading completion. + * @param {function} onRejected The callback for the loading failure. + * @return {Promise} A promise that is resolved after the onFulfilled or + * onRejected callback. + */ + then: function PDFDocumentLoadingTask_then(onFulfilled, onRejected) { + return this.promise.then.apply(this.promise, arguments); + } + }; + + return PDFDocumentLoadingTask; +})(); + +/** + * Abstract class to support range requests file loading. + * @class + */ +var PDFDataRangeTransport = (function pdfDataRangeTransportClosure() { + /** + * @constructs PDFDataRangeTransport + * @param {number} length + * @param {Uint8Array} initialData + */ + function PDFDataRangeTransport(length, initialData) { + this.length = length; + this.initialData = initialData; + + this._rangeListeners = []; + this._progressListeners = []; + this._progressiveReadListeners = []; + this._readyCapability = createPromiseCapability(); + } + PDFDataRangeTransport.prototype = + /** @lends PDFDataRangeTransport.prototype */ { + addRangeListener: + function PDFDataRangeTransport_addRangeListener(listener) { + this._rangeListeners.push(listener); + }, + + addProgressListener: + function PDFDataRangeTransport_addProgressListener(listener) { + this._progressListeners.push(listener); + }, + + addProgressiveReadListener: + function PDFDataRangeTransport_addProgressiveReadListener(listener) { + this._progressiveReadListeners.push(listener); + }, + + onDataRange: function PDFDataRangeTransport_onDataRange(begin, chunk) { + var listeners = this._rangeListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](begin, chunk); + } + }, + + onDataProgress: function PDFDataRangeTransport_onDataProgress(loaded) { + this._readyCapability.promise.then(function () { + var listeners = this._progressListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](loaded); + } + }.bind(this)); + }, + + onDataProgressiveRead: + function PDFDataRangeTransport_onDataProgress(chunk) { + this._readyCapability.promise.then(function () { + var listeners = this._progressiveReadListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](chunk); + } + }.bind(this)); + }, + + transportReady: function PDFDataRangeTransport_transportReady() { + this._readyCapability.resolve(); + }, + + requestDataRange: + function PDFDataRangeTransport_requestDataRange(begin, end) { + throw new Error('Abstract method PDFDataRangeTransport.requestDataRange'); + } + }; + return PDFDataRangeTransport; +})(); + +PDFJS.PDFDataRangeTransport = PDFDataRangeTransport; + +/** + * Proxy to a PDFDocument in the worker thread. Also, contains commonly used + * properties that can be read synchronously. + * @class + */ +var PDFDocumentProxy = (function PDFDocumentProxyClosure() { + function PDFDocumentProxy(pdfInfo, transport) { + this.pdfInfo = pdfInfo; + this.transport = transport; + } + PDFDocumentProxy.prototype = /** @lends PDFDocumentProxy.prototype */ { + /** + * @return {number} Total number of pages the PDF contains. + */ + get numPages() { + return this.pdfInfo.numPages; + }, + /** + * @return {string} A unique ID to identify a PDF. Not guaranteed to be + * unique. + */ + get fingerprint() { + return this.pdfInfo.fingerprint; + }, + /** + * @param {number} pageNumber The page number to get. The first page is 1. + * @return {Promise} A promise that is resolved with a {@link PDFPageProxy} + * object. + */ + getPage: function PDFDocumentProxy_getPage(pageNumber) { + return this.transport.getPage(pageNumber); + }, + /** + * @param {{num: number, gen: number}} ref The page reference. Must have + * the 'num' and 'gen' properties. + * @return {Promise} A promise that is resolved with the page index that is + * associated with the reference. + */ + getPageIndex: function PDFDocumentProxy_getPageIndex(ref) { + return this.transport.getPageIndex(ref); + }, + /** + * @return {Promise} A promise that is resolved with a lookup table for + * mapping named destinations to reference numbers. + * + * This can be slow for large documents: use getDestination instead + */ + getDestinations: function PDFDocumentProxy_getDestinations() { + return this.transport.getDestinations(); + }, + /** + * @param {string} id The named destination to get. + * @return {Promise} A promise that is resolved with all information + * of the given named destination. + */ + getDestination: function PDFDocumentProxy_getDestination(id) { + return this.transport.getDestination(id); + }, + /** + * @return {Promise} A promise that is resolved with a lookup table for + * mapping named attachments to their content. + */ + getAttachments: function PDFDocumentProxy_getAttachments() { + return this.transport.getAttachments(); + }, + /** + * @return {Promise} A promise that is resolved with an array of all the + * JavaScript strings in the name tree. + */ + getJavaScript: function PDFDocumentProxy_getJavaScript() { + return this.transport.getJavaScript(); + }, + /** + * @return {Promise} A promise that is resolved with an {Array} that is a + * tree outline (if it has one) of the PDF. The tree is in the format of: + * [ + * { + * title: string, + * bold: boolean, + * italic: boolean, + * color: rgb array, + * dest: dest obj, + * items: array of more items like this + * }, + * ... + * ]. + */ + getOutline: function PDFDocumentProxy_getOutline() { + return this.transport.getOutline(); + }, + /** + * @return {Promise} A promise that is resolved with an {Object} that has + * info and metadata properties. Info is an {Object} filled with anything + * available in the information dictionary and similarly metadata is a + * {Metadata} object with information from the metadata section of the PDF. + */ + getMetadata: function PDFDocumentProxy_getMetadata() { + return this.transport.getMetadata(); + }, + /** + * @return {Promise} A promise that is resolved with a TypedArray that has + * the raw data from the PDF. + */ + getData: function PDFDocumentProxy_getData() { + return this.transport.getData(); + }, + /** + * @return {Promise} A promise that is resolved when the document's data + * is loaded. It is resolved with an {Object} that contains the length + * property that indicates size of the PDF data in bytes. + */ + getDownloadInfo: function PDFDocumentProxy_getDownloadInfo() { + return this.transport.downloadInfoCapability.promise; + }, + /** + * @return {Promise} A promise this is resolved with current stats about + * document structures (see {@link PDFDocumentStats}). + */ + getStats: function PDFDocumentProxy_getStats() { + return this.transport.getStats(); + }, + /** + * Cleans up resources allocated by the document, e.g. created @font-face. + */ + cleanup: function PDFDocumentProxy_cleanup() { + this.transport.startCleanup(); + }, + /** + * Destroys current document instance and terminates worker. + */ + destroy: function PDFDocumentProxy_destroy() { + this.transport.destroy(); + } + }; + return PDFDocumentProxy; +})(); + +/** + * Page text content. + * + * @typedef {Object} TextContent + * @property {array} items - array of {@link TextItem} + * @property {Object} styles - {@link TextStyles} objects, indexed by font + * name. + */ + +/** + * Page text content part. + * + * @typedef {Object} TextItem + * @property {string} str - text content. + * @property {string} dir - text direction: 'ttb', 'ltr' or 'rtl'. + * @property {array} transform - transformation matrix. + * @property {number} width - width in device space. + * @property {number} height - height in device space. + * @property {string} fontName - font name used by pdf.js for converted font. + */ + +/** + * Text style. + * + * @typedef {Object} TextStyle + * @property {number} ascent - font ascent. + * @property {number} descent - font descent. + * @property {boolean} vertical - text is in vertical mode. + * @property {string} fontFamily - possible font family + */ + +/** + * Page render parameters. + * + * @typedef {Object} RenderParameters + * @property {Object} canvasContext - A 2D context of a DOM Canvas object. + * @property {PDFJS.PageViewport} viewport - Rendering viewport obtained by + * calling of PDFPage.getViewport method. + * @property {string} intent - Rendering intent, can be 'display' or 'print' + * (default value is 'display'). + * @property {Object} imageLayer - (optional) An object that has beginLayout, + * endLayout and appendImage functions. + * @property {function} continueCallback - (deprecated) A function that will be + * called each time the rendering is paused. To continue + * rendering call the function that is the first argument + * to the callback. + */ + +/** + * PDF page operator list. + * + * @typedef {Object} PDFOperatorList + * @property {Array} fnArray - Array containing the operator functions. + * @property {Array} argsArray - Array containing the arguments of the + * functions. + */ + +/** + * Proxy to a PDFPage in the worker thread. + * @class + */ +var PDFPageProxy = (function PDFPageProxyClosure() { + function PDFPageProxy(pageIndex, pageInfo, transport) { + this.pageIndex = pageIndex; + this.pageInfo = pageInfo; + this.transport = transport; + this.stats = new StatTimer(); + this.stats.enabled = !!globalScope.PDFJS.enableStats; + this.commonObjs = transport.commonObjs; + this.objs = new PDFObjects(); + this.cleanupAfterRender = false; + this.pendingDestroy = false; + this.intentStates = {}; + } + PDFPageProxy.prototype = /** @lends PDFPageProxy.prototype */ { + /** + * @return {number} Page number of the page. First page is 1. + */ + get pageNumber() { + return this.pageIndex + 1; + }, + /** + * @return {number} The number of degrees the page is rotated clockwise. + */ + get rotate() { + return this.pageInfo.rotate; + }, + /** + * @return {Object} The reference that points to this page. It has 'num' and + * 'gen' properties. + */ + get ref() { + return this.pageInfo.ref; + }, + /** + * @return {Array} An array of the visible portion of the PDF page in the + * user space units - [x1, y1, x2, y2]. + */ + get view() { + return this.pageInfo.view; + }, + /** + * @param {number} scale The desired scale of the viewport. + * @param {number} rotate Degrees to rotate the viewport. If omitted this + * defaults to the page rotation. + * @return {PDFJS.PageViewport} Contains 'width' and 'height' properties + * along with transforms required for rendering. + */ + getViewport: function PDFPageProxy_getViewport(scale, rotate) { + if (arguments.length < 2) { + rotate = this.rotate; + } + return new PDFJS.PageViewport(this.view, scale, rotate, 0, 0); + }, + /** + * @return {Promise} A promise that is resolved with an {Array} of the + * annotation objects. + */ + getAnnotations: function PDFPageProxy_getAnnotations() { + if (this.annotationsPromise) { + return this.annotationsPromise; + } + + var promise = this.transport.getAnnotations(this.pageIndex); + this.annotationsPromise = promise; + return promise; + }, + /** + * Begins the process of rendering a page to the desired context. + * @param {RenderParameters} params Page render parameters. + * @return {RenderTask} An object that contains the promise, which + * is resolved when the page finishes rendering. + */ + render: function PDFPageProxy_render(params) { + var stats = this.stats; + stats.time('Overall'); + + // If there was a pending destroy cancel it so no cleanup happens during + // this call to render. + this.pendingDestroy = false; + + var renderingIntent = (params.intent === 'print' ? 'print' : 'display'); + + if (!this.intentStates[renderingIntent]) { + this.intentStates[renderingIntent] = {}; + } + var intentState = this.intentStates[renderingIntent]; + + // If there's no displayReadyCapability yet, then the operatorList + // was never requested before. Make the request and create the promise. + if (!intentState.displayReadyCapability) { + intentState.receivingOperatorList = true; + intentState.displayReadyCapability = createPromiseCapability(); + intentState.operatorList = { + fnArray: [], + argsArray: [], + lastChunk: false + }; + + this.stats.time('Page Request'); + this.transport.messageHandler.send('RenderPageRequest', { + pageIndex: this.pageNumber - 1, + intent: renderingIntent + }); + } + + var internalRenderTask = new InternalRenderTask(complete, params, + this.objs, + this.commonObjs, + intentState.operatorList, + this.pageNumber); + internalRenderTask.useRequestAnimationFrame = renderingIntent !== 'print'; + if (!intentState.renderTasks) { + intentState.renderTasks = []; + } + intentState.renderTasks.push(internalRenderTask); + var renderTask = internalRenderTask.task; + + // Obsolete parameter support + if (params.continueCallback) { + renderTask.onContinue = params.continueCallback; + } + + var self = this; + intentState.displayReadyCapability.promise.then( + function pageDisplayReadyPromise(transparency) { + if (self.pendingDestroy) { + complete(); + return; + } + stats.time('Rendering'); + internalRenderTask.initalizeGraphics(transparency); + internalRenderTask.operatorListChanged(); + }, + function pageDisplayReadPromiseError(reason) { + complete(reason); + } + ); + + function complete(error) { + var i = intentState.renderTasks.indexOf(internalRenderTask); + if (i >= 0) { + intentState.renderTasks.splice(i, 1); + } + + if (self.cleanupAfterRender) { + self.pendingDestroy = true; + } + self._tryDestroy(); + + if (error) { + internalRenderTask.capability.reject(error); + } else { + internalRenderTask.capability.resolve(); + } + stats.timeEnd('Rendering'); + stats.timeEnd('Overall'); + } + + return renderTask; + }, + + /** + * @return {Promise} A promise resolved with an {@link PDFOperatorList} + * object that represents page's operator list. + */ + getOperatorList: function PDFPageProxy_getOperatorList() { + function operatorListChanged() { + if (intentState.operatorList.lastChunk) { + intentState.opListReadCapability.resolve(intentState.operatorList); + } + } + + var renderingIntent = 'oplist'; + if (!this.intentStates[renderingIntent]) { + this.intentStates[renderingIntent] = {}; + } + var intentState = this.intentStates[renderingIntent]; + + if (!intentState.opListReadCapability) { + var opListTask = {}; + opListTask.operatorListChanged = operatorListChanged; + intentState.receivingOperatorList = true; + intentState.opListReadCapability = createPromiseCapability(); + intentState.renderTasks = []; + intentState.renderTasks.push(opListTask); + intentState.operatorList = { + fnArray: [], + argsArray: [], + lastChunk: false + }; + + this.transport.messageHandler.send('RenderPageRequest', { + pageIndex: this.pageIndex, + intent: renderingIntent + }); + } + return intentState.opListReadCapability.promise; + }, + + /** + * @return {Promise} That is resolved a {@link TextContent} + * object that represent the page text content. + */ + getTextContent: function PDFPageProxy_getTextContent() { + return this.transport.messageHandler.sendWithPromise('GetTextContent', { + pageIndex: this.pageNumber - 1 + }); + }, + /** + * Destroys resources allocated by the page. + */ + destroy: function PDFPageProxy_destroy() { + this.pendingDestroy = true; + this._tryDestroy(); + }, + /** + * For internal use only. Attempts to clean up if rendering is in a state + * where that's possible. + * @ignore + */ + _tryDestroy: function PDFPageProxy__destroy() { + if (!this.pendingDestroy || + Object.keys(this.intentStates).some(function(intent) { + var intentState = this.intentStates[intent]; + return (intentState.renderTasks.length !== 0 || + intentState.receivingOperatorList); + }, this)) { + return; + } + + Object.keys(this.intentStates).forEach(function(intent) { + delete this.intentStates[intent]; + }, this); + this.objs.clear(); + this.annotationsPromise = null; + this.pendingDestroy = false; + }, + /** + * For internal use only. + * @ignore + */ + _startRenderPage: function PDFPageProxy_startRenderPage(transparency, + intent) { + var intentState = this.intentStates[intent]; + // TODO Refactor RenderPageRequest to separate rendering + // and operator list logic + if (intentState.displayReadyCapability) { + intentState.displayReadyCapability.resolve(transparency); + } + }, + /** + * For internal use only. + * @ignore + */ + _renderPageChunk: function PDFPageProxy_renderPageChunk(operatorListChunk, + intent) { + var intentState = this.intentStates[intent]; + var i, ii; + // Add the new chunk to the current operator list. + for (i = 0, ii = operatorListChunk.length; i < ii; i++) { + intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]); + intentState.operatorList.argsArray.push( + operatorListChunk.argsArray[i]); + } + intentState.operatorList.lastChunk = operatorListChunk.lastChunk; + + // Notify all the rendering tasks there are more operators to be consumed. + for (i = 0; i < intentState.renderTasks.length; i++) { + intentState.renderTasks[i].operatorListChanged(); + } + + if (operatorListChunk.lastChunk) { + intentState.receivingOperatorList = false; + this._tryDestroy(); + } + } + }; + return PDFPageProxy; +})(); + +/** + * For internal use only. + * @ignore + */ +var WorkerTransport = (function WorkerTransportClosure() { + function WorkerTransport(workerInitializedCapability, pdfDataRangeTransport) { + this.pdfDataRangeTransport = pdfDataRangeTransport; + this.workerInitializedCapability = workerInitializedCapability; + this.commonObjs = new PDFObjects(); + + this.loadingTask = null; + + this.pageCache = []; + this.pagePromises = []; + this.downloadInfoCapability = createPromiseCapability(); + + // If worker support isn't disabled explicit and the browser has worker + // support, create a new web worker and test if it/the browser fullfills + // all requirements to run parts of pdf.js in a web worker. + // Right now, the requirement is, that an Uint8Array is still an Uint8Array + // as it arrives on the worker. Chrome added this with version 15. + if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') { + var workerSrc = PDFJS.workerSrc; + if (!workerSrc) { + error('No PDFJS.workerSrc specified'); + } + + try { + // Some versions of FF can't create a worker on localhost, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 + var worker = new Worker(workerSrc); + var messageHandler = new MessageHandler('main', worker); + this.messageHandler = messageHandler; + + messageHandler.on('test', function transportTest(data) { + var supportTypedArray = data && data.supportTypedArray; + if (supportTypedArray) { + this.worker = worker; + if (!data.supportTransfers) { + PDFJS.postMessageTransfers = false; + } + this.setupMessageHandler(messageHandler); + workerInitializedCapability.resolve(); + } else { + this.setupFakeWorker(); + } + }.bind(this)); + + var testObj = new Uint8Array([PDFJS.postMessageTransfers ? 255 : 0]); + // Some versions of Opera throw a DATA_CLONE_ERR on serializing the + // typed array. Also, checking if we can use transfers. + try { + messageHandler.send('test', testObj, [testObj.buffer]); + } catch (ex) { + info('Cannot use postMessage transfers'); + testObj[0] = 0; + messageHandler.send('test', testObj); + } + return; + } catch (e) { + info('The worker has been disabled.'); + } + } + // Either workers are disabled, not supported or have thrown an exception. + // Thus, we fallback to a faked worker. + this.setupFakeWorker(); + } + WorkerTransport.prototype = { + destroy: function WorkerTransport_destroy() { + this.pageCache = []; + this.pagePromises = []; + var self = this; + this.messageHandler.sendWithPromise('Terminate', null).then(function () { + FontLoader.clear(); + if (self.worker) { + self.worker.terminate(); + } + }); + }, + + setupFakeWorker: function WorkerTransport_setupFakeWorker() { + globalScope.PDFJS.disableWorker = true; + + if (!PDFJS.fakeWorkerFilesLoadedCapability) { + PDFJS.fakeWorkerFilesLoadedCapability = createPromiseCapability(); + // In the developer build load worker_loader which in turn loads all the + // other files and resolves the promise. In production only the + // pdf.worker.js file is needed. + Util.loadScript(PDFJS.workerSrc, function() { + PDFJS.fakeWorkerFilesLoadedCapability.resolve(); + }); + } + PDFJS.fakeWorkerFilesLoadedCapability.promise.then(function () { + warn('Setting up fake worker.'); + // If we don't use a worker, just post/sendMessage to the main thread. + var fakeWorker = { + postMessage: function WorkerTransport_postMessage(obj) { + fakeWorker.onmessage({data: obj}); + }, + terminate: function WorkerTransport_terminate() {} + }; + + var messageHandler = new MessageHandler('main', fakeWorker); + this.setupMessageHandler(messageHandler); + + // If the main thread is our worker, setup the handling for the messages + // the main thread sends to it self. + PDFJS.WorkerMessageHandler.setup(messageHandler); + + this.workerInitializedCapability.resolve(); + }.bind(this)); + }, + + setupMessageHandler: + function WorkerTransport_setupMessageHandler(messageHandler) { + this.messageHandler = messageHandler; + + function updatePassword(password) { + messageHandler.send('UpdatePassword', password); + } + + var pdfDataRangeTransport = this.pdfDataRangeTransport; + if (pdfDataRangeTransport) { + pdfDataRangeTransport.addRangeListener(function(begin, chunk) { + messageHandler.send('OnDataRange', { + begin: begin, + chunk: chunk + }); + }); + + pdfDataRangeTransport.addProgressListener(function(loaded) { + messageHandler.send('OnDataProgress', { + loaded: loaded + }); + }); + + pdfDataRangeTransport.addProgressiveReadListener(function(chunk) { + messageHandler.send('OnDataRange', { + chunk: chunk + }); + }); + + messageHandler.on('RequestDataRange', + function transportDataRange(data) { + pdfDataRangeTransport.requestDataRange(data.begin, data.end); + }, this); + } + + messageHandler.on('GetDoc', function transportDoc(data) { + var pdfInfo = data.pdfInfo; + this.numPages = data.pdfInfo.numPages; + var pdfDocument = new PDFDocumentProxy(pdfInfo, this); + this.pdfDocument = pdfDocument; + this.loadingTask._capability.resolve(pdfDocument); + }, this); + + messageHandler.on('NeedPassword', + function transportNeedPassword(exception) { + var loadingTask = this.loadingTask; + if (loadingTask.onPassword) { + return loadingTask.onPassword(updatePassword, + PasswordResponses.NEED_PASSWORD); + } + loadingTask._capability.reject( + new PasswordException(exception.message, exception.code)); + }, this); + + messageHandler.on('IncorrectPassword', + function transportIncorrectPassword(exception) { + var loadingTask = this.loadingTask; + if (loadingTask.onPassword) { + return loadingTask.onPassword(updatePassword, + PasswordResponses.INCORRECT_PASSWORD); + } + loadingTask._capability.reject( + new PasswordException(exception.message, exception.code)); + }, this); + + messageHandler.on('InvalidPDF', function transportInvalidPDF(exception) { + this.loadingTask._capability.reject( + new InvalidPDFException(exception.message)); + }, this); + + messageHandler.on('MissingPDF', function transportMissingPDF(exception) { + this.loadingTask._capability.reject( + new MissingPDFException(exception.message)); + }, this); + + messageHandler.on('UnexpectedResponse', + function transportUnexpectedResponse(exception) { + this.loadingTask._capability.reject( + new UnexpectedResponseException(exception.message, exception.status)); + }, this); + + messageHandler.on('UnknownError', + function transportUnknownError(exception) { + this.loadingTask._capability.reject( + new UnknownErrorException(exception.message, exception.details)); + }, this); + + messageHandler.on('DataLoaded', function transportPage(data) { + this.downloadInfoCapability.resolve(data); + }, this); + + messageHandler.on('PDFManagerReady', function transportPage(data) { + if (this.pdfDataRangeTransport) { + this.pdfDataRangeTransport.transportReady(); + } + }, this); + + messageHandler.on('StartRenderPage', function transportRender(data) { + var page = this.pageCache[data.pageIndex]; + + page.stats.timeEnd('Page Request'); + page._startRenderPage(data.transparency, data.intent); + }, this); + + messageHandler.on('RenderPageChunk', function transportRender(data) { + var page = this.pageCache[data.pageIndex]; + + page._renderPageChunk(data.operatorList, data.intent); + }, this); + + messageHandler.on('commonobj', function transportObj(data) { + var id = data[0]; + var type = data[1]; + if (this.commonObjs.hasData(id)) { + return; + } + + switch (type) { + case 'Font': + var exportedData = data[2]; + + var font; + if ('error' in exportedData) { + var error = exportedData.error; + warn('Error during font loading: ' + error); + this.commonObjs.resolve(id, error); + break; + } else { + font = new FontFaceObject(exportedData); + } + + FontLoader.bind( + [font], + function fontReady(fontObjs) { + this.commonObjs.resolve(id, font); + }.bind(this) + ); + break; + case 'FontPath': + this.commonObjs.resolve(id, data[2]); + break; + default: + error('Got unknown common object type ' + type); + } + }, this); + + messageHandler.on('obj', function transportObj(data) { + var id = data[0]; + var pageIndex = data[1]; + var type = data[2]; + var pageProxy = this.pageCache[pageIndex]; + var imageData; + if (pageProxy.objs.hasData(id)) { + return; + } + + switch (type) { + case 'JpegStream': + imageData = data[3]; + loadJpegStream(id, imageData, pageProxy.objs); + break; + case 'Image': + imageData = data[3]; + pageProxy.objs.resolve(id, imageData); + + // heuristics that will allow not to store large data + var MAX_IMAGE_SIZE_TO_STORE = 8000000; + if (imageData && 'data' in imageData && + imageData.data.length > MAX_IMAGE_SIZE_TO_STORE) { + pageProxy.cleanupAfterRender = true; + } + break; + default: + error('Got unknown object type ' + type); + } + }, this); + + messageHandler.on('DocProgress', function transportDocProgress(data) { + var loadingTask = this.loadingTask; + if (loadingTask.onProgress) { + loadingTask.onProgress({ + loaded: data.loaded, + total: data.total + }); + } + }, this); + + messageHandler.on('PageError', function transportError(data) { + var page = this.pageCache[data.pageNum - 1]; + var intentState = page.intentStates[data.intent]; + if (intentState.displayReadyCapability) { + intentState.displayReadyCapability.reject(data.error); + } else { + error(data.error); + } + }, this); + + messageHandler.on('JpegDecode', function(data) { + var imageUrl = data[0]; + var components = data[1]; + if (components !== 3 && components !== 1) { + return Promise.reject( + new Error('Only 3 components or 1 component can be returned')); + } + + return new Promise(function (resolve, reject) { + var img = new Image(); + img.onload = function () { + var width = img.width; + var height = img.height; + var size = width * height; + var rgbaLength = size * 4; + var buf = new Uint8Array(size * components); + var tmpCanvas = createScratchCanvas(width, height); + var tmpCtx = tmpCanvas.getContext('2d'); + tmpCtx.drawImage(img, 0, 0); + var data = tmpCtx.getImageData(0, 0, width, height).data; + var i, j; + + if (components === 3) { + for (i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { + buf[j] = data[i]; + buf[j + 1] = data[i + 1]; + buf[j + 2] = data[i + 2]; + } + } else if (components === 1) { + for (i = 0, j = 0; i < rgbaLength; i += 4, j++) { + buf[j] = data[i]; + } + } + resolve({ data: buf, width: width, height: height}); + }; + img.onerror = function () { + reject(new Error('JpegDecode failed to load image')); + }; + img.src = imageUrl; + }); + }); + }, + + fetchDocument: function WorkerTransport_fetchDocument(loadingTask, source) { + this.loadingTask = loadingTask; + + source.disableAutoFetch = PDFJS.disableAutoFetch; + source.disableStream = PDFJS.disableStream; + source.chunkedViewerLoading = !!this.pdfDataRangeTransport; + if (this.pdfDataRangeTransport) { + source.length = this.pdfDataRangeTransport.length; + source.initialData = this.pdfDataRangeTransport.initialData; + } + this.messageHandler.send('GetDocRequest', { + source: source, + disableRange: PDFJS.disableRange, + maxImageSize: PDFJS.maxImageSize, + cMapUrl: PDFJS.cMapUrl, + cMapPacked: PDFJS.cMapPacked, + disableFontFace: PDFJS.disableFontFace, + disableCreateObjectURL: PDFJS.disableCreateObjectURL, + verbosity: PDFJS.verbosity + }); + }, + + getData: function WorkerTransport_getData() { + return this.messageHandler.sendWithPromise('GetData', null); + }, + + getPage: function WorkerTransport_getPage(pageNumber, capability) { + if (pageNumber <= 0 || pageNumber > this.numPages || + (pageNumber|0) !== pageNumber) { + return Promise.reject(new Error('Invalid page request')); + } + + var pageIndex = pageNumber - 1; + if (pageIndex in this.pagePromises) { + return this.pagePromises[pageIndex]; + } + var promise = this.messageHandler.sendWithPromise('GetPage', { + pageIndex: pageIndex + }).then(function (pageInfo) { + var page = new PDFPageProxy(pageIndex, pageInfo, this); + this.pageCache[pageIndex] = page; + return page; + }.bind(this)); + this.pagePromises[pageIndex] = promise; + return promise; + }, + + getPageIndex: function WorkerTransport_getPageIndexByRef(ref) { + return this.messageHandler.sendWithPromise('GetPageIndex', { ref: ref }); + }, + + getAnnotations: function WorkerTransport_getAnnotations(pageIndex) { + return this.messageHandler.sendWithPromise('GetAnnotations', + { pageIndex: pageIndex }); + }, + + getDestinations: function WorkerTransport_getDestinations() { + return this.messageHandler.sendWithPromise('GetDestinations', null); + }, + + getDestination: function WorkerTransport_getDestination(id) { + return this.messageHandler.sendWithPromise('GetDestination', { id: id } ); + }, + + getAttachments: function WorkerTransport_getAttachments() { + return this.messageHandler.sendWithPromise('GetAttachments', null); + }, + + getJavaScript: function WorkerTransport_getJavaScript() { + return this.messageHandler.sendWithPromise('GetJavaScript', null); + }, + + getOutline: function WorkerTransport_getOutline() { + return this.messageHandler.sendWithPromise('GetOutline', null); + }, + + getMetadata: function WorkerTransport_getMetadata() { + return this.messageHandler.sendWithPromise('GetMetadata', null). + then(function transportMetadata(results) { + return { + info: results[0], + metadata: (results[1] ? new PDFJS.Metadata(results[1]) : null) + }; + }); + }, + + getStats: function WorkerTransport_getStats() { + return this.messageHandler.sendWithPromise('GetStats', null); + }, + + startCleanup: function WorkerTransport_startCleanup() { + this.messageHandler.sendWithPromise('Cleanup', null). + then(function endCleanup() { + for (var i = 0, ii = this.pageCache.length; i < ii; i++) { + var page = this.pageCache[i]; + if (page) { + page.destroy(); + } + } + this.commonObjs.clear(); + FontLoader.clear(); + }.bind(this)); + } + }; + return WorkerTransport; + +})(); + +/** + * A PDF document and page is built of many objects. E.g. there are objects + * for fonts, images, rendering code and such. These objects might get processed + * inside of a worker. The `PDFObjects` implements some basic functions to + * manage these objects. + * @ignore + */ +var PDFObjects = (function PDFObjectsClosure() { + function PDFObjects() { + this.objs = {}; + } + + PDFObjects.prototype = { + /** + * Internal function. + * Ensures there is an object defined for `objId`. + */ + ensureObj: function PDFObjects_ensureObj(objId) { + if (this.objs[objId]) { + return this.objs[objId]; + } + + var obj = { + capability: createPromiseCapability(), + data: null, + resolved: false + }; + this.objs[objId] = obj; + + return obj; + }, + + /** + * If called *without* callback, this returns the data of `objId` but the + * object needs to be resolved. If it isn't, this function throws. + * + * If called *with* a callback, the callback is called with the data of the + * object once the object is resolved. That means, if you call this + * function and the object is already resolved, the callback gets called + * right away. + */ + get: function PDFObjects_get(objId, callback) { + // If there is a callback, then the get can be async and the object is + // not required to be resolved right now + if (callback) { + this.ensureObj(objId).capability.promise.then(callback); + return null; + } + + // If there isn't a callback, the user expects to get the resolved data + // directly. + var obj = this.objs[objId]; + + // If there isn't an object yet or the object isn't resolved, then the + // data isn't ready yet! + if (!obj || !obj.resolved) { + error('Requesting object that isn\'t resolved yet ' + objId); + } + + return obj.data; + }, + + /** + * Resolves the object `objId` with optional `data`. + */ + resolve: function PDFObjects_resolve(objId, data) { + var obj = this.ensureObj(objId); + + obj.resolved = true; + obj.data = data; + obj.capability.resolve(data); + }, + + isResolved: function PDFObjects_isResolved(objId) { + var objs = this.objs; + + if (!objs[objId]) { + return false; + } else { + return objs[objId].resolved; + } + }, + + hasData: function PDFObjects_hasData(objId) { + return this.isResolved(objId); + }, + + /** + * Returns the data of `objId` if object exists, null otherwise. + */ + getData: function PDFObjects_getData(objId) { + var objs = this.objs; + if (!objs[objId] || !objs[objId].resolved) { + return null; + } else { + return objs[objId].data; + } + }, + + clear: function PDFObjects_clear() { + this.objs = {}; + } + }; + return PDFObjects; +})(); + +/** + * Allows controlling of the rendering tasks. + * @class + */ +var RenderTask = (function RenderTaskClosure() { + function RenderTask(internalRenderTask) { + this._internalRenderTask = internalRenderTask; + + /** + * Callback for incremental rendering -- a function that will be called + * each time the rendering is paused. To continue rendering call the + * function that is the first argument to the callback. + * @type {function} + */ + this.onContinue = null; + } + + RenderTask.prototype = /** @lends RenderTask.prototype */ { + /** + * Promise for rendering task completion. + * @return {Promise} + */ + get promise() { + return this._internalRenderTask.capability.promise; + }, + + /** + * Cancels the rendering task. If the task is currently rendering it will + * not be cancelled until graphics pauses with a timeout. The promise that + * this object extends will resolved when cancelled. + */ + cancel: function RenderTask_cancel() { + this._internalRenderTask.cancel(); + }, + + /** + * Registers callbacks to indicate the rendering task completion. + * + * @param {function} onFulfilled The callback for the rendering completion. + * @param {function} onRejected The callback for the rendering failure. + * @return {Promise} A promise that is resolved after the onFulfilled or + * onRejected callback. + */ + then: function RenderTask_then(onFulfilled, onRejected) { + return this.promise.then.apply(this.promise, arguments); + } + }; + + return RenderTask; +})(); + +/** + * For internal use only. + * @ignore + */ +var InternalRenderTask = (function InternalRenderTaskClosure() { + + function InternalRenderTask(callback, params, objs, commonObjs, operatorList, + pageNumber) { + this.callback = callback; + this.params = params; + this.objs = objs; + this.commonObjs = commonObjs; + this.operatorListIdx = null; + this.operatorList = operatorList; + this.pageNumber = pageNumber; + this.running = false; + this.graphicsReadyCallback = null; + this.graphicsReady = false; + this.useRequestAnimationFrame = false; + this.cancelled = false; + this.capability = createPromiseCapability(); + this.task = new RenderTask(this); + // caching this-bound methods + this._continueBound = this._continue.bind(this); + this._scheduleNextBound = this._scheduleNext.bind(this); + this._nextBound = this._next.bind(this); + } + + InternalRenderTask.prototype = { + + initalizeGraphics: + function InternalRenderTask_initalizeGraphics(transparency) { + + if (this.cancelled) { + return; + } + if (PDFJS.pdfBug && 'StepperManager' in globalScope && + globalScope.StepperManager.enabled) { + this.stepper = globalScope.StepperManager.create(this.pageNumber - 1); + this.stepper.init(this.operatorList); + this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint(); + } + + var params = this.params; + this.gfx = new CanvasGraphics(params.canvasContext, this.commonObjs, + this.objs, params.imageLayer); + + this.gfx.beginDrawing(params.viewport, transparency); + this.operatorListIdx = 0; + this.graphicsReady = true; + if (this.graphicsReadyCallback) { + this.graphicsReadyCallback(); + } + }, + + cancel: function InternalRenderTask_cancel() { + this.running = false; + this.cancelled = true; + this.callback('cancelled'); + }, + + operatorListChanged: function InternalRenderTask_operatorListChanged() { + if (!this.graphicsReady) { + if (!this.graphicsReadyCallback) { + this.graphicsReadyCallback = this._continueBound; + } + return; + } + + if (this.stepper) { + this.stepper.updateOperatorList(this.operatorList); + } + + if (this.running) { + return; + } + this._continue(); + }, + + _continue: function InternalRenderTask__continue() { + this.running = true; + if (this.cancelled) { + return; + } + if (this.task.onContinue) { + this.task.onContinue.call(this.task, this._scheduleNextBound); + } else { + this._scheduleNext(); + } + }, + + _scheduleNext: function InternalRenderTask__scheduleNext() { + if (this.useRequestAnimationFrame) { + window.requestAnimationFrame(this._nextBound); + } else { + Promise.resolve(undefined).then(this._nextBound); + } + }, + + _next: function InternalRenderTask__next() { + if (this.cancelled) { + return; + } + this.operatorListIdx = this.gfx.executeOperatorList(this.operatorList, + this.operatorListIdx, + this._continueBound, + this.stepper); + if (this.operatorListIdx === this.operatorList.argsArray.length) { + this.running = false; + if (this.operatorList.lastChunk) { + this.gfx.endDrawing(); + this.callback(); + } + } + } + + }; + + return InternalRenderTask; +})(); + + +var Metadata = PDFJS.Metadata = (function MetadataClosure() { + function fixMetadata(meta) { + return meta.replace(/>\\376\\377([^<]+)/g, function(all, codes) { + var bytes = codes.replace(/\\([0-3])([0-7])([0-7])/g, + function(code, d1, d2, d3) { + return String.fromCharCode(d1 * 64 + d2 * 8 + d3 * 1); + }); + var chars = ''; + for (var i = 0; i < bytes.length; i += 2) { + var code = bytes.charCodeAt(i) * 256 + bytes.charCodeAt(i + 1); + chars += code >= 32 && code < 127 && code !== 60 && code !== 62 && + code !== 38 && false ? String.fromCharCode(code) : + '&#x' + (0x10000 + code).toString(16).substring(1) + ';'; + } + return '>' + chars; + }); + } + + function Metadata(meta) { + if (typeof meta === 'string') { + // Ghostscript produces invalid metadata + meta = fixMetadata(meta); + + var parser = new DOMParser(); + meta = parser.parseFromString(meta, 'application/xml'); + } else if (!(meta instanceof Document)) { + error('Metadata: Invalid metadata object'); + } + + this.metaDocument = meta; + this.metadata = {}; + this.parse(); + } + + Metadata.prototype = { + parse: function Metadata_parse() { + var doc = this.metaDocument; + var rdf = doc.documentElement; + + if (rdf.nodeName.toLowerCase() !== 'rdf:rdf') { // Wrapped in + rdf = rdf.firstChild; + while (rdf && rdf.nodeName.toLowerCase() !== 'rdf:rdf') { + rdf = rdf.nextSibling; + } + } + + var nodeName = (rdf) ? rdf.nodeName.toLowerCase() : null; + if (!rdf || nodeName !== 'rdf:rdf' || !rdf.hasChildNodes()) { + return; + } + + var children = rdf.childNodes, desc, entry, name, i, ii, length, iLength; + for (i = 0, length = children.length; i < length; i++) { + desc = children[i]; + if (desc.nodeName.toLowerCase() !== 'rdf:description') { + continue; + } + + for (ii = 0, iLength = desc.childNodes.length; ii < iLength; ii++) { + if (desc.childNodes[ii].nodeName.toLowerCase() !== '#text') { + entry = desc.childNodes[ii]; + name = entry.nodeName.toLowerCase(); + this.metadata[name] = entry.textContent.trim(); + } + } + } + }, + + get: function Metadata_get(name) { + return this.metadata[name] || null; + }, + + has: function Metadata_has(name) { + return typeof this.metadata[name] !== 'undefined'; + } + }; + + return Metadata; +})(); + + +// contexts store most of the state we need natively. +// However, PDF needs a bit more state, which we store here. + +// Minimal font size that would be used during canvas fillText operations. +var MIN_FONT_SIZE = 16; +// Maximum font size that would be used during canvas fillText operations. +var MAX_FONT_SIZE = 100; +var MAX_GROUP_SIZE = 4096; + +// Heuristic value used when enforcing minimum line widths. +var MIN_WIDTH_FACTOR = 0.65; + +var COMPILE_TYPE3_GLYPHS = true; +var MAX_SIZE_TO_COMPILE = 1000; + +var FULL_CHUNK_HEIGHT = 16; + +function createScratchCanvas(width, height) { + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; +} + +function addContextCurrentTransform(ctx) { + // If the context doesn't expose a `mozCurrentTransform`, add a JS based one. + if (!ctx.mozCurrentTransform) { + ctx._originalSave = ctx.save; + ctx._originalRestore = ctx.restore; + ctx._originalRotate = ctx.rotate; + ctx._originalScale = ctx.scale; + ctx._originalTranslate = ctx.translate; + ctx._originalTransform = ctx.transform; + ctx._originalSetTransform = ctx.setTransform; + + ctx._transformMatrix = ctx._transformMatrix || [1, 0, 0, 1, 0, 0]; + ctx._transformStack = []; + + Object.defineProperty(ctx, 'mozCurrentTransform', { + get: function getCurrentTransform() { + return this._transformMatrix; + } + }); + + Object.defineProperty(ctx, 'mozCurrentTransformInverse', { + get: function getCurrentTransformInverse() { + // Calculation done using WolframAlpha: + // http://www.wolframalpha.com/input/? + // i=Inverse+{{a%2C+c%2C+e}%2C+{b%2C+d%2C+f}%2C+{0%2C+0%2C+1}} + + var m = this._transformMatrix; + var a = m[0], b = m[1], c = m[2], d = m[3], e = m[4], f = m[5]; + + var ad_bc = a * d - b * c; + var bc_ad = b * c - a * d; + + return [ + d / ad_bc, + b / bc_ad, + c / bc_ad, + a / ad_bc, + (d * e - c * f) / bc_ad, + (b * e - a * f) / ad_bc + ]; + } + }); + + ctx.save = function ctxSave() { + var old = this._transformMatrix; + this._transformStack.push(old); + this._transformMatrix = old.slice(0, 6); + + this._originalSave(); + }; + + ctx.restore = function ctxRestore() { + var prev = this._transformStack.pop(); + if (prev) { + this._transformMatrix = prev; + this._originalRestore(); + } + }; + + ctx.translate = function ctxTranslate(x, y) { + var m = this._transformMatrix; + m[4] = m[0] * x + m[2] * y + m[4]; + m[5] = m[1] * x + m[3] * y + m[5]; + + this._originalTranslate(x, y); + }; + + ctx.scale = function ctxScale(x, y) { + var m = this._transformMatrix; + m[0] = m[0] * x; + m[1] = m[1] * x; + m[2] = m[2] * y; + m[3] = m[3] * y; + + this._originalScale(x, y); + }; + + ctx.transform = function ctxTransform(a, b, c, d, e, f) { + var m = this._transformMatrix; + this._transformMatrix = [ + m[0] * a + m[2] * b, + m[1] * a + m[3] * b, + m[0] * c + m[2] * d, + m[1] * c + m[3] * d, + m[0] * e + m[2] * f + m[4], + m[1] * e + m[3] * f + m[5] + ]; + + ctx._originalTransform(a, b, c, d, e, f); + }; + + ctx.setTransform = function ctxSetTransform(a, b, c, d, e, f) { + this._transformMatrix = [a, b, c, d, e, f]; + + ctx._originalSetTransform(a, b, c, d, e, f); + }; + + ctx.rotate = function ctxRotate(angle) { + var cosValue = Math.cos(angle); + var sinValue = Math.sin(angle); + + var m = this._transformMatrix; + this._transformMatrix = [ + m[0] * cosValue + m[2] * sinValue, + m[1] * cosValue + m[3] * sinValue, + m[0] * (-sinValue) + m[2] * cosValue, + m[1] * (-sinValue) + m[3] * cosValue, + m[4], + m[5] + ]; + + this._originalRotate(angle); + }; + } +} + +var CachedCanvases = (function CachedCanvasesClosure() { + var cache = {}; + return { + getCanvas: function CachedCanvases_getCanvas(id, width, height, + trackTransform) { + var canvasEntry; + if (cache[id] !== undefined) { + canvasEntry = cache[id]; + canvasEntry.canvas.width = width; + canvasEntry.canvas.height = height; + // reset canvas transform for emulated mozCurrentTransform, if needed + canvasEntry.context.setTransform(1, 0, 0, 1, 0, 0); + } else { + var canvas = createScratchCanvas(width, height); + var ctx = canvas.getContext('2d'); + if (trackTransform) { + addContextCurrentTransform(ctx); + } + cache[id] = canvasEntry = {canvas: canvas, context: ctx}; + } + return canvasEntry; + }, + clear: function () { + for (var id in cache) { + var canvasEntry = cache[id]; + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + canvasEntry.canvas.width = 0; + canvasEntry.canvas.height = 0; + delete cache[id]; + } + } + }; +})(); + +function compileType3Glyph(imgData) { + var POINT_TO_PROCESS_LIMIT = 1000; + + var width = imgData.width, height = imgData.height; + var i, j, j0, width1 = width + 1; + var points = new Uint8Array(width1 * (height + 1)); + var POINT_TYPES = + new Uint8Array([0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0]); + + // decodes bit-packed mask data + var lineSize = (width + 7) & ~7, data0 = imgData.data; + var data = new Uint8Array(lineSize * height), pos = 0, ii; + for (i = 0, ii = data0.length; i < ii; i++) { + var mask = 128, elem = data0[i]; + while (mask > 0) { + data[pos++] = (elem & mask) ? 0 : 255; + mask >>= 1; + } + } + + // finding iteresting points: every point is located between mask pixels, + // so there will be points of the (width + 1)x(height + 1) grid. Every point + // will have flags assigned based on neighboring mask pixels: + // 4 | 8 + // --P-- + // 2 | 1 + // We are interested only in points with the flags: + // - outside corners: 1, 2, 4, 8; + // - inside corners: 7, 11, 13, 14; + // - and, intersections: 5, 10. + var count = 0; + pos = 0; + if (data[pos] !== 0) { + points[0] = 1; + ++count; + } + for (j = 1; j < width; j++) { + if (data[pos] !== data[pos + 1]) { + points[j] = data[pos] ? 2 : 1; + ++count; + } + pos++; + } + if (data[pos] !== 0) { + points[j] = 2; + ++count; + } + for (i = 1; i < height; i++) { + pos = i * lineSize; + j0 = i * width1; + if (data[pos - lineSize] !== data[pos]) { + points[j0] = data[pos] ? 1 : 8; + ++count; + } + // 'sum' is the position of the current pixel configuration in the 'TYPES' + // array (in order 8-1-2-4, so we can use '>>2' to shift the column). + var sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0); + for (j = 1; j < width; j++) { + sum = (sum >> 2) + (data[pos + 1] ? 4 : 0) + + (data[pos - lineSize + 1] ? 8 : 0); + if (POINT_TYPES[sum]) { + points[j0 + j] = POINT_TYPES[sum]; + ++count; + } + pos++; + } + if (data[pos - lineSize] !== data[pos]) { + points[j0 + j] = data[pos] ? 2 : 4; + ++count; + } + + if (count > POINT_TO_PROCESS_LIMIT) { + return null; + } + } + + pos = lineSize * (height - 1); + j0 = i * width1; + if (data[pos] !== 0) { + points[j0] = 8; + ++count; + } + for (j = 1; j < width; j++) { + if (data[pos] !== data[pos + 1]) { + points[j0 + j] = data[pos] ? 4 : 8; + ++count; + } + pos++; + } + if (data[pos] !== 0) { + points[j0 + j] = 4; + ++count; + } + if (count > POINT_TO_PROCESS_LIMIT) { + return null; + } + + // building outlines + var steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]); + var outlines = []; + for (i = 0; count && i <= height; i++) { + var p = i * width1; + var end = p + width; + while (p < end && !points[p]) { + p++; + } + if (p === end) { + continue; + } + var coords = [p % width1, i]; + + var type = points[p], p0 = p, pp; + do { + var step = steps[type]; + do { + p += step; + } while (!points[p]); + + pp = points[p]; + if (pp !== 5 && pp !== 10) { + // set new direction + type = pp; + // delete mark + points[p] = 0; + } else { // type is 5 or 10, ie, a crossing + // set new direction + type = pp & ((0x33 * type) >> 4); + // set new type for "future hit" + points[p] &= (type >> 2 | type << 2); + } + + coords.push(p % width1); + coords.push((p / width1) | 0); + --count; + } while (p0 !== p); + outlines.push(coords); + --i; + } + + var drawOutline = function(c) { + c.save(); + // the path shall be painted in [0..1]x[0..1] space + c.scale(1 / width, -1 / height); + c.translate(0, -height); + c.beginPath(); + for (var i = 0, ii = outlines.length; i < ii; i++) { + var o = outlines[i]; + c.moveTo(o[0], o[1]); + for (var j = 2, jj = o.length; j < jj; j += 2) { + c.lineTo(o[j], o[j+1]); + } + } + c.fill(); + c.beginPath(); + c.restore(); + }; + + return drawOutline; +} + +var CanvasExtraState = (function CanvasExtraStateClosure() { + function CanvasExtraState(old) { + // Are soft masks and alpha values shapes or opacities? + this.alphaIsShape = false; + this.fontSize = 0; + this.fontSizeScale = 1; + this.textMatrix = IDENTITY_MATRIX; + this.textMatrixScale = 1; + this.fontMatrix = FONT_IDENTITY_MATRIX; + this.leading = 0; + // Current point (in user coordinates) + this.x = 0; + this.y = 0; + // Start of text line (in text coordinates) + this.lineX = 0; + this.lineY = 0; + // Character and word spacing + this.charSpacing = 0; + this.wordSpacing = 0; + this.textHScale = 1; + this.textRenderingMode = TextRenderingMode.FILL; + this.textRise = 0; + // Default fore and background colors + this.fillColor = '#000000'; + this.strokeColor = '#000000'; + this.patternFill = false; + // Note: fill alpha applies to all non-stroking operations + this.fillAlpha = 1; + this.strokeAlpha = 1; + this.lineWidth = 1; + this.activeSMask = null; // nonclonable field (see the save method below) + + this.old = old; + } + + CanvasExtraState.prototype = { + clone: function CanvasExtraState_clone() { + return Object.create(this); + }, + setCurrentPoint: function CanvasExtraState_setCurrentPoint(x, y) { + this.x = x; + this.y = y; + } + }; + return CanvasExtraState; +})(); + +var CanvasGraphics = (function CanvasGraphicsClosure() { + // Defines the time the executeOperatorList is going to be executing + // before it stops and shedules a continue of execution. + var EXECUTION_TIME = 15; + // Defines the number of steps before checking the execution time + var EXECUTION_STEPS = 10; + + function CanvasGraphics(canvasCtx, commonObjs, objs, imageLayer) { + this.ctx = canvasCtx; + this.current = new CanvasExtraState(); + this.stateStack = []; + this.pendingClip = null; + this.pendingEOFill = false; + this.res = null; + this.xobjs = null; + this.commonObjs = commonObjs; + this.objs = objs; + this.imageLayer = imageLayer; + this.groupStack = []; + this.processingType3 = null; + // Patterns are painted relative to the initial page/form transform, see pdf + // spec 8.7.2 NOTE 1. + this.baseTransform = null; + this.baseTransformStack = []; + this.groupLevel = 0; + this.smaskStack = []; + this.smaskCounter = 0; + this.tempSMask = null; + if (canvasCtx) { + // NOTE: if mozCurrentTransform is polyfilled, then the current state of + // the transformation must already be set in canvasCtx._transformMatrix. + addContextCurrentTransform(canvasCtx); + } + this.cachedGetSinglePixelWidth = null; + } + + function putBinaryImageData(ctx, imgData) { + if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) { + ctx.putImageData(imgData, 0, 0); + return; + } + + // Put the image data to the canvas in chunks, rather than putting the + // whole image at once. This saves JS memory, because the ImageData object + // is smaller. It also possibly saves C++ memory within the implementation + // of putImageData(). (E.g. in Firefox we make two short-lived copies of + // the data passed to putImageData()). |n| shouldn't be too small, however, + // because too many putImageData() calls will slow things down. + // + // Note: as written, if the last chunk is partial, the putImageData() call + // will (conceptually) put pixels past the bounds of the canvas. But + // that's ok; any such pixels are ignored. + + var height = imgData.height, width = imgData.width; + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; + + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); + var srcPos = 0, destPos; + var src = imgData.data; + var dest = chunkImgData.data; + var i, j, thisChunkHeight, elemsInThisChunk; + + // There are multiple forms in which the pixel data can be passed, and + // imgData.kind tells us which one this is. + if (imgData.kind === ImageKind.GRAYSCALE_1BPP) { + // Grayscale, 1 bit per pixel (i.e. black-and-white). + var srcLength = src.byteLength; + var dest32 = PDFJS.hasCanvasTypedArrays ? new Uint32Array(dest.buffer) : + new Uint32ArrayView(dest); + var dest32DataLength = dest32.length; + var fullSrcDiff = (width + 7) >> 3; + var white = 0xFFFFFFFF; + var black = (PDFJS.isLittleEndian || !PDFJS.hasCanvasTypedArrays) ? + 0xFF000000 : 0x000000FF; + for (i = 0; i < totalChunks; i++) { + thisChunkHeight = + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; + destPos = 0; + for (j = 0; j < thisChunkHeight; j++) { + var srcDiff = srcLength - srcPos; + var k = 0; + var kEnd = (srcDiff > fullSrcDiff) ? width : srcDiff * 8 - 7; + var kEndUnrolled = kEnd & ~7; + var mask = 0; + var srcByte = 0; + for (; k < kEndUnrolled; k += 8) { + srcByte = src[srcPos++]; + dest32[destPos++] = (srcByte & 128) ? white : black; + dest32[destPos++] = (srcByte & 64) ? white : black; + dest32[destPos++] = (srcByte & 32) ? white : black; + dest32[destPos++] = (srcByte & 16) ? white : black; + dest32[destPos++] = (srcByte & 8) ? white : black; + dest32[destPos++] = (srcByte & 4) ? white : black; + dest32[destPos++] = (srcByte & 2) ? white : black; + dest32[destPos++] = (srcByte & 1) ? white : black; + } + for (; k < kEnd; k++) { + if (mask === 0) { + srcByte = src[srcPos++]; + mask = 128; + } + + dest32[destPos++] = (srcByte & mask) ? white : black; + mask >>= 1; + } + } + // We ran out of input. Make all remaining pixels transparent. + while (destPos < dest32DataLength) { + dest32[destPos++] = 0; + } + + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); + } + } else if (imgData.kind === ImageKind.RGBA_32BPP) { + // RGBA, 32-bits per pixel. + + j = 0; + elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4; + for (i = 0; i < fullChunks; i++) { + dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); + srcPos += elemsInThisChunk; + + ctx.putImageData(chunkImgData, 0, j); + j += FULL_CHUNK_HEIGHT; + } + if (i < totalChunks) { + elemsInThisChunk = width * partialChunkHeight * 4; + dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); + ctx.putImageData(chunkImgData, 0, j); + } + + } else if (imgData.kind === ImageKind.RGB_24BPP) { + // RGB, 24-bits per pixel. + thisChunkHeight = FULL_CHUNK_HEIGHT; + elemsInThisChunk = width * thisChunkHeight; + for (i = 0; i < totalChunks; i++) { + if (i >= fullChunks) { + thisChunkHeight = partialChunkHeight; + elemsInThisChunk = width * thisChunkHeight; + } + + destPos = 0; + for (j = elemsInThisChunk; j--;) { + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = 255; + } + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); + } + } else { + error('bad image kind: ' + imgData.kind); + } + } + + function putBinaryImageMask(ctx, imgData) { + var height = imgData.height, width = imgData.width; + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; + + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); + var srcPos = 0; + var src = imgData.data; + var dest = chunkImgData.data; + + for (var i = 0; i < totalChunks; i++) { + var thisChunkHeight = + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; + + // Expand the mask so it can be used by the canvas. Any required + // inversion has already been handled. + var destPos = 3; // alpha component offset + for (var j = 0; j < thisChunkHeight; j++) { + var mask = 0; + for (var k = 0; k < width; k++) { + if (!mask) { + var elem = src[srcPos++]; + mask = 128; + } + dest[destPos] = (elem & mask) ? 0 : 255; + destPos += 4; + mask >>= 1; + } + } + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); + } + } + + function copyCtxState(sourceCtx, destCtx) { + var properties = ['strokeStyle', 'fillStyle', 'fillRule', 'globalAlpha', + 'lineWidth', 'lineCap', 'lineJoin', 'miterLimit', + 'globalCompositeOperation', 'font']; + for (var i = 0, ii = properties.length; i < ii; i++) { + var property = properties[i]; + if (sourceCtx[property] !== undefined) { + destCtx[property] = sourceCtx[property]; + } + } + if (sourceCtx.setLineDash !== undefined) { + destCtx.setLineDash(sourceCtx.getLineDash()); + destCtx.lineDashOffset = sourceCtx.lineDashOffset; + } else if (sourceCtx.mozDashOffset !== undefined) { + destCtx.mozDash = sourceCtx.mozDash; + destCtx.mozDashOffset = sourceCtx.mozDashOffset; + } + } + + function composeSMaskBackdrop(bytes, r0, g0, b0) { + var length = bytes.length; + for (var i = 3; i < length; i += 4) { + var alpha = bytes[i]; + if (alpha === 0) { + bytes[i - 3] = r0; + bytes[i - 2] = g0; + bytes[i - 1] = b0; + } else if (alpha < 255) { + var alpha_ = 255 - alpha; + bytes[i - 3] = (bytes[i - 3] * alpha + r0 * alpha_) >> 8; + bytes[i - 2] = (bytes[i - 2] * alpha + g0 * alpha_) >> 8; + bytes[i - 1] = (bytes[i - 1] * alpha + b0 * alpha_) >> 8; + } + } + } + + function composeSMaskAlpha(maskData, layerData) { + var length = maskData.length; + var scale = 1 / 255; + for (var i = 3; i < length; i += 4) { + var alpha = maskData[i]; + layerData[i] = (layerData[i] * alpha * scale) | 0; + } + } + + function composeSMaskLuminosity(maskData, layerData) { + var length = maskData.length; + for (var i = 3; i < length; i += 4) { + var y = (maskData[i - 3] * 77) + // * 0.3 / 255 * 0x10000 + (maskData[i - 2] * 152) + // * 0.59 .... + (maskData[i - 1] * 28); // * 0.11 .... + layerData[i] = (layerData[i] * y) >> 16; + } + } + + function genericComposeSMask(maskCtx, layerCtx, width, height, + subtype, backdrop) { + var hasBackdrop = !!backdrop; + var r0 = hasBackdrop ? backdrop[0] : 0; + var g0 = hasBackdrop ? backdrop[1] : 0; + var b0 = hasBackdrop ? backdrop[2] : 0; + + var composeFn; + if (subtype === 'Luminosity') { + composeFn = composeSMaskLuminosity; + } else { + composeFn = composeSMaskAlpha; + } + + // processing image in chunks to save memory + var PIXELS_TO_PROCESS = 1048576; + var chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width)); + for (var row = 0; row < height; row += chunkSize) { + var chunkHeight = Math.min(chunkSize, height - row); + var maskData = maskCtx.getImageData(0, row, width, chunkHeight); + var layerData = layerCtx.getImageData(0, row, width, chunkHeight); + + if (hasBackdrop) { + composeSMaskBackdrop(maskData.data, r0, g0, b0); + } + composeFn(maskData.data, layerData.data); + + maskCtx.putImageData(layerData, 0, row); + } + } + + function composeSMask(ctx, smask, layerCtx) { + var mask = smask.canvas; + var maskCtx = smask.context; + + ctx.setTransform(smask.scaleX, 0, 0, smask.scaleY, + smask.offsetX, smask.offsetY); + + var backdrop = smask.backdrop || null; + if (WebGLUtils.isEnabled) { + var composed = WebGLUtils.composeSMask(layerCtx.canvas, mask, + {subtype: smask.subtype, backdrop: backdrop}); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.drawImage(composed, smask.offsetX, smask.offsetY); + return; + } + genericComposeSMask(maskCtx, layerCtx, mask.width, mask.height, + smask.subtype, backdrop); + ctx.drawImage(mask, 0, 0); + } + + var LINE_CAP_STYLES = ['butt', 'round', 'square']; + var LINE_JOIN_STYLES = ['miter', 'round', 'bevel']; + var NORMAL_CLIP = {}; + var EO_CLIP = {}; + + CanvasGraphics.prototype = { + + beginDrawing: function CanvasGraphics_beginDrawing(viewport, transparency) { + // For pdfs that use blend modes we have to clear the canvas else certain + // blend modes can look wrong since we'd be blending with a white + // backdrop. The problem with a transparent backdrop though is we then + // don't get sub pixel anti aliasing on text, so we fill with white if + // we can. + var width = this.ctx.canvas.width; + var height = this.ctx.canvas.height; + if (transparency) { + this.ctx.clearRect(0, 0, width, height); + } else { + this.ctx.mozOpaque = true; + this.ctx.save(); + this.ctx.fillStyle = 'rgb(255, 255, 255)'; + this.ctx.fillRect(0, 0, width, height); + this.ctx.restore(); + } + + var transform = viewport.transform; + + this.ctx.save(); + this.ctx.transform.apply(this.ctx, transform); + + this.baseTransform = this.ctx.mozCurrentTransform.slice(); + + if (this.imageLayer) { + this.imageLayer.beginLayout(); + } + }, + + executeOperatorList: function CanvasGraphics_executeOperatorList( + operatorList, + executionStartIdx, continueCallback, + stepper) { + var argsArray = operatorList.argsArray; + var fnArray = operatorList.fnArray; + var i = executionStartIdx || 0; + var argsArrayLen = argsArray.length; + + // Sometimes the OperatorList to execute is empty. + if (argsArrayLen === i) { + return i; + } + + var chunkOperations = (argsArrayLen - i > EXECUTION_STEPS && + typeof continueCallback === 'function'); + var endTime = chunkOperations ? Date.now() + EXECUTION_TIME : 0; + var steps = 0; + + var commonObjs = this.commonObjs; + var objs = this.objs; + var fnId; + + while (true) { + if (stepper !== undefined && i === stepper.nextBreakPoint) { + stepper.breakIt(i, continueCallback); + return i; + } + + fnId = fnArray[i]; + + if (fnId !== OPS.dependency) { + this[fnId].apply(this, argsArray[i]); + } else { + var deps = argsArray[i]; + for (var n = 0, nn = deps.length; n < nn; n++) { + var depObjId = deps[n]; + var common = depObjId[0] === 'g' && depObjId[1] === '_'; + var objsPool = common ? commonObjs : objs; + + // If the promise isn't resolved yet, add the continueCallback + // to the promise and bail out. + if (!objsPool.isResolved(depObjId)) { + objsPool.get(depObjId, continueCallback); + return i; + } + } + } + + i++; + + // If the entire operatorList was executed, stop as were done. + if (i === argsArrayLen) { + return i; + } + + // If the execution took longer then a certain amount of time and + // `continueCallback` is specified, interrupt the execution. + if (chunkOperations && ++steps > EXECUTION_STEPS) { + if (Date.now() > endTime) { + continueCallback(); + return i; + } + steps = 0; + } + + // If the operatorList isn't executed completely yet OR the execution + // time was short enough, do another execution round. + } + }, + + endDrawing: function CanvasGraphics_endDrawing() { + this.ctx.restore(); + CachedCanvases.clear(); + WebGLUtils.clear(); + + if (this.imageLayer) { + this.imageLayer.endLayout(); + } + }, + + // Graphics state + setLineWidth: function CanvasGraphics_setLineWidth(width) { + this.current.lineWidth = width; + this.ctx.lineWidth = width; + }, + setLineCap: function CanvasGraphics_setLineCap(style) { + this.ctx.lineCap = LINE_CAP_STYLES[style]; + }, + setLineJoin: function CanvasGraphics_setLineJoin(style) { + this.ctx.lineJoin = LINE_JOIN_STYLES[style]; + }, + setMiterLimit: function CanvasGraphics_setMiterLimit(limit) { + this.ctx.miterLimit = limit; + }, + setDash: function CanvasGraphics_setDash(dashArray, dashPhase) { + var ctx = this.ctx; + if (ctx.setLineDash !== undefined) { + ctx.setLineDash(dashArray); + ctx.lineDashOffset = dashPhase; + } else { + ctx.mozDash = dashArray; + ctx.mozDashOffset = dashPhase; + } + }, + setRenderingIntent: function CanvasGraphics_setRenderingIntent(intent) { + // Maybe if we one day fully support color spaces this will be important + // for now we can ignore. + // TODO set rendering intent? + }, + setFlatness: function CanvasGraphics_setFlatness(flatness) { + // There's no way to control this with canvas, but we can safely ignore. + // TODO set flatness? + }, + setGState: function CanvasGraphics_setGState(states) { + for (var i = 0, ii = states.length; i < ii; i++) { + var state = states[i]; + var key = state[0]; + var value = state[1]; + + switch (key) { + case 'LW': + this.setLineWidth(value); + break; + case 'LC': + this.setLineCap(value); + break; + case 'LJ': + this.setLineJoin(value); + break; + case 'ML': + this.setMiterLimit(value); + break; + case 'D': + this.setDash(value[0], value[1]); + break; + case 'RI': + this.setRenderingIntent(value); + break; + case 'FL': + this.setFlatness(value); + break; + case 'Font': + this.setFont(value[0], value[1]); + break; + case 'CA': + this.current.strokeAlpha = state[1]; + break; + case 'ca': + this.current.fillAlpha = state[1]; + this.ctx.globalAlpha = state[1]; + break; + case 'BM': + if (value && value.name && (value.name !== 'Normal')) { + var mode = value.name.replace(/([A-Z])/g, + function(c) { + return '-' + c.toLowerCase(); + } + ).substring(1); + this.ctx.globalCompositeOperation = mode; + if (this.ctx.globalCompositeOperation !== mode) { + warn('globalCompositeOperation "' + mode + + '" is not supported'); + } + } else { + this.ctx.globalCompositeOperation = 'source-over'; + } + break; + case 'SMask': + if (this.current.activeSMask) { + this.endSMaskGroup(); + } + this.current.activeSMask = value ? this.tempSMask : null; + if (this.current.activeSMask) { + this.beginSMaskGroup(); + } + this.tempSMask = null; + break; + } + } + }, + beginSMaskGroup: function CanvasGraphics_beginSMaskGroup() { + + var activeSMask = this.current.activeSMask; + var drawnWidth = activeSMask.canvas.width; + var drawnHeight = activeSMask.canvas.height; + var cacheId = 'smaskGroupAt' + this.groupLevel; + var scratchCanvas = CachedCanvases.getCanvas( + cacheId, drawnWidth, drawnHeight, true); + + var currentCtx = this.ctx; + var currentTransform = currentCtx.mozCurrentTransform; + this.ctx.save(); + + var groupCtx = scratchCanvas.context; + groupCtx.scale(1 / activeSMask.scaleX, 1 / activeSMask.scaleY); + groupCtx.translate(-activeSMask.offsetX, -activeSMask.offsetY); + groupCtx.transform.apply(groupCtx, currentTransform); + + copyCtxState(currentCtx, groupCtx); + this.ctx = groupCtx; + this.setGState([ + ['BM', 'Normal'], + ['ca', 1], + ['CA', 1] + ]); + this.groupStack.push(currentCtx); + this.groupLevel++; + }, + endSMaskGroup: function CanvasGraphics_endSMaskGroup() { + var groupCtx = this.ctx; + this.groupLevel--; + this.ctx = this.groupStack.pop(); + + composeSMask(this.ctx, this.current.activeSMask, groupCtx); + this.ctx.restore(); + }, + save: function CanvasGraphics_save() { + this.ctx.save(); + var old = this.current; + this.stateStack.push(old); + this.current = old.clone(); + this.current.activeSMask = null; + }, + restore: function CanvasGraphics_restore() { + if (this.stateStack.length !== 0) { + if (this.current.activeSMask !== null) { + this.endSMaskGroup(); + } + + this.current = this.stateStack.pop(); + this.ctx.restore(); + + this.cachedGetSinglePixelWidth = null; + } + }, + transform: function CanvasGraphics_transform(a, b, c, d, e, f) { + this.ctx.transform(a, b, c, d, e, f); + + this.cachedGetSinglePixelWidth = null; + }, + + // Path + constructPath: function CanvasGraphics_constructPath(ops, args) { + var ctx = this.ctx; + var current = this.current; + var x = current.x, y = current.y; + for (var i = 0, j = 0, ii = ops.length; i < ii; i++) { + switch (ops[i] | 0) { + case OPS.rectangle: + x = args[j++]; + y = args[j++]; + var width = args[j++]; + var height = args[j++]; + if (width === 0) { + width = this.getSinglePixelWidth(); + } + if (height === 0) { + height = this.getSinglePixelWidth(); + } + var xw = x + width; + var yh = y + height; + this.ctx.moveTo(x, y); + this.ctx.lineTo(xw, y); + this.ctx.lineTo(xw, yh); + this.ctx.lineTo(x, yh); + this.ctx.lineTo(x, y); + this.ctx.closePath(); + break; + case OPS.moveTo: + x = args[j++]; + y = args[j++]; + ctx.moveTo(x, y); + break; + case OPS.lineTo: + x = args[j++]; + y = args[j++]; + ctx.lineTo(x, y); + break; + case OPS.curveTo: + x = args[j + 4]; + y = args[j + 5]; + ctx.bezierCurveTo(args[j], args[j + 1], args[j + 2], args[j + 3], + x, y); + j += 6; + break; + case OPS.curveTo2: + ctx.bezierCurveTo(x, y, args[j], args[j + 1], + args[j + 2], args[j + 3]); + x = args[j + 2]; + y = args[j + 3]; + j += 4; + break; + case OPS.curveTo3: + x = args[j + 2]; + y = args[j + 3]; + ctx.bezierCurveTo(args[j], args[j + 1], x, y, x, y); + j += 4; + break; + case OPS.closePath: + ctx.closePath(); + break; + } + } + current.setCurrentPoint(x, y); + }, + closePath: function CanvasGraphics_closePath() { + this.ctx.closePath(); + }, + stroke: function CanvasGraphics_stroke(consumePath) { + consumePath = typeof consumePath !== 'undefined' ? consumePath : true; + var ctx = this.ctx; + var strokeColor = this.current.strokeColor; + // Prevent drawing too thin lines by enforcing a minimum line width. + ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, + this.current.lineWidth); + // For stroke we want to temporarily change the global alpha to the + // stroking alpha. + ctx.globalAlpha = this.current.strokeAlpha; + if (strokeColor && strokeColor.hasOwnProperty('type') && + strokeColor.type === 'Pattern') { + // for patterns, we transform to pattern space, calculate + // the pattern, call stroke, and restore to user space + ctx.save(); + ctx.strokeStyle = strokeColor.getPattern(ctx, this); + ctx.stroke(); + ctx.restore(); + } else { + ctx.stroke(); + } + if (consumePath) { + this.consumePath(); + } + // Restore the global alpha to the fill alpha + ctx.globalAlpha = this.current.fillAlpha; + }, + closeStroke: function CanvasGraphics_closeStroke() { + this.closePath(); + this.stroke(); + }, + fill: function CanvasGraphics_fill(consumePath) { + consumePath = typeof consumePath !== 'undefined' ? consumePath : true; + var ctx = this.ctx; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; + var needRestore = false; + + if (isPatternFill) { + ctx.save(); + ctx.fillStyle = fillColor.getPattern(ctx, this); + needRestore = true; + } + + if (this.pendingEOFill) { + if (ctx.mozFillRule !== undefined) { + ctx.mozFillRule = 'evenodd'; + ctx.fill(); + ctx.mozFillRule = 'nonzero'; + } else { + try { + ctx.fill('evenodd'); + } catch (ex) { + // shouldn't really happen, but browsers might think differently + ctx.fill(); + } + } + this.pendingEOFill = false; + } else { + ctx.fill(); + } + + if (needRestore) { + ctx.restore(); + } + if (consumePath) { + this.consumePath(); + } + }, + eoFill: function CanvasGraphics_eoFill() { + this.pendingEOFill = true; + this.fill(); + }, + fillStroke: function CanvasGraphics_fillStroke() { + this.fill(false); + this.stroke(false); + + this.consumePath(); + }, + eoFillStroke: function CanvasGraphics_eoFillStroke() { + this.pendingEOFill = true; + this.fillStroke(); + }, + closeFillStroke: function CanvasGraphics_closeFillStroke() { + this.closePath(); + this.fillStroke(); + }, + closeEOFillStroke: function CanvasGraphics_closeEOFillStroke() { + this.pendingEOFill = true; + this.closePath(); + this.fillStroke(); + }, + endPath: function CanvasGraphics_endPath() { + this.consumePath(); + }, + + // Clipping + clip: function CanvasGraphics_clip() { + this.pendingClip = NORMAL_CLIP; + }, + eoClip: function CanvasGraphics_eoClip() { + this.pendingClip = EO_CLIP; + }, + + // Text + beginText: function CanvasGraphics_beginText() { + this.current.textMatrix = IDENTITY_MATRIX; + this.current.textMatrixScale = 1; + this.current.x = this.current.lineX = 0; + this.current.y = this.current.lineY = 0; + }, + endText: function CanvasGraphics_endText() { + var paths = this.pendingTextPaths; + var ctx = this.ctx; + if (paths === undefined) { + ctx.beginPath(); + return; + } + + ctx.save(); + ctx.beginPath(); + for (var i = 0; i < paths.length; i++) { + var path = paths[i]; + ctx.setTransform.apply(ctx, path.transform); + ctx.translate(path.x, path.y); + path.addToPath(ctx, path.fontSize); + } + ctx.restore(); + ctx.clip(); + ctx.beginPath(); + delete this.pendingTextPaths; + }, + setCharSpacing: function CanvasGraphics_setCharSpacing(spacing) { + this.current.charSpacing = spacing; + }, + setWordSpacing: function CanvasGraphics_setWordSpacing(spacing) { + this.current.wordSpacing = spacing; + }, + setHScale: function CanvasGraphics_setHScale(scale) { + this.current.textHScale = scale / 100; + }, + setLeading: function CanvasGraphics_setLeading(leading) { + this.current.leading = -leading; + }, + setFont: function CanvasGraphics_setFont(fontRefName, size) { + var fontObj = this.commonObjs.get(fontRefName); + var current = this.current; + + if (!fontObj) { + error('Can\'t find font for ' + fontRefName); + } + + current.fontMatrix = (fontObj.fontMatrix ? + fontObj.fontMatrix : FONT_IDENTITY_MATRIX); + + // A valid matrix needs all main diagonal elements to be non-zero + // This also ensures we bypass FF bugzilla bug #719844. + if (current.fontMatrix[0] === 0 || + current.fontMatrix[3] === 0) { + warn('Invalid font matrix for font ' + fontRefName); + } + + // The spec for Tf (setFont) says that 'size' specifies the font 'scale', + // and in some docs this can be negative (inverted x-y axes). + if (size < 0) { + size = -size; + current.fontDirection = -1; + } else { + current.fontDirection = 1; + } + + this.current.font = fontObj; + this.current.fontSize = size; + + if (fontObj.isType3Font) { + return; // we don't need ctx.font for Type3 fonts + } + + var name = fontObj.loadedName || 'sans-serif'; + var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') : + (fontObj.bold ? 'bold' : 'normal'); + + var italic = fontObj.italic ? 'italic' : 'normal'; + var typeface = '"' + name + '", ' + fontObj.fallbackName; + + // Some font backends cannot handle fonts below certain size. + // Keeping the font at minimal size and using the fontSizeScale to change + // the current transformation matrix before the fillText/strokeText. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=726227 + var browserFontSize = size < MIN_FONT_SIZE ? MIN_FONT_SIZE : + size > MAX_FONT_SIZE ? MAX_FONT_SIZE : size; + this.current.fontSizeScale = size / browserFontSize; + + var rule = italic + ' ' + bold + ' ' + browserFontSize + 'px ' + typeface; + this.ctx.font = rule; + }, + setTextRenderingMode: function CanvasGraphics_setTextRenderingMode(mode) { + this.current.textRenderingMode = mode; + }, + setTextRise: function CanvasGraphics_setTextRise(rise) { + this.current.textRise = rise; + }, + moveText: function CanvasGraphics_moveText(x, y) { + this.current.x = this.current.lineX += x; + this.current.y = this.current.lineY += y; + }, + setLeadingMoveText: function CanvasGraphics_setLeadingMoveText(x, y) { + this.setLeading(-y); + this.moveText(x, y); + }, + setTextMatrix: function CanvasGraphics_setTextMatrix(a, b, c, d, e, f) { + this.current.textMatrix = [a, b, c, d, e, f]; + this.current.textMatrixScale = Math.sqrt(a * a + b * b); + + this.current.x = this.current.lineX = 0; + this.current.y = this.current.lineY = 0; + }, + nextLine: function CanvasGraphics_nextLine() { + this.moveText(0, this.current.leading); + }, + + paintChar: function CanvasGraphics_paintChar(character, x, y) { + var ctx = this.ctx; + var current = this.current; + var font = current.font; + var textRenderingMode = current.textRenderingMode; + var fontSize = current.fontSize / current.fontSizeScale; + var fillStrokeMode = textRenderingMode & + TextRenderingMode.FILL_STROKE_MASK; + var isAddToPathSet = !!(textRenderingMode & + TextRenderingMode.ADD_TO_PATH_FLAG); + + var addToPath; + if (font.disableFontFace || isAddToPathSet) { + addToPath = font.getPathGenerator(this.commonObjs, character); + } + + if (font.disableFontFace) { + ctx.save(); + ctx.translate(x, y); + ctx.beginPath(); + addToPath(ctx, fontSize); + if (fillStrokeMode === TextRenderingMode.FILL || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.fill(); + } + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.stroke(); + } + ctx.restore(); + } else { + if (fillStrokeMode === TextRenderingMode.FILL || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.fillText(character, x, y); + } + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + ctx.strokeText(character, x, y); + } + } + + if (isAddToPathSet) { + var paths = this.pendingTextPaths || (this.pendingTextPaths = []); + paths.push({ + transform: ctx.mozCurrentTransform, + x: x, + y: y, + fontSize: fontSize, + addToPath: addToPath + }); + } + }, + + get isFontSubpixelAAEnabled() { + // Checks if anti-aliasing is enabled when scaled text is painted. + // On Windows GDI scaled fonts looks bad. + var ctx = document.createElement('canvas').getContext('2d'); + ctx.scale(1.5, 1); + ctx.fillText('I', 0, 10); + var data = ctx.getImageData(0, 0, 10, 10).data; + var enabled = false; + for (var i = 3; i < data.length; i += 4) { + if (data[i] > 0 && data[i] < 255) { + enabled = true; + break; + } + } + return shadow(this, 'isFontSubpixelAAEnabled', enabled); + }, + + showText: function CanvasGraphics_showText(glyphs) { + var current = this.current; + var font = current.font; + if (font.isType3Font) { + return this.showType3Text(glyphs); + } + + var fontSize = current.fontSize; + if (fontSize === 0) { + return; + } + + var ctx = this.ctx; + var fontSizeScale = current.fontSizeScale; + var charSpacing = current.charSpacing; + var wordSpacing = current.wordSpacing; + var fontDirection = current.fontDirection; + var textHScale = current.textHScale * fontDirection; + var glyphsLength = glyphs.length; + var vertical = font.vertical; + var defaultVMetrics = font.defaultVMetrics; + var widthAdvanceScale = fontSize * current.fontMatrix[0]; + + var simpleFillText = + current.textRenderingMode === TextRenderingMode.FILL && + !font.disableFontFace; + + ctx.save(); + ctx.transform.apply(ctx, current.textMatrix); + ctx.translate(current.x, current.y + current.textRise); + + if (fontDirection > 0) { + ctx.scale(textHScale, -1); + } else { + ctx.scale(textHScale, 1); + } + + var lineWidth = current.lineWidth; + var scale = current.textMatrixScale; + if (scale === 0 || lineWidth === 0) { + var fillStrokeMode = current.textRenderingMode & + TextRenderingMode.FILL_STROKE_MASK; + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + this.cachedGetSinglePixelWidth = null; + lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR; + } + } else { + lineWidth /= scale; + } + + if (fontSizeScale !== 1.0) { + ctx.scale(fontSizeScale, fontSizeScale); + lineWidth /= fontSizeScale; + } + + ctx.lineWidth = lineWidth; + + var x = 0, i; + for (i = 0; i < glyphsLength; ++i) { + var glyph = glyphs[i]; + if (glyph === null) { + // word break + x += fontDirection * wordSpacing; + continue; + } else if (isNum(glyph)) { + x += -glyph * fontSize * 0.001; + continue; + } + + var restoreNeeded = false; + var character = glyph.fontChar; + var accent = glyph.accent; + var scaledX, scaledY, scaledAccentX, scaledAccentY; + var width = glyph.width; + if (vertical) { + var vmetric, vx, vy; + vmetric = glyph.vmetric || defaultVMetrics; + vx = glyph.vmetric ? vmetric[1] : width * 0.5; + vx = -vx * widthAdvanceScale; + vy = vmetric[2] * widthAdvanceScale; + + width = vmetric ? -vmetric[0] : width; + scaledX = vx / fontSizeScale; + scaledY = (x + vy) / fontSizeScale; + } else { + scaledX = x / fontSizeScale; + scaledY = 0; + } + + if (font.remeasure && width > 0 && this.isFontSubpixelAAEnabled) { + // some standard fonts may not have the exact width, trying to + // rescale per character + var measuredWidth = ctx.measureText(character).width * 1000 / + fontSize * fontSizeScale; + var characterScaleX = width / measuredWidth; + restoreNeeded = true; + ctx.save(); + ctx.scale(characterScaleX, 1); + scaledX /= characterScaleX; + } + + if (simpleFillText && !accent) { + // common case + ctx.fillText(character, scaledX, scaledY); + } else { + this.paintChar(character, scaledX, scaledY); + if (accent) { + scaledAccentX = scaledX + accent.offset.x / fontSizeScale; + scaledAccentY = scaledY - accent.offset.y / fontSizeScale; + this.paintChar(accent.fontChar, scaledAccentX, scaledAccentY); + } + } + + var charWidth = width * widthAdvanceScale + charSpacing * fontDirection; + x += charWidth; + + if (restoreNeeded) { + ctx.restore(); + } + } + if (vertical) { + current.y -= x * textHScale; + } else { + current.x += x * textHScale; + } + ctx.restore(); + }, + + showType3Text: function CanvasGraphics_showType3Text(glyphs) { + // Type3 fonts - each glyph is a "mini-PDF" + var ctx = this.ctx; + var current = this.current; + var font = current.font; + var fontSize = current.fontSize; + var fontDirection = current.fontDirection; + var charSpacing = current.charSpacing; + var wordSpacing = current.wordSpacing; + var textHScale = current.textHScale * fontDirection; + var fontMatrix = current.fontMatrix || FONT_IDENTITY_MATRIX; + var glyphsLength = glyphs.length; + var isTextInvisible = + current.textRenderingMode === TextRenderingMode.INVISIBLE; + var i, glyph, width; + + if (isTextInvisible || fontSize === 0) { + return; + } + + ctx.save(); + ctx.transform.apply(ctx, current.textMatrix); + ctx.translate(current.x, current.y); + + ctx.scale(textHScale, fontDirection); + + for (i = 0; i < glyphsLength; ++i) { + glyph = glyphs[i]; + if (glyph === null) { + // word break + this.ctx.translate(wordSpacing, 0); + current.x += wordSpacing * textHScale; + continue; + } else if (isNum(glyph)) { + var spacingLength = -glyph * 0.001 * fontSize; + this.ctx.translate(spacingLength, 0); + current.x += spacingLength * textHScale; + continue; + } + + var operatorList = font.charProcOperatorList[glyph.operatorListId]; + if (!operatorList) { + warn('Type3 character \"' + glyph.operatorListId + + '\" is not available'); + continue; + } + this.processingType3 = glyph; + this.save(); + ctx.scale(fontSize, fontSize); + ctx.transform.apply(ctx, fontMatrix); + this.executeOperatorList(operatorList); + this.restore(); + + var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); + width = transformed[0] * fontSize + charSpacing; + + ctx.translate(width, 0); + current.x += width * textHScale; + } + ctx.restore(); + this.processingType3 = null; + }, + + // Type3 fonts + setCharWidth: function CanvasGraphics_setCharWidth(xWidth, yWidth) { + // We can safely ignore this since the width should be the same + // as the width in the Widths array. + }, + setCharWidthAndBounds: function CanvasGraphics_setCharWidthAndBounds(xWidth, + yWidth, + llx, + lly, + urx, + ury) { + // TODO According to the spec we're also suppose to ignore any operators + // that set color or include images while processing this type3 font. + this.ctx.rect(llx, lly, urx - llx, ury - lly); + this.clip(); + this.endPath(); + }, + + // Color + getColorN_Pattern: function CanvasGraphics_getColorN_Pattern(IR) { + var pattern; + if (IR[0] === 'TilingPattern') { + var color = IR[1]; + pattern = new TilingPattern(IR, color, this.ctx, this.objs, + this.commonObjs, this.baseTransform); + } else { + pattern = getShadingPatternFromIR(IR); + } + return pattern; + }, + setStrokeColorN: function CanvasGraphics_setStrokeColorN(/*...*/) { + this.current.strokeColor = this.getColorN_Pattern(arguments); + }, + setFillColorN: function CanvasGraphics_setFillColorN(/*...*/) { + this.current.fillColor = this.getColorN_Pattern(arguments); + this.current.patternFill = true; + }, + setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) { + var color = Util.makeCssRgb(r, g, b); + this.ctx.strokeStyle = color; + this.current.strokeColor = color; + }, + setFillRGBColor: function CanvasGraphics_setFillRGBColor(r, g, b) { + var color = Util.makeCssRgb(r, g, b); + this.ctx.fillStyle = color; + this.current.fillColor = color; + this.current.patternFill = false; + }, + + shadingFill: function CanvasGraphics_shadingFill(patternIR) { + var ctx = this.ctx; + + this.save(); + var pattern = getShadingPatternFromIR(patternIR); + ctx.fillStyle = pattern.getPattern(ctx, this, true); + + var inv = ctx.mozCurrentTransformInverse; + if (inv) { + var canvas = ctx.canvas; + var width = canvas.width; + var height = canvas.height; + + var bl = Util.applyTransform([0, 0], inv); + var br = Util.applyTransform([0, height], inv); + var ul = Util.applyTransform([width, 0], inv); + var ur = Util.applyTransform([width, height], inv); + + var x0 = Math.min(bl[0], br[0], ul[0], ur[0]); + var y0 = Math.min(bl[1], br[1], ul[1], ur[1]); + var x1 = Math.max(bl[0], br[0], ul[0], ur[0]); + var y1 = Math.max(bl[1], br[1], ul[1], ur[1]); + + this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0); + } else { + // HACK to draw the gradient onto an infinite rectangle. + // PDF gradients are drawn across the entire image while + // Canvas only allows gradients to be drawn in a rectangle + // The following bug should allow us to remove this. + // https://bugzilla.mozilla.org/show_bug.cgi?id=664884 + + this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); + } + + this.restore(); + }, + + // Images + beginInlineImage: function CanvasGraphics_beginInlineImage() { + error('Should not call beginInlineImage'); + }, + beginImageData: function CanvasGraphics_beginImageData() { + error('Should not call beginImageData'); + }, + + paintFormXObjectBegin: function CanvasGraphics_paintFormXObjectBegin(matrix, + bbox) { + this.save(); + this.baseTransformStack.push(this.baseTransform); + + if (isArray(matrix) && 6 === matrix.length) { + this.transform.apply(this, matrix); + } + + this.baseTransform = this.ctx.mozCurrentTransform; + + if (isArray(bbox) && 4 === bbox.length) { + var width = bbox[2] - bbox[0]; + var height = bbox[3] - bbox[1]; + this.ctx.rect(bbox[0], bbox[1], width, height); + this.clip(); + this.endPath(); + } + }, + + paintFormXObjectEnd: function CanvasGraphics_paintFormXObjectEnd() { + this.restore(); + this.baseTransform = this.baseTransformStack.pop(); + }, + + beginGroup: function CanvasGraphics_beginGroup(group) { + this.save(); + var currentCtx = this.ctx; + // TODO non-isolated groups - according to Rik at adobe non-isolated + // group results aren't usually that different and they even have tools + // that ignore this setting. Notes from Rik on implmenting: + // - When you encounter an transparency group, create a new canvas with + // the dimensions of the bbox + // - copy the content from the previous canvas to the new canvas + // - draw as usual + // - remove the backdrop alpha: + // alphaNew = 1 - (1 - alpha)/(1 - alphaBackdrop) with 'alpha' the alpha + // value of your transparency group and 'alphaBackdrop' the alpha of the + // backdrop + // - remove background color: + // colorNew = color - alphaNew *colorBackdrop /(1 - alphaNew) + if (!group.isolated) { + info('TODO: Support non-isolated groups.'); + } + + // TODO knockout - supposedly possible with the clever use of compositing + // modes. + if (group.knockout) { + warn('Knockout groups not supported.'); + } + + var currentTransform = currentCtx.mozCurrentTransform; + if (group.matrix) { + currentCtx.transform.apply(currentCtx, group.matrix); + } + assert(group.bbox, 'Bounding box is required.'); + + // Based on the current transform figure out how big the bounding box + // will actually be. + var bounds = Util.getAxialAlignedBoundingBox( + group.bbox, + currentCtx.mozCurrentTransform); + // Clip the bounding box to the current canvas. + var canvasBounds = [0, + 0, + currentCtx.canvas.width, + currentCtx.canvas.height]; + bounds = Util.intersect(bounds, canvasBounds) || [0, 0, 0, 0]; + // Use ceil in case we're between sizes so we don't create canvas that is + // too small and make the canvas at least 1x1 pixels. + var offsetX = Math.floor(bounds[0]); + var offsetY = Math.floor(bounds[1]); + var drawnWidth = Math.max(Math.ceil(bounds[2]) - offsetX, 1); + var drawnHeight = Math.max(Math.ceil(bounds[3]) - offsetY, 1); + var scaleX = 1, scaleY = 1; + if (drawnWidth > MAX_GROUP_SIZE) { + scaleX = drawnWidth / MAX_GROUP_SIZE; + drawnWidth = MAX_GROUP_SIZE; + } + if (drawnHeight > MAX_GROUP_SIZE) { + scaleY = drawnHeight / MAX_GROUP_SIZE; + drawnHeight = MAX_GROUP_SIZE; + } + + var cacheId = 'groupAt' + this.groupLevel; + if (group.smask) { + // Using two cache entries is case if masks are used one after another. + cacheId += '_smask_' + ((this.smaskCounter++) % 2); + } + var scratchCanvas = CachedCanvases.getCanvas( + cacheId, drawnWidth, drawnHeight, true); + var groupCtx = scratchCanvas.context; + + // Since we created a new canvas that is just the size of the bounding box + // we have to translate the group ctx. + groupCtx.scale(1 / scaleX, 1 / scaleY); + groupCtx.translate(-offsetX, -offsetY); + groupCtx.transform.apply(groupCtx, currentTransform); + + if (group.smask) { + // Saving state and cached mask to be used in setGState. + this.smaskStack.push({ + canvas: scratchCanvas.canvas, + context: groupCtx, + offsetX: offsetX, + offsetY: offsetY, + scaleX: scaleX, + scaleY: scaleY, + subtype: group.smask.subtype, + backdrop: group.smask.backdrop + }); + } else { + // Setup the current ctx so when the group is popped we draw it at the + // right location. + currentCtx.setTransform(1, 0, 0, 1, 0, 0); + currentCtx.translate(offsetX, offsetY); + currentCtx.scale(scaleX, scaleY); + } + // The transparency group inherits all off the current graphics state + // except the blend mode, soft mask, and alpha constants. + copyCtxState(currentCtx, groupCtx); + this.ctx = groupCtx; + this.setGState([ + ['BM', 'Normal'], + ['ca', 1], + ['CA', 1] + ]); + this.groupStack.push(currentCtx); + this.groupLevel++; + }, + + endGroup: function CanvasGraphics_endGroup(group) { + this.groupLevel--; + var groupCtx = this.ctx; + this.ctx = this.groupStack.pop(); + // Turn off image smoothing to avoid sub pixel interpolation which can + // look kind of blurry for some pdfs. + if (this.ctx.imageSmoothingEnabled !== undefined) { + this.ctx.imageSmoothingEnabled = false; + } else { + this.ctx.mozImageSmoothingEnabled = false; + } + if (group.smask) { + this.tempSMask = this.smaskStack.pop(); + } else { + this.ctx.drawImage(groupCtx.canvas, 0, 0); + } + this.restore(); + }, + + beginAnnotations: function CanvasGraphics_beginAnnotations() { + this.save(); + this.current = new CanvasExtraState(); + }, + + endAnnotations: function CanvasGraphics_endAnnotations() { + this.restore(); + }, + + beginAnnotation: function CanvasGraphics_beginAnnotation(rect, transform, + matrix) { + this.save(); + + if (isArray(rect) && 4 === rect.length) { + var width = rect[2] - rect[0]; + var height = rect[3] - rect[1]; + this.ctx.rect(rect[0], rect[1], width, height); + this.clip(); + this.endPath(); + } + + this.transform.apply(this, transform); + this.transform.apply(this, matrix); + }, + + endAnnotation: function CanvasGraphics_endAnnotation() { + this.restore(); + }, + + paintJpegXObject: function CanvasGraphics_paintJpegXObject(objId, w, h) { + var domImage = this.objs.get(objId); + if (!domImage) { + warn('Dependent image isn\'t ready yet'); + return; + } + + this.save(); + + var ctx = this.ctx; + // scale the image to the unit square + ctx.scale(1 / w, -1 / h); + + ctx.drawImage(domImage, 0, 0, domImage.width, domImage.height, + 0, -h, w, h); + if (this.imageLayer) { + var currentTransform = ctx.mozCurrentTransformInverse; + var position = this.getCanvasPosition(0, 0); + this.imageLayer.appendImage({ + objId: objId, + left: position[0], + top: position[1], + width: w / currentTransform[0], + height: h / currentTransform[3] + }); + } + this.restore(); + }, + + paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) { + var ctx = this.ctx; + var width = img.width, height = img.height; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; + + var glyph = this.processingType3; + + if (COMPILE_TYPE3_GLYPHS && glyph && glyph.compiled === undefined) { + if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) { + glyph.compiled = + compileType3Glyph({data: img.data, width: width, height: height}); + } else { + glyph.compiled = null; + } + } + + if (glyph && glyph.compiled) { + glyph.compiled(ctx); + return; + } + + var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCtx = maskCanvas.context; + maskCtx.save(); + + putBinaryImageMask(maskCtx, img); + + maskCtx.globalCompositeOperation = 'source-in'; + + maskCtx.fillStyle = isPatternFill ? + fillColor.getPattern(maskCtx, this) : fillColor; + maskCtx.fillRect(0, 0, width, height); + + maskCtx.restore(); + + this.paintInlineImageXObject(maskCanvas.canvas); + }, + + paintImageMaskXObjectRepeat: + function CanvasGraphics_paintImageMaskXObjectRepeat(imgData, scaleX, + scaleY, positions) { + var width = imgData.width; + var height = imgData.height; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; + + var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCtx = maskCanvas.context; + maskCtx.save(); + + putBinaryImageMask(maskCtx, imgData); + + maskCtx.globalCompositeOperation = 'source-in'; + + maskCtx.fillStyle = isPatternFill ? + fillColor.getPattern(maskCtx, this) : fillColor; + maskCtx.fillRect(0, 0, width, height); + + maskCtx.restore(); + + var ctx = this.ctx; + for (var i = 0, ii = positions.length; i < ii; i += 2) { + ctx.save(); + ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]); + ctx.scale(1, -1); + ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, + 0, -1, 1, 1); + ctx.restore(); + } + }, + + paintImageMaskXObjectGroup: + function CanvasGraphics_paintImageMaskXObjectGroup(images) { + var ctx = this.ctx; + + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; + for (var i = 0, ii = images.length; i < ii; i++) { + var image = images[i]; + var width = image.width, height = image.height; + + var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCtx = maskCanvas.context; + maskCtx.save(); + + putBinaryImageMask(maskCtx, image); + + maskCtx.globalCompositeOperation = 'source-in'; + + maskCtx.fillStyle = isPatternFill ? + fillColor.getPattern(maskCtx, this) : fillColor; + maskCtx.fillRect(0, 0, width, height); + + maskCtx.restore(); + + ctx.save(); + ctx.transform.apply(ctx, image.transform); + ctx.scale(1, -1); + ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, + 0, -1, 1, 1); + ctx.restore(); + } + }, + + paintImageXObject: function CanvasGraphics_paintImageXObject(objId) { + var imgData = this.objs.get(objId); + if (!imgData) { + warn('Dependent image isn\'t ready yet'); + return; + } + + this.paintInlineImageXObject(imgData); + }, + + paintImageXObjectRepeat: + function CanvasGraphics_paintImageXObjectRepeat(objId, scaleX, scaleY, + positions) { + var imgData = this.objs.get(objId); + if (!imgData) { + warn('Dependent image isn\'t ready yet'); + return; + } + + var width = imgData.width; + var height = imgData.height; + var map = []; + for (var i = 0, ii = positions.length; i < ii; i += 2) { + map.push({transform: [scaleX, 0, 0, scaleY, positions[i], + positions[i + 1]], x: 0, y: 0, w: width, h: height}); + } + this.paintInlineImageXObjectGroup(imgData, map); + }, + + paintInlineImageXObject: + function CanvasGraphics_paintInlineImageXObject(imgData) { + var width = imgData.width; + var height = imgData.height; + var ctx = this.ctx; + + this.save(); + // scale the image to the unit square + ctx.scale(1 / width, -1 / height); + + var currentTransform = ctx.mozCurrentTransformInverse; + var a = currentTransform[0], b = currentTransform[1]; + var widthScale = Math.max(Math.sqrt(a * a + b * b), 1); + var c = currentTransform[2], d = currentTransform[3]; + var heightScale = Math.max(Math.sqrt(c * c + d * d), 1); + + var imgToPaint, tmpCanvas; + // instanceof HTMLElement does not work in jsdom node.js module + if (imgData instanceof HTMLElement || !imgData.data) { + imgToPaint = imgData; + } else { + tmpCanvas = CachedCanvases.getCanvas('inlineImage', width, height); + var tmpCtx = tmpCanvas.context; + putBinaryImageData(tmpCtx, imgData); + imgToPaint = tmpCanvas.canvas; + } + + var paintWidth = width, paintHeight = height; + var tmpCanvasId = 'prescale1'; + // Vertial or horizontal scaling shall not be more than 2 to not loose the + // pixels during drawImage operation, painting on the temporary canvas(es) + // that are twice smaller in size + while ((widthScale > 2 && paintWidth > 1) || + (heightScale > 2 && paintHeight > 1)) { + var newWidth = paintWidth, newHeight = paintHeight; + if (widthScale > 2 && paintWidth > 1) { + newWidth = Math.ceil(paintWidth / 2); + widthScale /= paintWidth / newWidth; + } + if (heightScale > 2 && paintHeight > 1) { + newHeight = Math.ceil(paintHeight / 2); + heightScale /= paintHeight / newHeight; + } + tmpCanvas = CachedCanvases.getCanvas(tmpCanvasId, newWidth, newHeight); + tmpCtx = tmpCanvas.context; + tmpCtx.clearRect(0, 0, newWidth, newHeight); + tmpCtx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight, + 0, 0, newWidth, newHeight); + imgToPaint = tmpCanvas.canvas; + paintWidth = newWidth; + paintHeight = newHeight; + tmpCanvasId = tmpCanvasId === 'prescale1' ? 'prescale2' : 'prescale1'; + } + ctx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight, + 0, -height, width, height); + + if (this.imageLayer) { + var position = this.getCanvasPosition(0, -height); + this.imageLayer.appendImage({ + imgData: imgData, + left: position[0], + top: position[1], + width: width / currentTransform[0], + height: height / currentTransform[3] + }); + } + this.restore(); + }, + + paintInlineImageXObjectGroup: + function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) { + var ctx = this.ctx; + var w = imgData.width; + var h = imgData.height; + + var tmpCanvas = CachedCanvases.getCanvas('inlineImage', w, h); + var tmpCtx = tmpCanvas.context; + putBinaryImageData(tmpCtx, imgData); + + for (var i = 0, ii = map.length; i < ii; i++) { + var entry = map[i]; + ctx.save(); + ctx.transform.apply(ctx, entry.transform); + ctx.scale(1, -1); + ctx.drawImage(tmpCanvas.canvas, entry.x, entry.y, entry.w, entry.h, + 0, -1, 1, 1); + if (this.imageLayer) { + var position = this.getCanvasPosition(entry.x, entry.y); + this.imageLayer.appendImage({ + imgData: imgData, + left: position[0], + top: position[1], + width: w, + height: h + }); + } + ctx.restore(); + } + }, + + paintSolidColorImageMask: + function CanvasGraphics_paintSolidColorImageMask() { + this.ctx.fillRect(0, 0, 1, 1); + }, + + // Marked content + + markPoint: function CanvasGraphics_markPoint(tag) { + // TODO Marked content. + }, + markPointProps: function CanvasGraphics_markPointProps(tag, properties) { + // TODO Marked content. + }, + beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) { + // TODO Marked content. + }, + beginMarkedContentProps: function CanvasGraphics_beginMarkedContentProps( + tag, properties) { + // TODO Marked content. + }, + endMarkedContent: function CanvasGraphics_endMarkedContent() { + // TODO Marked content. + }, + + // Compatibility + + beginCompat: function CanvasGraphics_beginCompat() { + // TODO ignore undefined operators (should we do that anyway?) + }, + endCompat: function CanvasGraphics_endCompat() { + // TODO stop ignoring undefined operators + }, + + // Helper functions + + consumePath: function CanvasGraphics_consumePath() { + var ctx = this.ctx; + if (this.pendingClip) { + if (this.pendingClip === EO_CLIP) { + if (ctx.mozFillRule !== undefined) { + ctx.mozFillRule = 'evenodd'; + ctx.clip(); + ctx.mozFillRule = 'nonzero'; + } else { + try { + ctx.clip('evenodd'); + } catch (ex) { + // shouldn't really happen, but browsers might think differently + ctx.clip(); + } + } + } else { + ctx.clip(); + } + this.pendingClip = null; + } + ctx.beginPath(); + }, + getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) { + if (this.cachedGetSinglePixelWidth === null) { + var inverse = this.ctx.mozCurrentTransformInverse; + // max of the current horizontal and vertical scale + this.cachedGetSinglePixelWidth = Math.sqrt(Math.max( + (inverse[0] * inverse[0] + inverse[1] * inverse[1]), + (inverse[2] * inverse[2] + inverse[3] * inverse[3]))); + } + return this.cachedGetSinglePixelWidth; + }, + getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) { + var transform = this.ctx.mozCurrentTransform; + return [ + transform[0] * x + transform[2] * y + transform[4], + transform[1] * x + transform[3] * y + transform[5] + ]; + } + }; + + for (var op in OPS) { + CanvasGraphics.prototype[OPS[op]] = CanvasGraphics.prototype[op]; + } + + return CanvasGraphics; +})(); + + +var WebGLUtils = (function WebGLUtilsClosure() { + function loadShader(gl, code, shaderType) { + var shader = gl.createShader(shaderType); + gl.shaderSource(shader, code); + gl.compileShader(shader); + var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!compiled) { + var errorMsg = gl.getShaderInfoLog(shader); + throw new Error('Error during shader compilation: ' + errorMsg); + } + return shader; + } + function createVertexShader(gl, code) { + return loadShader(gl, code, gl.VERTEX_SHADER); + } + function createFragmentShader(gl, code) { + return loadShader(gl, code, gl.FRAGMENT_SHADER); + } + function createProgram(gl, shaders) { + var program = gl.createProgram(); + for (var i = 0, ii = shaders.length; i < ii; ++i) { + gl.attachShader(program, shaders[i]); + } + gl.linkProgram(program); + var linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + var errorMsg = gl.getProgramInfoLog(program); + throw new Error('Error during program linking: ' + errorMsg); + } + return program; + } + function createTexture(gl, image, textureId) { + gl.activeTexture(textureId); + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + // Set the parameters so we can render any size image. + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + // Upload the image into the texture. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + return texture; + } + + var currentGL, currentCanvas; + function generateGL() { + if (currentGL) { + return; + } + currentCanvas = document.createElement('canvas'); + currentGL = currentCanvas.getContext('webgl', + { premultipliedalpha: false }); + } + + var smaskVertexShaderCode = '\ + attribute vec2 a_position; \ + attribute vec2 a_texCoord; \ + \ + uniform vec2 u_resolution; \ + \ + varying vec2 v_texCoord; \ + \ + void main() { \ + vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0; \ + gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); \ + \ + v_texCoord = a_texCoord; \ + } '; + + var smaskFragmentShaderCode = '\ + precision mediump float; \ + \ + uniform vec4 u_backdrop; \ + uniform int u_subtype; \ + uniform sampler2D u_image; \ + uniform sampler2D u_mask; \ + \ + varying vec2 v_texCoord; \ + \ + void main() { \ + vec4 imageColor = texture2D(u_image, v_texCoord); \ + vec4 maskColor = texture2D(u_mask, v_texCoord); \ + if (u_backdrop.a > 0.0) { \ + maskColor.rgb = maskColor.rgb * maskColor.a + \ + u_backdrop.rgb * (1.0 - maskColor.a); \ + } \ + float lum; \ + if (u_subtype == 0) { \ + lum = maskColor.a; \ + } else { \ + lum = maskColor.r * 0.3 + maskColor.g * 0.59 + \ + maskColor.b * 0.11; \ + } \ + imageColor.a *= lum; \ + imageColor.rgb *= imageColor.a; \ + gl_FragColor = imageColor; \ + } '; + + var smaskCache = null; + + function initSmaskGL() { + var canvas, gl; + + generateGL(); + canvas = currentCanvas; + currentCanvas = null; + gl = currentGL; + currentGL = null; + + // setup a GLSL program + var vertexShader = createVertexShader(gl, smaskVertexShaderCode); + var fragmentShader = createFragmentShader(gl, smaskFragmentShaderCode); + var program = createProgram(gl, [vertexShader, fragmentShader]); + gl.useProgram(program); + + var cache = {}; + cache.gl = gl; + cache.canvas = canvas; + cache.resolutionLocation = gl.getUniformLocation(program, 'u_resolution'); + cache.positionLocation = gl.getAttribLocation(program, 'a_position'); + cache.backdropLocation = gl.getUniformLocation(program, 'u_backdrop'); + cache.subtypeLocation = gl.getUniformLocation(program, 'u_subtype'); + + var texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); + var texLayerLocation = gl.getUniformLocation(program, 'u_image'); + var texMaskLocation = gl.getUniformLocation(program, 'u_mask'); + + // provide texture coordinates for the rectangle. + var texCoordBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + 0.0, 0.0, + 1.0, 0.0, + 0.0, 1.0, + 0.0, 1.0, + 1.0, 0.0, + 1.0, 1.0]), gl.STATIC_DRAW); + gl.enableVertexAttribArray(texCoordLocation); + gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); + + gl.uniform1i(texLayerLocation, 0); + gl.uniform1i(texMaskLocation, 1); + + smaskCache = cache; + } + + function composeSMask(layer, mask, properties) { + var width = layer.width, height = layer.height; + + if (!smaskCache) { + initSmaskGL(); + } + var cache = smaskCache,canvas = cache.canvas, gl = cache.gl; + canvas.width = width; + canvas.height = height; + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.uniform2f(cache.resolutionLocation, width, height); + + if (properties.backdrop) { + gl.uniform4f(cache.resolutionLocation, properties.backdrop[0], + properties.backdrop[1], properties.backdrop[2], 1); + } else { + gl.uniform4f(cache.resolutionLocation, 0, 0, 0, 0); + } + gl.uniform1i(cache.subtypeLocation, + properties.subtype === 'Luminosity' ? 1 : 0); + + // Create a textures + var texture = createTexture(gl, layer, gl.TEXTURE0); + var maskTexture = createTexture(gl, mask, gl.TEXTURE1); + + + // Create a buffer and put a single clipspace rectangle in + // it (2 triangles) + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + 0, 0, + width, 0, + 0, height, + 0, height, + width, 0, + width, height]), gl.STATIC_DRAW); + gl.enableVertexAttribArray(cache.positionLocation); + gl.vertexAttribPointer(cache.positionLocation, 2, gl.FLOAT, false, 0, 0); + + // draw + gl.clearColor(0, 0, 0, 0); + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + + gl.flush(); + + gl.deleteTexture(texture); + gl.deleteTexture(maskTexture); + gl.deleteBuffer(buffer); + + return canvas; + } + + var figuresVertexShaderCode = '\ + attribute vec2 a_position; \ + attribute vec3 a_color; \ + \ + uniform vec2 u_resolution; \ + uniform vec2 u_scale; \ + uniform vec2 u_offset; \ + \ + varying vec4 v_color; \ + \ + void main() { \ + vec2 position = (a_position + u_offset) * u_scale; \ + vec2 clipSpace = (position / u_resolution) * 2.0 - 1.0; \ + gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); \ + \ + v_color = vec4(a_color / 255.0, 1.0); \ + } '; + + var figuresFragmentShaderCode = '\ + precision mediump float; \ + \ + varying vec4 v_color; \ + \ + void main() { \ + gl_FragColor = v_color; \ + } '; + + var figuresCache = null; + + function initFiguresGL() { + var canvas, gl; + + generateGL(); + canvas = currentCanvas; + currentCanvas = null; + gl = currentGL; + currentGL = null; + + // setup a GLSL program + var vertexShader = createVertexShader(gl, figuresVertexShaderCode); + var fragmentShader = createFragmentShader(gl, figuresFragmentShaderCode); + var program = createProgram(gl, [vertexShader, fragmentShader]); + gl.useProgram(program); + + var cache = {}; + cache.gl = gl; + cache.canvas = canvas; + cache.resolutionLocation = gl.getUniformLocation(program, 'u_resolution'); + cache.scaleLocation = gl.getUniformLocation(program, 'u_scale'); + cache.offsetLocation = gl.getUniformLocation(program, 'u_offset'); + cache.positionLocation = gl.getAttribLocation(program, 'a_position'); + cache.colorLocation = gl.getAttribLocation(program, 'a_color'); + + figuresCache = cache; + } + + function drawFigures(width, height, backgroundColor, figures, context) { + if (!figuresCache) { + initFiguresGL(); + } + var cache = figuresCache, canvas = cache.canvas, gl = cache.gl; + + canvas.width = width; + canvas.height = height; + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.uniform2f(cache.resolutionLocation, width, height); + + // count triangle points + var count = 0; + var i, ii, rows; + for (i = 0, ii = figures.length; i < ii; i++) { + switch (figures[i].type) { + case 'lattice': + rows = (figures[i].coords.length / figures[i].verticesPerRow) | 0; + count += (rows - 1) * (figures[i].verticesPerRow - 1) * 6; + break; + case 'triangles': + count += figures[i].coords.length; + break; + } + } + // transfer data + var coords = new Float32Array(count * 2); + var colors = new Uint8Array(count * 3); + var coordsMap = context.coords, colorsMap = context.colors; + var pIndex = 0, cIndex = 0; + for (i = 0, ii = figures.length; i < ii; i++) { + var figure = figures[i], ps = figure.coords, cs = figure.colors; + switch (figure.type) { + case 'lattice': + var cols = figure.verticesPerRow; + rows = (ps.length / cols) | 0; + for (var row = 1; row < rows; row++) { + var offset = row * cols + 1; + for (var col = 1; col < cols; col++, offset++) { + coords[pIndex] = coordsMap[ps[offset - cols - 1]]; + coords[pIndex + 1] = coordsMap[ps[offset - cols - 1] + 1]; + coords[pIndex + 2] = coordsMap[ps[offset - cols]]; + coords[pIndex + 3] = coordsMap[ps[offset - cols] + 1]; + coords[pIndex + 4] = coordsMap[ps[offset - 1]]; + coords[pIndex + 5] = coordsMap[ps[offset - 1] + 1]; + colors[cIndex] = colorsMap[cs[offset - cols - 1]]; + colors[cIndex + 1] = colorsMap[cs[offset - cols - 1] + 1]; + colors[cIndex + 2] = colorsMap[cs[offset - cols - 1] + 2]; + colors[cIndex + 3] = colorsMap[cs[offset - cols]]; + colors[cIndex + 4] = colorsMap[cs[offset - cols] + 1]; + colors[cIndex + 5] = colorsMap[cs[offset - cols] + 2]; + colors[cIndex + 6] = colorsMap[cs[offset - 1]]; + colors[cIndex + 7] = colorsMap[cs[offset - 1] + 1]; + colors[cIndex + 8] = colorsMap[cs[offset - 1] + 2]; + + coords[pIndex + 6] = coords[pIndex + 2]; + coords[pIndex + 7] = coords[pIndex + 3]; + coords[pIndex + 8] = coords[pIndex + 4]; + coords[pIndex + 9] = coords[pIndex + 5]; + coords[pIndex + 10] = coordsMap[ps[offset]]; + coords[pIndex + 11] = coordsMap[ps[offset] + 1]; + colors[cIndex + 9] = colors[cIndex + 3]; + colors[cIndex + 10] = colors[cIndex + 4]; + colors[cIndex + 11] = colors[cIndex + 5]; + colors[cIndex + 12] = colors[cIndex + 6]; + colors[cIndex + 13] = colors[cIndex + 7]; + colors[cIndex + 14] = colors[cIndex + 8]; + colors[cIndex + 15] = colorsMap[cs[offset]]; + colors[cIndex + 16] = colorsMap[cs[offset] + 1]; + colors[cIndex + 17] = colorsMap[cs[offset] + 2]; + pIndex += 12; + cIndex += 18; + } + } + break; + case 'triangles': + for (var j = 0, jj = ps.length; j < jj; j++) { + coords[pIndex] = coordsMap[ps[j]]; + coords[pIndex + 1] = coordsMap[ps[j] + 1]; + colors[cIndex] = colorsMap[cs[i]]; + colors[cIndex + 1] = colorsMap[cs[j] + 1]; + colors[cIndex + 2] = colorsMap[cs[j] + 2]; + pIndex += 2; + cIndex += 3; + } + break; + } + } + + // draw + if (backgroundColor) { + gl.clearColor(backgroundColor[0] / 255, backgroundColor[1] / 255, + backgroundColor[2] / 255, 1.0); + } else { + gl.clearColor(0, 0, 0, 0); + } + gl.clear(gl.COLOR_BUFFER_BIT); + + var coordsBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, coordsBuffer); + gl.bufferData(gl.ARRAY_BUFFER, coords, gl.STATIC_DRAW); + gl.enableVertexAttribArray(cache.positionLocation); + gl.vertexAttribPointer(cache.positionLocation, 2, gl.FLOAT, false, 0, 0); + + var colorsBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer); + gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW); + gl.enableVertexAttribArray(cache.colorLocation); + gl.vertexAttribPointer(cache.colorLocation, 3, gl.UNSIGNED_BYTE, false, + 0, 0); + + gl.uniform2f(cache.scaleLocation, context.scaleX, context.scaleY); + gl.uniform2f(cache.offsetLocation, context.offsetX, context.offsetY); + + gl.drawArrays(gl.TRIANGLES, 0, count); + + gl.flush(); + + gl.deleteBuffer(coordsBuffer); + gl.deleteBuffer(colorsBuffer); + + return canvas; + } + + function cleanup() { + if (smaskCache && smaskCache.canvas) { + smaskCache.canvas.width = 0; + smaskCache.canvas.height = 0; + } + if (figuresCache && figuresCache.canvas) { + figuresCache.canvas.width = 0; + figuresCache.canvas.height = 0; + } + smaskCache = null; + figuresCache = null; + } + + return { + get isEnabled() { + if (PDFJS.disableWebGL) { + return false; + } + var enabled = false; + try { + generateGL(); + enabled = !!currentGL; + } catch (e) { } + return shadow(this, 'isEnabled', enabled); + }, + composeSMask: composeSMask, + drawFigures: drawFigures, + clear: cleanup + }; +})(); + + +var ShadingIRs = {}; + +ShadingIRs.RadialAxial = { + fromIR: function RadialAxial_fromIR(raw) { + var type = raw[1]; + var colorStops = raw[2]; + var p0 = raw[3]; + var p1 = raw[4]; + var r0 = raw[5]; + var r1 = raw[6]; + return { + type: 'Pattern', + getPattern: function RadialAxial_getPattern(ctx) { + var grad; + if (type === 'axial') { + grad = ctx.createLinearGradient(p0[0], p0[1], p1[0], p1[1]); + } else if (type === 'radial') { + grad = ctx.createRadialGradient(p0[0], p0[1], r0, p1[0], p1[1], r1); + } + + for (var i = 0, ii = colorStops.length; i < ii; ++i) { + var c = colorStops[i]; + grad.addColorStop(c[0], c[1]); + } + return grad; + } + }; + } +}; + +var createMeshCanvas = (function createMeshCanvasClosure() { + function drawTriangle(data, context, p1, p2, p3, c1, c2, c3) { + // Very basic Gouraud-shaded triangle rasterization algorithm. + var coords = context.coords, colors = context.colors; + var bytes = data.data, rowSize = data.width * 4; + var tmp; + if (coords[p1 + 1] > coords[p2 + 1]) { + tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp; + } + if (coords[p2 + 1] > coords[p3 + 1]) { + tmp = p2; p2 = p3; p3 = tmp; tmp = c2; c2 = c3; c3 = tmp; + } + if (coords[p1 + 1] > coords[p2 + 1]) { + tmp = p1; p1 = p2; p2 = tmp; tmp = c1; c1 = c2; c2 = tmp; + } + var x1 = (coords[p1] + context.offsetX) * context.scaleX; + var y1 = (coords[p1 + 1] + context.offsetY) * context.scaleY; + var x2 = (coords[p2] + context.offsetX) * context.scaleX; + var y2 = (coords[p2 + 1] + context.offsetY) * context.scaleY; + var x3 = (coords[p3] + context.offsetX) * context.scaleX; + var y3 = (coords[p3 + 1] + context.offsetY) * context.scaleY; + if (y1 >= y3) { + return; + } + var c1r = colors[c1], c1g = colors[c1 + 1], c1b = colors[c1 + 2]; + var c2r = colors[c2], c2g = colors[c2 + 1], c2b = colors[c2 + 2]; + var c3r = colors[c3], c3g = colors[c3 + 1], c3b = colors[c3 + 2]; + + var minY = Math.round(y1), maxY = Math.round(y3); + var xa, car, cag, cab; + var xb, cbr, cbg, cbb; + var k; + for (var y = minY; y <= maxY; y++) { + if (y < y2) { + k = y < y1 ? 0 : y1 === y2 ? 1 : (y1 - y) / (y1 - y2); + xa = x1 - (x1 - x2) * k; + car = c1r - (c1r - c2r) * k; + cag = c1g - (c1g - c2g) * k; + cab = c1b - (c1b - c2b) * k; + } else { + k = y > y3 ? 1 : y2 === y3 ? 0 : (y2 - y) / (y2 - y3); + xa = x2 - (x2 - x3) * k; + car = c2r - (c2r - c3r) * k; + cag = c2g - (c2g - c3g) * k; + cab = c2b - (c2b - c3b) * k; + } + k = y < y1 ? 0 : y > y3 ? 1 : (y1 - y) / (y1 - y3); + xb = x1 - (x1 - x3) * k; + cbr = c1r - (c1r - c3r) * k; + cbg = c1g - (c1g - c3g) * k; + cbb = c1b - (c1b - c3b) * k; + var x1_ = Math.round(Math.min(xa, xb)); + var x2_ = Math.round(Math.max(xa, xb)); + var j = rowSize * y + x1_ * 4; + for (var x = x1_; x <= x2_; x++) { + k = (xa - x) / (xa - xb); + k = k < 0 ? 0 : k > 1 ? 1 : k; + bytes[j++] = (car - (car - cbr) * k) | 0; + bytes[j++] = (cag - (cag - cbg) * k) | 0; + bytes[j++] = (cab - (cab - cbb) * k) | 0; + bytes[j++] = 255; + } + } + } + + function drawFigure(data, figure, context) { + var ps = figure.coords; + var cs = figure.colors; + var i, ii; + switch (figure.type) { + case 'lattice': + var verticesPerRow = figure.verticesPerRow; + var rows = Math.floor(ps.length / verticesPerRow) - 1; + var cols = verticesPerRow - 1; + for (i = 0; i < rows; i++) { + var q = i * verticesPerRow; + for (var j = 0; j < cols; j++, q++) { + drawTriangle(data, context, + ps[q], ps[q + 1], ps[q + verticesPerRow], + cs[q], cs[q + 1], cs[q + verticesPerRow]); + drawTriangle(data, context, + ps[q + verticesPerRow + 1], ps[q + 1], ps[q + verticesPerRow], + cs[q + verticesPerRow + 1], cs[q + 1], cs[q + verticesPerRow]); + } + } + break; + case 'triangles': + for (i = 0, ii = ps.length; i < ii; i += 3) { + drawTriangle(data, context, + ps[i], ps[i + 1], ps[i + 2], + cs[i], cs[i + 1], cs[i + 2]); + } + break; + default: + error('illigal figure'); + break; + } + } + + function createMeshCanvas(bounds, combinesScale, coords, colors, figures, + backgroundColor) { + // we will increase scale on some weird factor to let antialiasing take + // care of "rough" edges + var EXPECTED_SCALE = 1.1; + // MAX_PATTERN_SIZE is used to avoid OOM situation. + var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough + + var offsetX = Math.floor(bounds[0]); + var offsetY = Math.floor(bounds[1]); + var boundsWidth = Math.ceil(bounds[2]) - offsetX; + var boundsHeight = Math.ceil(bounds[3]) - offsetY; + + var width = Math.min(Math.ceil(Math.abs(boundsWidth * combinesScale[0] * + EXPECTED_SCALE)), MAX_PATTERN_SIZE); + var height = Math.min(Math.ceil(Math.abs(boundsHeight * combinesScale[1] * + EXPECTED_SCALE)), MAX_PATTERN_SIZE); + var scaleX = boundsWidth / width; + var scaleY = boundsHeight / height; + + var context = { + coords: coords, + colors: colors, + offsetX: -offsetX, + offsetY: -offsetY, + scaleX: 1 / scaleX, + scaleY: 1 / scaleY + }; + + var canvas, tmpCanvas, i, ii; + if (WebGLUtils.isEnabled) { + canvas = WebGLUtils.drawFigures(width, height, backgroundColor, + figures, context); + + // https://bugzilla.mozilla.org/show_bug.cgi?id=972126 + tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false); + tmpCanvas.context.drawImage(canvas, 0, 0); + canvas = tmpCanvas.canvas; + } else { + tmpCanvas = CachedCanvases.getCanvas('mesh', width, height, false); + var tmpCtx = tmpCanvas.context; + + var data = tmpCtx.createImageData(width, height); + if (backgroundColor) { + var bytes = data.data; + for (i = 0, ii = bytes.length; i < ii; i += 4) { + bytes[i] = backgroundColor[0]; + bytes[i + 1] = backgroundColor[1]; + bytes[i + 2] = backgroundColor[2]; + bytes[i + 3] = 255; + } + } + for (i = 0; i < figures.length; i++) { + drawFigure(data, figures[i], context); + } + tmpCtx.putImageData(data, 0, 0); + canvas = tmpCanvas.canvas; + } + + return {canvas: canvas, offsetX: offsetX, offsetY: offsetY, + scaleX: scaleX, scaleY: scaleY}; + } + return createMeshCanvas; +})(); + +ShadingIRs.Mesh = { + fromIR: function Mesh_fromIR(raw) { + //var type = raw[1]; + var coords = raw[2]; + var colors = raw[3]; + var figures = raw[4]; + var bounds = raw[5]; + var matrix = raw[6]; + //var bbox = raw[7]; + var background = raw[8]; + return { + type: 'Pattern', + getPattern: function Mesh_getPattern(ctx, owner, shadingFill) { + var scale; + if (shadingFill) { + scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform); + } else { + // Obtain scale from matrix and current transformation matrix. + scale = Util.singularValueDecompose2dScale(owner.baseTransform); + if (matrix) { + var matrixScale = Util.singularValueDecompose2dScale(matrix); + scale = [scale[0] * matrixScale[0], + scale[1] * matrixScale[1]]; + } + } + + + // Rasterizing on the main thread since sending/queue large canvases + // might cause OOM. + var temporaryPatternCanvas = createMeshCanvas(bounds, scale, coords, + colors, figures, shadingFill ? null : background); + + if (!shadingFill) { + ctx.setTransform.apply(ctx, owner.baseTransform); + if (matrix) { + ctx.transform.apply(ctx, matrix); + } + } + + ctx.translate(temporaryPatternCanvas.offsetX, + temporaryPatternCanvas.offsetY); + ctx.scale(temporaryPatternCanvas.scaleX, + temporaryPatternCanvas.scaleY); + + return ctx.createPattern(temporaryPatternCanvas.canvas, 'no-repeat'); + } + }; + } +}; + +ShadingIRs.Dummy = { + fromIR: function Dummy_fromIR() { + return { + type: 'Pattern', + getPattern: function Dummy_fromIR_getPattern() { + return 'hotpink'; + } + }; + } +}; + +function getShadingPatternFromIR(raw) { + var shadingIR = ShadingIRs[raw[0]]; + if (!shadingIR) { + error('Unknown IR type: ' + raw[0]); + } + return shadingIR.fromIR(raw); +} + +var TilingPattern = (function TilingPatternClosure() { + var PaintType = { + COLORED: 1, + UNCOLORED: 2 + }; + + var MAX_PATTERN_SIZE = 3000; // 10in @ 300dpi shall be enough + + function TilingPattern(IR, color, ctx, objs, commonObjs, baseTransform) { + this.operatorList = IR[2]; + this.matrix = IR[3] || [1, 0, 0, 1, 0, 0]; + this.bbox = IR[4]; + this.xstep = IR[5]; + this.ystep = IR[6]; + this.paintType = IR[7]; + this.tilingType = IR[8]; + this.color = color; + this.objs = objs; + this.commonObjs = commonObjs; + this.baseTransform = baseTransform; + this.type = 'Pattern'; + this.ctx = ctx; + } + + TilingPattern.prototype = { + createPatternCanvas: function TilinPattern_createPatternCanvas(owner) { + var operatorList = this.operatorList; + var bbox = this.bbox; + var xstep = this.xstep; + var ystep = this.ystep; + var paintType = this.paintType; + var tilingType = this.tilingType; + var color = this.color; + var objs = this.objs; + var commonObjs = this.commonObjs; + + info('TilingType: ' + tilingType); + + var x0 = bbox[0], y0 = bbox[1], x1 = bbox[2], y1 = bbox[3]; + + var topLeft = [x0, y0]; + // we want the canvas to be as large as the step size + var botRight = [x0 + xstep, y0 + ystep]; + + var width = botRight[0] - topLeft[0]; + var height = botRight[1] - topLeft[1]; + + // Obtain scale from matrix and current transformation matrix. + var matrixScale = Util.singularValueDecompose2dScale(this.matrix); + var curMatrixScale = Util.singularValueDecompose2dScale( + this.baseTransform); + var combinedScale = [matrixScale[0] * curMatrixScale[0], + matrixScale[1] * curMatrixScale[1]]; + + // MAX_PATTERN_SIZE is used to avoid OOM situation. + // Use width and height values that are as close as possible to the end + // result when the pattern is used. Too low value makes the pattern look + // blurry. Too large value makes it look too crispy. + width = Math.min(Math.ceil(Math.abs(width * combinedScale[0])), + MAX_PATTERN_SIZE); + + height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])), + MAX_PATTERN_SIZE); + + var tmpCanvas = CachedCanvases.getCanvas('pattern', width, height, true); + var tmpCtx = tmpCanvas.context; + var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs); + graphics.groupLevel = owner.groupLevel; + + this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color); + + this.setScale(width, height, xstep, ystep); + this.transformToScale(graphics); + + // transform coordinates to pattern space + var tmpTranslate = [1, 0, 0, 1, -topLeft[0], -topLeft[1]]; + graphics.transform.apply(graphics, tmpTranslate); + + this.clipBbox(graphics, bbox, x0, y0, x1, y1); + + graphics.executeOperatorList(operatorList); + return tmpCanvas.canvas; + }, + + setScale: function TilingPattern_setScale(width, height, xstep, ystep) { + this.scale = [width / xstep, height / ystep]; + }, + + transformToScale: function TilingPattern_transformToScale(graphics) { + var scale = this.scale; + var tmpScale = [scale[0], 0, 0, scale[1], 0, 0]; + graphics.transform.apply(graphics, tmpScale); + }, + + scaleToContext: function TilingPattern_scaleToContext() { + var scale = this.scale; + this.ctx.scale(1 / scale[0], 1 / scale[1]); + }, + + clipBbox: function clipBbox(graphics, bbox, x0, y0, x1, y1) { + if (bbox && isArray(bbox) && bbox.length === 4) { + var bboxWidth = x1 - x0; + var bboxHeight = y1 - y0; + graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); + graphics.clip(); + graphics.endPath(); + } + }, + + setFillAndStrokeStyleToContext: + function setFillAndStrokeStyleToContext(context, paintType, color) { + switch (paintType) { + case PaintType.COLORED: + var ctx = this.ctx; + context.fillStyle = ctx.fillStyle; + context.strokeStyle = ctx.strokeStyle; + break; + case PaintType.UNCOLORED: + var cssColor = Util.makeCssRgb(color[0], color[1], color[2]); + context.fillStyle = cssColor; + context.strokeStyle = cssColor; + break; + default: + error('Unsupported paint type: ' + paintType); + } + }, + + getPattern: function TilingPattern_getPattern(ctx, owner) { + var temporaryPatternCanvas = this.createPatternCanvas(owner); + + ctx = this.ctx; + ctx.setTransform.apply(ctx, this.baseTransform); + ctx.transform.apply(ctx, this.matrix); + this.scaleToContext(); + + return ctx.createPattern(temporaryPatternCanvas, 'repeat'); + } + }; + + return TilingPattern; +})(); + + +PDFJS.disableFontFace = false; + +var FontLoader = { + insertRule: function fontLoaderInsertRule(rule) { + var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG'); + if (!styleElement) { + styleElement = document.createElement('style'); + styleElement.id = 'PDFJS_FONT_STYLE_TAG'; + document.documentElement.getElementsByTagName('head')[0].appendChild( + styleElement); + } + + var styleSheet = styleElement.sheet; + styleSheet.insertRule(rule, styleSheet.cssRules.length); + }, + + clear: function fontLoaderClear() { + var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG'); + if (styleElement) { + styleElement.parentNode.removeChild(styleElement); + } + this.nativeFontFaces.forEach(function(nativeFontFace) { + document.fonts.delete(nativeFontFace); + }); + this.nativeFontFaces.length = 0; + }, + get loadTestFont() { + // This is a CFF font with 1 glyph for '.' that fills its entire width and + // height. + return shadow(this, 'loadTestFont', atob( + 'T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQ' + + 'AABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwA' + + 'AAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbm' + + 'FtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAA' + + 'AADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6A' + + 'ABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAA' + + 'MQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAA' + + 'AAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAA' + + 'AAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQ' + + 'AAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMA' + + 'AQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAA' + + 'EAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAA' + + 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAA' + + 'AAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgc' + + 'A/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF' + + 'hYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQA' + + 'AAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAg' + + 'ABAAAAAAAAAAAD6AAAAAAAAA==' + )); + }, + + loadTestFontId: 0, + + loadingContext: { + requests: [], + nextRequestId: 0 + }, + + isSyncFontLoadingSupported: (function detectSyncFontLoadingSupport() { + if (isWorker) { + return false; + } + + // User agent string sniffing is bad, but there is no reliable way to tell + // if font is fully loaded and ready to be used with canvas. + var userAgent = window.navigator.userAgent; + var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(userAgent); + if (m && m[1] >= 14) { + return true; + } + // TODO other browsers + if (userAgent === 'node') { + return true; + } + return false; + })(), + + nativeFontFaces: [], + + isFontLoadingAPISupported: (!isWorker && typeof document !== 'undefined' && + !!document.fonts), + + addNativeFontFace: function fontLoader_addNativeFontFace(nativeFontFace) { + this.nativeFontFaces.push(nativeFontFace); + document.fonts.add(nativeFontFace); + }, + + bind: function fontLoaderBind(fonts, callback) { + assert(!isWorker, 'bind() shall be called from main thread'); + + var rules = []; + var fontsToLoad = []; + var fontLoadPromises = []; + for (var i = 0, ii = fonts.length; i < ii; i++) { + var font = fonts[i]; + + // Add the font to the DOM only once or skip if the font + // is already loaded. + if (font.attached || font.loading === false) { + continue; + } + font.attached = true; + + if (this.isFontLoadingAPISupported) { + var nativeFontFace = font.createNativeFontFace(); + if (nativeFontFace) { + fontLoadPromises.push(nativeFontFace.loaded); + } + } else { + var rule = font.bindDOM(); + if (rule) { + rules.push(rule); + fontsToLoad.push(font); + } + } + } + + var request = FontLoader.queueLoadingCallback(callback); + if (this.isFontLoadingAPISupported) { + Promise.all(fontsToLoad).then(function() { + request.complete(); + }); + } else if (rules.length > 0 && !this.isSyncFontLoadingSupported) { + FontLoader.prepareFontLoadEvent(rules, fontsToLoad, request); + } else { + request.complete(); + } + }, + + queueLoadingCallback: function FontLoader_queueLoadingCallback(callback) { + function LoadLoader_completeRequest() { + assert(!request.end, 'completeRequest() cannot be called twice'); + request.end = Date.now(); + + // sending all completed requests in order how they were queued + while (context.requests.length > 0 && context.requests[0].end) { + var otherRequest = context.requests.shift(); + setTimeout(otherRequest.callback, 0); + } + } + + var context = FontLoader.loadingContext; + var requestId = 'pdfjs-font-loading-' + (context.nextRequestId++); + var request = { + id: requestId, + complete: LoadLoader_completeRequest, + callback: callback, + started: Date.now() + }; + context.requests.push(request); + return request; + }, + + prepareFontLoadEvent: function fontLoaderPrepareFontLoadEvent(rules, + fonts, + request) { + /** Hack begin */ + // There's currently no event when a font has finished downloading so the + // following code is a dirty hack to 'guess' when a font is + // ready. It's assumed fonts are loaded in order, so add a known test + // font after the desired fonts and then test for the loading of that + // test font. + + function int32(data, offset) { + return (data.charCodeAt(offset) << 24) | + (data.charCodeAt(offset + 1) << 16) | + (data.charCodeAt(offset + 2) << 8) | + (data.charCodeAt(offset + 3) & 0xff); + } + + function spliceString(s, offset, remove, insert) { + var chunk1 = s.substr(0, offset); + var chunk2 = s.substr(offset + remove); + return chunk1 + insert + chunk2; + } + + var i, ii; + + var canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + var ctx = canvas.getContext('2d'); + + var called = 0; + function isFontReady(name, callback) { + called++; + // With setTimeout clamping this gives the font ~100ms to load. + if(called > 30) { + warn('Load test font never loaded.'); + callback(); + return; + } + ctx.font = '30px ' + name; + ctx.fillText('.', 0, 20); + var imageData = ctx.getImageData(0, 0, 1, 1); + if (imageData.data[3] > 0) { + callback(); + return; + } + setTimeout(isFontReady.bind(null, name, callback)); + } + + var loadTestFontId = 'lt' + Date.now() + this.loadTestFontId++; + // Chromium seems to cache fonts based on a hash of the actual font data, + // so the font must be modified for each load test else it will appear to + // be loaded already. + // TODO: This could maybe be made faster by avoiding the btoa of the full + // font by splitting it in chunks before hand and padding the font id. + var data = this.loadTestFont; + var COMMENT_OFFSET = 976; // has to be on 4 byte boundary (for checksum) + data = spliceString(data, COMMENT_OFFSET, loadTestFontId.length, + loadTestFontId); + // CFF checksum is important for IE, adjusting it + var CFF_CHECKSUM_OFFSET = 16; + var XXXX_VALUE = 0x58585858; // the "comment" filled with 'X' + var checksum = int32(data, CFF_CHECKSUM_OFFSET); + for (i = 0, ii = loadTestFontId.length - 3; i < ii; i += 4) { + checksum = (checksum - XXXX_VALUE + int32(loadTestFontId, i)) | 0; + } + if (i < loadTestFontId.length) { // align to 4 bytes boundary + checksum = (checksum - XXXX_VALUE + + int32(loadTestFontId + 'XXX', i)) | 0; + } + data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, string32(checksum)); + + var url = 'url(data:font/opentype;base64,' + btoa(data) + ');'; + var rule = '@font-face { font-family:"' + loadTestFontId + '";src:' + + url + '}'; + FontLoader.insertRule(rule); + + var names = []; + for (i = 0, ii = fonts.length; i < ii; i++) { + names.push(fonts[i].loadedName); + } + names.push(loadTestFontId); + + var div = document.createElement('div'); + div.setAttribute('style', + 'visibility: hidden;' + + 'width: 10px; height: 10px;' + + 'position: absolute; top: 0px; left: 0px;'); + for (i = 0, ii = names.length; i < ii; ++i) { + var span = document.createElement('span'); + span.textContent = 'Hi'; + span.style.fontFamily = names[i]; + div.appendChild(span); + } + document.body.appendChild(div); + + isFontReady(loadTestFontId, function() { + document.body.removeChild(div); + request.complete(); + }); + /** Hack end */ + } +}; + +var FontFaceObject = (function FontFaceObjectClosure() { + function FontFaceObject(name, file, properties) { + this.compiledGlyphs = {}; + if (arguments.length === 1) { + // importing translated data + var data = arguments[0]; + for (var i in data) { + this[i] = data[i]; + } + return; + } + } + FontFaceObject.prototype = { + createNativeFontFace: function FontFaceObject_createNativeFontFace() { + if (!this.data) { + return null; + } + + if (PDFJS.disableFontFace) { + this.disableFontFace = true; + return null; + } + + var nativeFontFace = new FontFace(this.loadedName, this.data, {}); + + FontLoader.addNativeFontFace(nativeFontFace); + + if (PDFJS.pdfBug && 'FontInspector' in globalScope && + globalScope['FontInspector'].enabled) { + globalScope['FontInspector'].fontAdded(this); + } + return nativeFontFace; + }, + + bindDOM: function FontFaceObject_bindDOM() { + if (!this.data) { + return null; + } + + if (PDFJS.disableFontFace) { + this.disableFontFace = true; + return null; + } + + var data = bytesToString(new Uint8Array(this.data)); + var fontName = this.loadedName; + + // Add the font-face rule to the document + var url = ('url(data:' + this.mimetype + ';base64,' + + window.btoa(data) + ');'); + var rule = '@font-face { font-family:"' + fontName + '";src:' + url + '}'; + FontLoader.insertRule(rule); + + if (PDFJS.pdfBug && 'FontInspector' in globalScope && + globalScope['FontInspector'].enabled) { + globalScope['FontInspector'].fontAdded(this, url); + } + + return rule; + }, + + getPathGenerator: function FontLoader_getPathGenerator(objs, character) { + if (!(character in this.compiledGlyphs)) { + var js = objs.get(this.loadedName + '_path_' + character); + /*jshint -W054 */ + this.compiledGlyphs[character] = new Function('c', 'size', js); + } + return this.compiledGlyphs[character]; + } + }; + return FontFaceObject; +})(); + + +var ANNOT_MIN_SIZE = 10; // px + +var AnnotationUtils = (function AnnotationUtilsClosure() { + // TODO(mack): This dupes some of the logic in CanvasGraphics.setFont() + function setTextStyles(element, item, fontObj) { + + var style = element.style; + style.fontSize = item.fontSize + 'px'; + style.direction = item.fontDirection < 0 ? 'rtl': 'ltr'; + + if (!fontObj) { + return; + } + + style.fontWeight = fontObj.black ? + (fontObj.bold ? 'bolder' : 'bold') : + (fontObj.bold ? 'bold' : 'normal'); + style.fontStyle = fontObj.italic ? 'italic' : 'normal'; + + var fontName = fontObj.loadedName; + var fontFamily = fontName ? '"' + fontName + '", ' : ''; + // Use a reasonable default font if the font doesn't specify a fallback + var fallbackName = fontObj.fallbackName || 'Helvetica, sans-serif'; + style.fontFamily = fontFamily + fallbackName; + } + + function initContainer(item, drawBorder) { + var container = document.createElement('section'); + var cstyle = container.style; + var width = item.rect[2] - item.rect[0]; + var height = item.rect[3] - item.rect[1]; + + var bWidth = item.borderWidth || 0; + if (bWidth) { + width = width - 2 * bWidth; + height = height - 2 * bWidth; + cstyle.borderWidth = bWidth + 'px'; + var color = item.color; + if (drawBorder && color) { + cstyle.borderStyle = 'solid'; + cstyle.borderColor = Util.makeCssRgb(Math.round(color[0] * 255), + Math.round(color[1] * 255), + Math.round(color[2] * 255)); + } + } + cstyle.width = width + 'px'; + cstyle.height = height + 'px'; + return container; + } + + function getHtmlElementForTextWidgetAnnotation(item, commonObjs) { + var element = document.createElement('div'); + var width = item.rect[2] - item.rect[0]; + var height = item.rect[3] - item.rect[1]; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + element.style.display = 'table'; + + var content = document.createElement('div'); + content.textContent = item.fieldValue; + var textAlignment = item.textAlignment; + content.style.textAlign = ['left', 'center', 'right'][textAlignment]; + content.style.verticalAlign = 'middle'; + content.style.display = 'table-cell'; + + var fontObj = item.fontRefName ? + commonObjs.getData(item.fontRefName) : null; + setTextStyles(content, item, fontObj); + + element.appendChild(content); + + return element; + } + + function getHtmlElementForTextAnnotation(item) { + var rect = item.rect; + + // sanity check because of OOo-generated PDFs + if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) { + rect[3] = rect[1] + ANNOT_MIN_SIZE; + } + if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) { + rect[2] = rect[0] + (rect[3] - rect[1]); // make it square + } + + var container = initContainer(item, false); + container.className = 'annotText'; + + var image = document.createElement('img'); + image.style.height = container.style.height; + image.style.width = container.style.width; + var iconName = item.name; + image.src = PDFJS.imageResourcesPath + 'annotation-' + + iconName.toLowerCase() + '.svg'; + image.alt = '[{{type}} Annotation]'; + image.dataset.l10nId = 'text_annotation_type'; + image.dataset.l10nArgs = JSON.stringify({type: iconName}); + + var contentWrapper = document.createElement('div'); + contentWrapper.className = 'annotTextContentWrapper'; + contentWrapper.style.left = Math.floor(rect[2] - rect[0] + 5) + 'px'; + contentWrapper.style.top = '-10px'; + + var content = document.createElement('div'); + content.className = 'annotTextContent'; + content.setAttribute('hidden', true); + + var i, ii; + if (item.hasBgColor) { + var color = item.color; + + // Enlighten the color (70%) + var BACKGROUND_ENLIGHT = 0.7; + var r = BACKGROUND_ENLIGHT * (1.0 - color[0]) + color[0]; + var g = BACKGROUND_ENLIGHT * (1.0 - color[1]) + color[1]; + var b = BACKGROUND_ENLIGHT * (1.0 - color[2]) + color[2]; + content.style.backgroundColor = Util.makeCssRgb((r * 255) | 0, + (g * 255) | 0, + (b * 255) | 0); + } + + var title = document.createElement('h1'); + var text = document.createElement('p'); + title.textContent = item.title; + + if (!item.content && !item.title) { + content.setAttribute('hidden', true); + } else { + var e = document.createElement('span'); + var lines = item.content.split(/(?:\r\n?|\n)/); + for (i = 0, ii = lines.length; i < ii; ++i) { + var line = lines[i]; + e.appendChild(document.createTextNode(line)); + if (i < (ii - 1)) { + e.appendChild(document.createElement('br')); + } + } + text.appendChild(e); + + var pinned = false; + + var showAnnotation = function showAnnotation(pin) { + if (pin) { + pinned = true; + } + if (content.hasAttribute('hidden')) { + container.style.zIndex += 1; + content.removeAttribute('hidden'); + } + }; + + var hideAnnotation = function hideAnnotation(unpin) { + if (unpin) { + pinned = false; + } + if (!content.hasAttribute('hidden') && !pinned) { + container.style.zIndex -= 1; + content.setAttribute('hidden', true); + } + }; + + var toggleAnnotation = function toggleAnnotation() { + if (pinned) { + hideAnnotation(true); + } else { + showAnnotation(true); + } + }; + + image.addEventListener('click', function image_clickHandler() { + toggleAnnotation(); + }, false); + image.addEventListener('mouseover', function image_mouseOverHandler() { + showAnnotation(); + }, false); + image.addEventListener('mouseout', function image_mouseOutHandler() { + hideAnnotation(); + }, false); + + content.addEventListener('click', function content_clickHandler() { + hideAnnotation(true); + }, false); + } + + content.appendChild(title); + content.appendChild(text); + contentWrapper.appendChild(content); + container.appendChild(image); + container.appendChild(contentWrapper); + + return container; + } + + function getHtmlElementForLinkAnnotation(item) { + var container = initContainer(item, true); + container.className = 'annotLink'; + + var link = document.createElement('a'); + link.href = link.title = item.url || ''; + if (item.url && PDFJS.openExternalLinksInNewWindow) { + link.target = '_blank'; + } + + container.appendChild(link); + + return container; + } + + function getHtmlElement(data, objs) { + switch (data.annotationType) { + case AnnotationType.WIDGET: + return getHtmlElementForTextWidgetAnnotation(data, objs); + case AnnotationType.TEXT: + return getHtmlElementForTextAnnotation(data); + case AnnotationType.LINK: + return getHtmlElementForLinkAnnotation(data); + default: + throw new Error('Unsupported annotationType: ' + data.annotationType); + } + } + + return { + getHtmlElement: getHtmlElement + }; +})(); +PDFJS.AnnotationUtils = AnnotationUtils; + + +var SVG_DEFAULTS = { + fontStyle: 'normal', + fontWeight: 'normal', + fillColor: '#000000' +}; + +var convertImgDataToPng = (function convertImgDataToPngClosure() { + var PNG_HEADER = + new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + + var CHUNK_WRAPPER_SIZE = 12; + + var crcTable = new Int32Array(256); + for (var i = 0; i < 256; i++) { + var c = i; + for (var h = 0; h < 8; h++) { + if (c & 1) { + c = 0xedB88320 ^ ((c >> 1) & 0x7fffffff); + } else { + c = (c >> 1) & 0x7fffffff; + } + } + crcTable[i] = c; + } + + function crc32(data, start, end) { + var crc = -1; + for (var i = start; i < end; i++) { + var a = (crc ^ data[i]) & 0xff; + var b = crcTable[a]; + crc = (crc >>> 8) ^ b; + } + return crc ^ -1; + } + + function writePngChunk(type, body, data, offset) { + var p = offset; + var len = body.length; + + data[p] = len >> 24 & 0xff; + data[p + 1] = len >> 16 & 0xff; + data[p + 2] = len >> 8 & 0xff; + data[p + 3] = len & 0xff; + p += 4; + + data[p] = type.charCodeAt(0) & 0xff; + data[p + 1] = type.charCodeAt(1) & 0xff; + data[p + 2] = type.charCodeAt(2) & 0xff; + data[p + 3] = type.charCodeAt(3) & 0xff; + p += 4; + + data.set(body, p); + p += body.length; + + var crc = crc32(data, offset + 4, p); + + data[p] = crc >> 24 & 0xff; + data[p + 1] = crc >> 16 & 0xff; + data[p + 2] = crc >> 8 & 0xff; + data[p + 3] = crc & 0xff; + } + + function adler32(data, start, end) { + var a = 1; + var b = 0; + for (var i = start; i < end; ++i) { + a = (a + (data[i] & 0xff)) % 65521; + b = (b + a) % 65521; + } + return (b << 16) | a; + } + + function encode(imgData, kind) { + var width = imgData.width; + var height = imgData.height; + var bitDepth, colorType, lineSize; + var bytes = imgData.data; + + switch (kind) { + case ImageKind.GRAYSCALE_1BPP: + colorType = 0; + bitDepth = 1; + lineSize = (width + 7) >> 3; + break; + case ImageKind.RGB_24BPP: + colorType = 2; + bitDepth = 8; + lineSize = width * 3; + break; + case ImageKind.RGBA_32BPP: + colorType = 6; + bitDepth = 8; + lineSize = width * 4; + break; + default: + throw new Error('invalid format'); + } + + // prefix every row with predictor 0 + var literals = new Uint8Array((1 + lineSize) * height); + var offsetLiterals = 0, offsetBytes = 0; + var y, i; + for (y = 0; y < height; ++y) { + literals[offsetLiterals++] = 0; // no prediction + literals.set(bytes.subarray(offsetBytes, offsetBytes + lineSize), + offsetLiterals); + offsetBytes += lineSize; + offsetLiterals += lineSize; + } + + if (kind === ImageKind.GRAYSCALE_1BPP) { + // inverting for B/W + offsetLiterals = 0; + for (y = 0; y < height; y++) { + offsetLiterals++; // skipping predictor + for (i = 0; i < lineSize; i++) { + literals[offsetLiterals++] ^= 0xFF; + } + } + } + + var ihdr = new Uint8Array([ + width >> 24 & 0xff, + width >> 16 & 0xff, + width >> 8 & 0xff, + width & 0xff, + height >> 24 & 0xff, + height >> 16 & 0xff, + height >> 8 & 0xff, + height & 0xff, + bitDepth, // bit depth + colorType, // color type + 0x00, // compression method + 0x00, // filter method + 0x00 // interlace method + ]); + + var len = literals.length; + var maxBlockLength = 0xFFFF; + + var deflateBlocks = Math.ceil(len / maxBlockLength); + var idat = new Uint8Array(2 + len + deflateBlocks * 5 + 4); + var pi = 0; + idat[pi++] = 0x78; // compression method and flags + idat[pi++] = 0x9c; // flags + + var pos = 0; + while (len > maxBlockLength) { + // writing non-final DEFLATE blocks type 0 and length of 65535 + idat[pi++] = 0x00; + idat[pi++] = 0xff; + idat[pi++] = 0xff; + idat[pi++] = 0x00; + idat[pi++] = 0x00; + idat.set(literals.subarray(pos, pos + maxBlockLength), pi); + pi += maxBlockLength; + pos += maxBlockLength; + len -= maxBlockLength; + } + + // writing non-final DEFLATE blocks type 0 + idat[pi++] = 0x01; + idat[pi++] = len & 0xff; + idat[pi++] = len >> 8 & 0xff; + idat[pi++] = (~len & 0xffff) & 0xff; + idat[pi++] = (~len & 0xffff) >> 8 & 0xff; + idat.set(literals.subarray(pos), pi); + pi += literals.length - pos; + + var adler = adler32(literals, 0, literals.length); // checksum + idat[pi++] = adler >> 24 & 0xff; + idat[pi++] = adler >> 16 & 0xff; + idat[pi++] = adler >> 8 & 0xff; + idat[pi++] = adler & 0xff; + + // PNG will consists: header, IHDR+data, IDAT+data, and IEND. + var pngLength = PNG_HEADER.length + (CHUNK_WRAPPER_SIZE * 3) + + ihdr.length + idat.length; + var data = new Uint8Array(pngLength); + var offset = 0; + data.set(PNG_HEADER, offset); + offset += PNG_HEADER.length; + writePngChunk('IHDR', ihdr, data, offset); + offset += CHUNK_WRAPPER_SIZE + ihdr.length; + writePngChunk('IDATA', idat, data, offset); + offset += CHUNK_WRAPPER_SIZE + idat.length; + writePngChunk('IEND', new Uint8Array(0), data, offset); + + return PDFJS.createObjectURL(data, 'image/png'); + } + + return function convertImgDataToPng(imgData) { + var kind = (imgData.kind === undefined ? + ImageKind.GRAYSCALE_1BPP : imgData.kind); + return encode(imgData, kind); + }; +})(); + +var SVGExtraState = (function SVGExtraStateClosure() { + function SVGExtraState() { + this.fontSizeScale = 1; + this.fontWeight = SVG_DEFAULTS.fontWeight; + this.fontSize = 0; + + this.textMatrix = IDENTITY_MATRIX; + this.fontMatrix = FONT_IDENTITY_MATRIX; + this.leading = 0; + + // Current point (in user coordinates) + this.x = 0; + this.y = 0; + + // Start of text line (in text coordinates) + this.lineX = 0; + this.lineY = 0; + + // Character and word spacing + this.charSpacing = 0; + this.wordSpacing = 0; + this.textHScale = 1; + this.textRise = 0; + + // Default foreground and background colors + this.fillColor = SVG_DEFAULTS.fillColor; + this.strokeColor = '#000000'; + + this.fillAlpha = 1; + this.strokeAlpha = 1; + this.lineWidth = 1; + this.lineJoin = ''; + this.lineCap = ''; + this.miterLimit = 0; + + this.dashArray = []; + this.dashPhase = 0; + + this.dependencies = []; + + // Clipping + this.clipId = ''; + this.pendingClip = false; + + this.maskId = ''; + } + + SVGExtraState.prototype = { + clone: function SVGExtraState_clone() { + return Object.create(this); + }, + setCurrentPoint: function SVGExtraState_setCurrentPoint(x, y) { + this.x = x; + this.y = y; + } + }; + return SVGExtraState; +})(); + +var SVGGraphics = (function SVGGraphicsClosure() { + function createScratchSVG(width, height) { + var NS = 'http://www.w3.org/2000/svg'; + var svg = document.createElementNS(NS, 'svg:svg'); + svg.setAttributeNS(null, 'version', '1.1'); + svg.setAttributeNS(null, 'width', width + 'px'); + svg.setAttributeNS(null, 'height', height + 'px'); + svg.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height); + return svg; + } + + function opListToTree(opList) { + var opTree = []; + var tmp = []; + var opListLen = opList.length; + + for (var x = 0; x < opListLen; x++) { + if (opList[x].fn === 'save') { + opTree.push({'fnId': 92, 'fn': 'group', 'items': []}); + tmp.push(opTree); + opTree = opTree[opTree.length - 1].items; + continue; + } + + if(opList[x].fn === 'restore') { + opTree = tmp.pop(); + } else { + opTree.push(opList[x]); + } + } + return opTree; + } + + /** + * Formats float number. + * @param value {number} number to format. + * @returns {string} + */ + function pf(value) { + if (value === (value | 0)) { // integer number + return value.toString(); + } + var s = value.toFixed(10); + var i = s.length - 1; + if (s[i] !== '0') { + return s; + } + // removing trailing zeros + do { + i--; + } while (s[i] === '0'); + return s.substr(0, s[i] === '.' ? i : i + 1); + } + + /** + * Formats transform matrix. The standard rotation, scale and translate + * matrices are replaced by their shorter forms, and for identity matrix + * returns empty string to save the memory. + * @param m {Array} matrix to format. + * @returns {string} + */ + function pm(m) { + if (m[4] === 0 && m[5] === 0) { + if (m[1] === 0 && m[2] === 0) { + if (m[0] === 1 && m[3] === 1) { + return ''; + } + return 'scale(' + pf(m[0]) + ' ' + pf(m[3]) + ')'; + } + if (m[0] === m[3] && m[1] === -m[2]) { + var a = Math.acos(m[0]) * 180 / Math.PI; + return 'rotate(' + pf(a) + ')'; + } + } else { + if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1) { + return 'translate(' + pf(m[4]) + ' ' + pf(m[5]) + ')'; + } + } + return 'matrix(' + pf(m[0]) + ' ' + pf(m[1]) + ' ' + pf(m[2]) + ' ' + + pf(m[3]) + ' ' + pf(m[4]) + ' ' + pf(m[5]) + ')'; + } + + function SVGGraphics(commonObjs, objs) { + this.current = new SVGExtraState(); + this.transformMatrix = IDENTITY_MATRIX; // Graphics state matrix + this.transformStack = []; + this.extraStack = []; + this.commonObjs = commonObjs; + this.objs = objs; + this.pendingEOFill = false; + + this.embedFonts = false; + this.embeddedFonts = {}; + this.cssStyle = null; + } + + var NS = 'http://www.w3.org/2000/svg'; + var XML_NS = 'http://www.w3.org/XML/1998/namespace'; + var XLINK_NS = 'http://www.w3.org/1999/xlink'; + var LINE_CAP_STYLES = ['butt', 'round', 'square']; + var LINE_JOIN_STYLES = ['miter', 'round', 'bevel']; + var clipCount = 0; + var maskCount = 0; + + SVGGraphics.prototype = { + save: function SVGGraphics_save() { + this.transformStack.push(this.transformMatrix); + var old = this.current; + this.extraStack.push(old); + this.current = old.clone(); + }, + + restore: function SVGGraphics_restore() { + this.transformMatrix = this.transformStack.pop(); + this.current = this.extraStack.pop(); + + this.tgrp = document.createElementNS(NS, 'svg:g'); + this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + this.pgrp.appendChild(this.tgrp); + }, + + group: function SVGGraphics_group(items) { + this.save(); + this.executeOpTree(items); + this.restore(); + }, + + loadDependencies: function SVGGraphics_loadDependencies(operatorList) { + var fnArray = operatorList.fnArray; + var fnArrayLen = fnArray.length; + var argsArray = operatorList.argsArray; + + var self = this; + for (var i = 0; i < fnArrayLen; i++) { + if (OPS.dependency === fnArray[i]) { + var deps = argsArray[i]; + for (var n = 0, nn = deps.length; n < nn; n++) { + var obj = deps[n]; + var common = obj.substring(0, 2) === 'g_'; + var promise; + if (common) { + promise = new Promise(function(resolve) { + self.commonObjs.get(obj, resolve); + }); + } else { + promise = new Promise(function(resolve) { + self.objs.get(obj, resolve); + }); + } + this.current.dependencies.push(promise); + } + } + } + return Promise.all(this.current.dependencies); + }, + + transform: function SVGGraphics_transform(a, b, c, d, e, f) { + var transformMatrix = [a, b, c, d, e, f]; + this.transformMatrix = PDFJS.Util.transform(this.transformMatrix, + transformMatrix); + + this.tgrp = document.createElementNS(NS, 'svg:g'); + this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + }, + + getSVG: function SVGGraphics_getSVG(operatorList, viewport) { + this.svg = createScratchSVG(viewport.width, viewport.height); + this.viewport = viewport; + + return this.loadDependencies(operatorList).then(function () { + this.transformMatrix = IDENTITY_MATRIX; + this.pgrp = document.createElementNS(NS, 'svg:g'); // Parent group + this.pgrp.setAttributeNS(null, 'transform', pm(viewport.transform)); + this.tgrp = document.createElementNS(NS, 'svg:g'); // Transform group + this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + this.defs = document.createElementNS(NS, 'svg:defs'); + this.pgrp.appendChild(this.defs); + this.pgrp.appendChild(this.tgrp); + this.svg.appendChild(this.pgrp); + var opTree = this.convertOpList(operatorList); + this.executeOpTree(opTree); + return this.svg; + }.bind(this)); + }, + + convertOpList: function SVGGraphics_convertOpList(operatorList) { + var argsArray = operatorList.argsArray; + var fnArray = operatorList.fnArray; + var fnArrayLen = fnArray.length; + var REVOPS = []; + var opList = []; + + for (var op in OPS) { + REVOPS[OPS[op]] = op; + } + + for (var x = 0; x < fnArrayLen; x++) { + var fnId = fnArray[x]; + opList.push({'fnId' : fnId, 'fn': REVOPS[fnId], 'args': argsArray[x]}); + } + return opListToTree(opList); + }, + + executeOpTree: function SVGGraphics_executeOpTree(opTree) { + var opTreeLen = opTree.length; + for(var x = 0; x < opTreeLen; x++) { + var fn = opTree[x].fn; + var fnId = opTree[x].fnId; + var args = opTree[x].args; + + switch (fnId | 0) { + case OPS.beginText: + this.beginText(); + break; + case OPS.setLeading: + this.setLeading(args); + break; + case OPS.setLeadingMoveText: + this.setLeadingMoveText(args[0], args[1]); + break; + case OPS.setFont: + this.setFont(args); + break; + case OPS.showText: + this.showText(args[0]); + break; + case OPS.showSpacedText: + this.showText(args[0]); + break; + case OPS.endText: + this.endText(); + break; + case OPS.moveText: + this.moveText(args[0], args[1]); + break; + case OPS.setCharSpacing: + this.setCharSpacing(args[0]); + break; + case OPS.setWordSpacing: + this.setWordSpacing(args[0]); + break; + case OPS.setHScale: + this.setHScale(args[0]); + break; + case OPS.setTextMatrix: + this.setTextMatrix(args[0], args[1], args[2], + args[3], args[4], args[5]); + break; + case OPS.setLineWidth: + this.setLineWidth(args[0]); + break; + case OPS.setLineJoin: + this.setLineJoin(args[0]); + break; + case OPS.setLineCap: + this.setLineCap(args[0]); + break; + case OPS.setMiterLimit: + this.setMiterLimit(args[0]); + break; + case OPS.setFillRGBColor: + this.setFillRGBColor(args[0], args[1], args[2]); + break; + case OPS.setStrokeRGBColor: + this.setStrokeRGBColor(args[0], args[1], args[2]); + break; + case OPS.setDash: + this.setDash(args[0], args[1]); + break; + case OPS.setGState: + this.setGState(args[0]); + break; + case OPS.fill: + this.fill(); + break; + case OPS.eoFill: + this.eoFill(); + break; + case OPS.stroke: + this.stroke(); + break; + case OPS.fillStroke: + this.fillStroke(); + break; + case OPS.eoFillStroke: + this.eoFillStroke(); + break; + case OPS.clip: + this.clip('nonzero'); + break; + case OPS.eoClip: + this.clip('evenodd'); + break; + case OPS.paintSolidColorImageMask: + this.paintSolidColorImageMask(); + break; + case OPS.paintJpegXObject: + this.paintJpegXObject(args[0], args[1], args[2]); + break; + case OPS.paintImageXObject: + this.paintImageXObject(args[0]); + break; + case OPS.paintInlineImageXObject: + this.paintInlineImageXObject(args[0]); + break; + case OPS.paintImageMaskXObject: + this.paintImageMaskXObject(args[0]); + break; + case OPS.paintFormXObjectBegin: + this.paintFormXObjectBegin(args[0], args[1]); + break; + case OPS.paintFormXObjectEnd: + this.paintFormXObjectEnd(); + break; + case OPS.closePath: + this.closePath(); + break; + case OPS.closeStroke: + this.closeStroke(); + break; + case OPS.closeFillStroke: + this.closeFillStroke(); + break; + case OPS.nextLine: + this.nextLine(); + break; + case OPS.transform: + this.transform(args[0], args[1], args[2], args[3], + args[4], args[5]); + break; + case OPS.constructPath: + this.constructPath(args[0], args[1]); + break; + case OPS.endPath: + this.endPath(); + break; + case 92: + this.group(opTree[x].items); + break; + default: + warn('Unimplemented method '+ fn); + break; + } + } + }, + + setWordSpacing: function SVGGraphics_setWordSpacing(wordSpacing) { + this.current.wordSpacing = wordSpacing; + }, + + setCharSpacing: function SVGGraphics_setCharSpacing(charSpacing) { + this.current.charSpacing = charSpacing; + }, + + nextLine: function SVGGraphics_nextLine() { + this.moveText(0, this.current.leading); + }, + + setTextMatrix: function SVGGraphics_setTextMatrix(a, b, c, d, e, f) { + var current = this.current; + this.current.textMatrix = this.current.lineMatrix = [a, b, c, d, e, f]; + + this.current.x = this.current.lineX = 0; + this.current.y = this.current.lineY = 0; + + current.xcoords = []; + current.tspan = document.createElementNS(NS, 'svg:tspan'); + current.tspan.setAttributeNS(null, 'font-family', current.fontFamily); + current.tspan.setAttributeNS(null, 'font-size', + pf(current.fontSize) + 'px'); + current.tspan.setAttributeNS(null, 'y', pf(-current.y)); + + current.txtElement = document.createElementNS(NS, 'svg:text'); + current.txtElement.appendChild(current.tspan); + }, + + beginText: function SVGGraphics_beginText() { + this.current.x = this.current.lineX = 0; + this.current.y = this.current.lineY = 0; + this.current.textMatrix = IDENTITY_MATRIX; + this.current.lineMatrix = IDENTITY_MATRIX; + this.current.tspan = document.createElementNS(NS, 'svg:tspan'); + this.current.txtElement = document.createElementNS(NS, 'svg:text'); + this.current.txtgrp = document.createElementNS(NS, 'svg:g'); + this.current.xcoords = []; + }, + + moveText: function SVGGraphics_moveText(x, y) { + var current = this.current; + this.current.x = this.current.lineX += x; + this.current.y = this.current.lineY += y; + + current.xcoords = []; + current.tspan = document.createElementNS(NS, 'svg:tspan'); + current.tspan.setAttributeNS(null, 'font-family', current.fontFamily); + current.tspan.setAttributeNS(null, 'font-size', + pf(current.fontSize) + 'px'); + current.tspan.setAttributeNS(null, 'y', pf(-current.y)); + }, + + showText: function SVGGraphics_showText(glyphs) { + var current = this.current; + var font = current.font; + var fontSize = current.fontSize; + + if (fontSize === 0) { + return; + } + + var charSpacing = current.charSpacing; + var wordSpacing = current.wordSpacing; + var fontDirection = current.fontDirection; + var textHScale = current.textHScale * fontDirection; + var glyphsLength = glyphs.length; + var vertical = font.vertical; + var widthAdvanceScale = fontSize * current.fontMatrix[0]; + + var x = 0, i; + for (i = 0; i < glyphsLength; ++i) { + var glyph = glyphs[i]; + if (glyph === null) { + // word break + x += fontDirection * wordSpacing; + continue; + } else if (isNum(glyph)) { + x += -glyph * fontSize * 0.001; + continue; + } + current.xcoords.push(current.x + x * textHScale); + + var width = glyph.width; + var character = glyph.fontChar; + var charWidth = width * widthAdvanceScale + charSpacing * fontDirection; + x += charWidth; + + current.tspan.textContent += character; + } + if (vertical) { + current.y -= x * textHScale; + } else { + current.x += x * textHScale; + } + + current.tspan.setAttributeNS(null, 'x', + current.xcoords.map(pf).join(' ')); + current.tspan.setAttributeNS(null, 'y', pf(-current.y)); + current.tspan.setAttributeNS(null, 'font-family', current.fontFamily); + current.tspan.setAttributeNS(null, 'font-size', + pf(current.fontSize) + 'px'); + if (current.fontStyle !== SVG_DEFAULTS.fontStyle) { + current.tspan.setAttributeNS(null, 'font-style', current.fontStyle); + } + if (current.fontWeight !== SVG_DEFAULTS.fontWeight) { + current.tspan.setAttributeNS(null, 'font-weight', current.fontWeight); + } + if (current.fillColor !== SVG_DEFAULTS.fillColor) { + current.tspan.setAttributeNS(null, 'fill', current.fillColor); + } + + current.txtElement.setAttributeNS(null, 'transform', + pm(current.textMatrix) + + ' scale(1, -1)' ); + current.txtElement.setAttributeNS(XML_NS, 'xml:space', 'preserve'); + current.txtElement.appendChild(current.tspan); + current.txtgrp.appendChild(current.txtElement); + + this.tgrp.appendChild(current.txtElement); + + }, + + setLeadingMoveText: function SVGGraphics_setLeadingMoveText(x, y) { + this.setLeading(-y); + this.moveText(x, y); + }, + + addFontStyle: function SVGGraphics_addFontStyle(fontObj) { + if (!this.cssStyle) { + this.cssStyle = document.createElementNS(NS, 'svg:style'); + this.cssStyle.setAttributeNS(null, 'type', 'text/css'); + this.defs.appendChild(this.cssStyle); + } + + var url = PDFJS.createObjectURL(fontObj.data, fontObj.mimetype); + this.cssStyle.textContent += + '@font-face { font-family: "' + fontObj.loadedName + '";' + + ' src: url(' + url + '); }\n'; + }, + + setFont: function SVGGraphics_setFont(details) { + var current = this.current; + var fontObj = this.commonObjs.get(details[0]); + var size = details[1]; + this.current.font = fontObj; + + if (this.embedFonts && fontObj.data && + !this.embeddedFonts[fontObj.loadedName]) { + this.addFontStyle(fontObj); + this.embeddedFonts[fontObj.loadedName] = fontObj; + } + + current.fontMatrix = (fontObj.fontMatrix ? + fontObj.fontMatrix : FONT_IDENTITY_MATRIX); + + var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') : + (fontObj.bold ? 'bold' : 'normal'); + var italic = fontObj.italic ? 'italic' : 'normal'; + + if (size < 0) { + size = -size; + current.fontDirection = -1; + } else { + current.fontDirection = 1; + } + current.fontSize = size; + current.fontFamily = fontObj.loadedName; + current.fontWeight = bold; + current.fontStyle = italic; + + current.tspan = document.createElementNS(NS, 'svg:tspan'); + current.tspan.setAttributeNS(null, 'y', pf(-current.y)); + current.xcoords = []; + }, + + endText: function SVGGraphics_endText() { + if (this.current.pendingClip) { + this.cgrp.appendChild(this.tgrp); + this.pgrp.appendChild(this.cgrp); + } else { + this.pgrp.appendChild(this.tgrp); + } + this.tgrp = document.createElementNS(NS, 'svg:g'); + this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + }, + + // Path properties + setLineWidth: function SVGGraphics_setLineWidth(width) { + this.current.lineWidth = width; + }, + setLineCap: function SVGGraphics_setLineCap(style) { + this.current.lineCap = LINE_CAP_STYLES[style]; + }, + setLineJoin: function SVGGraphics_setLineJoin(style) { + this.current.lineJoin = LINE_JOIN_STYLES[style]; + }, + setMiterLimit: function SVGGraphics_setMiterLimit(limit) { + this.current.miterLimit = limit; + }, + setStrokeRGBColor: function SVGGraphics_setStrokeRGBColor(r, g, b) { + var color = Util.makeCssRgb(r, g, b); + this.current.strokeColor = color; + }, + setFillRGBColor: function SVGGraphics_setFillRGBColor(r, g, b) { + var color = Util.makeCssRgb(r, g, b); + this.current.fillColor = color; + this.current.tspan = document.createElementNS(NS, 'svg:tspan'); + this.current.xcoords = []; + }, + setDash: function SVGGraphics_setDash(dashArray, dashPhase) { + this.current.dashArray = dashArray; + this.current.dashPhase = dashPhase; + }, + + constructPath: function SVGGraphics_constructPath(ops, args) { + var current = this.current; + var x = current.x, y = current.y; + current.path = document.createElementNS(NS, 'svg:path'); + var d = []; + var opLength = ops.length; + + for (var i = 0, j = 0; i < opLength; i++) { + switch (ops[i] | 0) { + case OPS.rectangle: + x = args[j++]; + y = args[j++]; + var width = args[j++]; + var height = args[j++]; + var xw = x + width; + var yh = y + height; + d.push('M', pf(x), pf(y), 'L', pf(xw) , pf(y), 'L', pf(xw), pf(yh), + 'L', pf(x), pf(yh), 'Z'); + break; + case OPS.moveTo: + x = args[j++]; + y = args[j++]; + d.push('M', pf(x), pf(y)); + break; + case OPS.lineTo: + x = args[j++]; + y = args[j++]; + d.push('L', pf(x) , pf(y)); + break; + case OPS.curveTo: + x = args[j + 4]; + y = args[j + 5]; + d.push('C', pf(args[j]), pf(args[j + 1]), pf(args[j + 2]), + pf(args[j + 3]), pf(x), pf(y)); + j += 6; + break; + case OPS.curveTo2: + x = args[j + 2]; + y = args[j + 3]; + d.push('C', pf(x), pf(y), pf(args[j]), pf(args[j + 1]), + pf(args[j + 2]), pf(args[j + 3])); + j += 4; + break; + case OPS.curveTo3: + x = args[j + 2]; + y = args[j + 3]; + d.push('C', pf(args[j]), pf(args[j + 1]), pf(x), pf(y), + pf(x), pf(y)); + j += 4; + break; + case OPS.closePath: + d.push('Z'); + break; + } + } + current.path.setAttributeNS(null, 'd', d.join(' ')); + current.path.setAttributeNS(null, 'stroke-miterlimit', + pf(current.miterLimit)); + current.path.setAttributeNS(null, 'stroke-linecap', current.lineCap); + current.path.setAttributeNS(null, 'stroke-linejoin', current.lineJoin); + current.path.setAttributeNS(null, 'stroke-width', + pf(current.lineWidth) + 'px'); + current.path.setAttributeNS(null, 'stroke-dasharray', + current.dashArray.map(pf).join(' ')); + current.path.setAttributeNS(null, 'stroke-dashoffset', + pf(current.dashPhase) + 'px'); + current.path.setAttributeNS(null, 'fill', 'none'); + + this.tgrp.appendChild(current.path); + if (current.pendingClip) { + this.cgrp.appendChild(this.tgrp); + this.pgrp.appendChild(this.cgrp); + } else { + this.pgrp.appendChild(this.tgrp); + } + // Saving a reference in current.element so that it can be addressed + // in 'fill' and 'stroke' + current.element = current.path; + current.setCurrentPoint(x, y); + }, + + endPath: function SVGGraphics_endPath() { + var current = this.current; + if (current.pendingClip) { + this.cgrp.appendChild(this.tgrp); + this.pgrp.appendChild(this.cgrp); + } else { + this.pgrp.appendChild(this.tgrp); + } + this.tgrp = document.createElementNS(NS, 'svg:g'); + this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + }, + + clip: function SVGGraphics_clip(type) { + var current = this.current; + // Add current path to clipping path + current.clipId = 'clippath' + clipCount; + clipCount++; + this.clippath = document.createElementNS(NS, 'svg:clipPath'); + this.clippath.setAttributeNS(null, 'id', current.clipId); + var clipElement = current.element.cloneNode(); + if (type === 'evenodd') { + clipElement.setAttributeNS(null, 'clip-rule', 'evenodd'); + } else { + clipElement.setAttributeNS(null, 'clip-rule', 'nonzero'); + } + this.clippath.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + this.clippath.appendChild(clipElement); + this.defs.appendChild(this.clippath); + + // Create a new group with that attribute + current.pendingClip = true; + this.cgrp = document.createElementNS(NS, 'svg:g'); + this.cgrp.setAttributeNS(null, 'clip-path', + 'url(#' + current.clipId + ')'); + this.pgrp.appendChild(this.cgrp); + }, + + closePath: function SVGGraphics_closePath() { + var current = this.current; + var d = current.path.getAttributeNS(null, 'd'); + d += 'Z'; + current.path.setAttributeNS(null, 'd', d); + }, + + setLeading: function SVGGraphics_setLeading(leading) { + this.current.leading = -leading; + }, + + setTextRise: function SVGGraphics_setTextRise(textRise) { + this.current.textRise = textRise; + }, + + setHScale: function SVGGraphics_setHScale(scale) { + this.current.textHScale = scale / 100; + }, + + setGState: function SVGGraphics_setGState(states) { + for (var i = 0, ii = states.length; i < ii; i++) { + var state = states[i]; + var key = state[0]; + var value = state[1]; + + switch (key) { + case 'LW': + this.setLineWidth(value); + break; + case 'LC': + this.setLineCap(value); + break; + case 'LJ': + this.setLineJoin(value); + break; + case 'ML': + this.setMiterLimit(value); + break; + case 'D': + this.setDash(value[0], value[1]); + break; + case 'RI': + break; + case 'FL': + break; + case 'Font': + this.setFont(value); + break; + case 'CA': + break; + case 'ca': + break; + case 'BM': + break; + case 'SMask': + break; + } + } + }, + + fill: function SVGGraphics_fill() { + var current = this.current; + current.element.setAttributeNS(null, 'fill', current.fillColor); + }, + + stroke: function SVGGraphics_stroke() { + var current = this.current; + current.element.setAttributeNS(null, 'stroke', current.strokeColor); + current.element.setAttributeNS(null, 'fill', 'none'); + }, + + eoFill: function SVGGraphics_eoFill() { + var current = this.current; + current.element.setAttributeNS(null, 'fill', current.fillColor); + current.element.setAttributeNS(null, 'fill-rule', 'evenodd'); + }, + + fillStroke: function SVGGraphics_fillStroke() { + // Order is important since stroke wants fill to be none. + // First stroke, then if fill needed, it will be overwritten. + this.stroke(); + this.fill(); + }, + + eoFillStroke: function SVGGraphics_eoFillStroke() { + this.current.element.setAttributeNS(null, 'fill-rule', 'evenodd'); + this.fillStroke(); + }, + + closeStroke: function SVGGraphics_closeStroke() { + this.closePath(); + this.stroke(); + }, + + closeFillStroke: function SVGGraphics_closeFillStroke() { + this.closePath(); + this.fillStroke(); + }, + + paintSolidColorImageMask: + function SVGGraphics_paintSolidColorImageMask() { + var current = this.current; + var rect = document.createElementNS(NS, 'svg:rect'); + rect.setAttributeNS(null, 'x', '0'); + rect.setAttributeNS(null, 'y', '0'); + rect.setAttributeNS(null, 'width', '1px'); + rect.setAttributeNS(null, 'height', '1px'); + rect.setAttributeNS(null, 'fill', current.fillColor); + this.tgrp.appendChild(rect); + }, + + paintJpegXObject: function SVGGraphics_paintJpegXObject(objId, w, h) { + var current = this.current; + var imgObj = this.objs.get(objId); + var imgEl = document.createElementNS(NS, 'svg:image'); + imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgObj.src); + imgEl.setAttributeNS(null, 'width', imgObj.width + 'px'); + imgEl.setAttributeNS(null, 'height', imgObj.height + 'px'); + imgEl.setAttributeNS(null, 'x', '0'); + imgEl.setAttributeNS(null, 'y', pf(-h)); + imgEl.setAttributeNS(null, 'transform', + 'scale(' + pf(1 / w) + ' ' + pf(-1 / h) + ')'); + + this.tgrp.appendChild(imgEl); + if (current.pendingClip) { + this.cgrp.appendChild(this.tgrp); + this.pgrp.appendChild(this.cgrp); + } else { + this.pgrp.appendChild(this.tgrp); + } + }, + + paintImageXObject: function SVGGraphics_paintImageXObject(objId) { + var imgData = this.objs.get(objId); + if (!imgData) { + warn('Dependent image isn\'t ready yet'); + return; + } + this.paintInlineImageXObject(imgData); + }, + + paintInlineImageXObject: + function SVGGraphics_paintInlineImageXObject(imgData, mask) { + var current = this.current; + var width = imgData.width; + var height = imgData.height; + + var imgSrc = convertImgDataToPng(imgData); + var cliprect = document.createElementNS(NS, 'svg:rect'); + cliprect.setAttributeNS(null, 'x', '0'); + cliprect.setAttributeNS(null, 'y', '0'); + cliprect.setAttributeNS(null, 'width', pf(width)); + cliprect.setAttributeNS(null, 'height', pf(height)); + current.element = cliprect; + this.clip('nonzero'); + var imgEl = document.createElementNS(NS, 'svg:image'); + imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgSrc); + imgEl.setAttributeNS(null, 'x', '0'); + imgEl.setAttributeNS(null, 'y', pf(-height)); + imgEl.setAttributeNS(null, 'width', pf(width) + 'px'); + imgEl.setAttributeNS(null, 'height', pf(height) + 'px'); + imgEl.setAttributeNS(null, 'transform', + 'scale(' + pf(1 / width) + ' ' + + pf(-1 / height) + ')'); + if (mask) { + mask.appendChild(imgEl); + } else { + this.tgrp.appendChild(imgEl); + } + if (current.pendingClip) { + this.cgrp.appendChild(this.tgrp); + this.pgrp.appendChild(this.cgrp); + } else { + this.pgrp.appendChild(this.tgrp); + } + }, + + paintImageMaskXObject: + function SVGGraphics_paintImageMaskXObject(imgData) { + var current = this.current; + var width = imgData.width; + var height = imgData.height; + var fillColor = current.fillColor; + + current.maskId = 'mask' + maskCount++; + var mask = document.createElementNS(NS, 'svg:mask'); + mask.setAttributeNS(null, 'id', current.maskId); + + var rect = document.createElementNS(NS, 'svg:rect'); + rect.setAttributeNS(null, 'x', '0'); + rect.setAttributeNS(null, 'y', '0'); + rect.setAttributeNS(null, 'width', pf(width)); + rect.setAttributeNS(null, 'height', pf(height)); + rect.setAttributeNS(null, 'fill', fillColor); + rect.setAttributeNS(null, 'mask', 'url(#' + current.maskId +')'); + this.defs.appendChild(mask); + this.tgrp.appendChild(rect); + + this.paintInlineImageXObject(imgData, mask); + }, + + paintFormXObjectBegin: + function SVGGraphics_paintFormXObjectBegin(matrix, bbox) { + this.save(); + + if (isArray(matrix) && matrix.length === 6) { + this.transform(matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5]); + } + + if (isArray(bbox) && bbox.length === 4) { + var width = bbox[2] - bbox[0]; + var height = bbox[3] - bbox[1]; + + var cliprect = document.createElementNS(NS, 'svg:rect'); + cliprect.setAttributeNS(null, 'x', bbox[0]); + cliprect.setAttributeNS(null, 'y', bbox[1]); + cliprect.setAttributeNS(null, 'width', pf(width)); + cliprect.setAttributeNS(null, 'height', pf(height)); + this.current.element = cliprect; + this.clip('nonzero'); + this.endPath(); + } + }, + + paintFormXObjectEnd: + function SVGGraphics_paintFormXObjectEnd() { + this.restore(); + } + }; + return SVGGraphics; +})(); + +PDFJS.SVGGraphics = SVGGraphics; + + +}).call((typeof window === 'undefined') ? this : window); + +if (!PDFJS.workerSrc && typeof document !== 'undefined') { + // workerSrc is not set -- using last script url to define default location + PDFJS.workerSrc = (function () { + 'use strict'; + var scriptTagContainer = document.body || + document.getElementsByTagName('head')[0]; + var pdfjsSrc = scriptTagContainer.lastChild.src; + return pdfjsSrc && pdfjsSrc.replace(/\.js$/i, '.worker.js'); + })(); +} + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/build/pdf.worker.js b/test-module-system/test-system-biz/src/main/resources/static/generic/build/pdf.worker.js new file mode 100644 index 0000000..c025c14 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/build/pdf.worker.js @@ -0,0 +1,39372 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*jshint globalstrict: false */ +/* globals PDFJS */ + +// Initializing PDFJS global object (if still undefined) +if (typeof PDFJS === 'undefined') { + (typeof window !== 'undefined' ? window : this).PDFJS = {}; +} + +PDFJS.version = '1.1.159'; +PDFJS.build = '82536f8'; + +(function pdfjsWrapper() { + // Use strict in our context only - users might not want it + 'use strict'; + +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* globals Cmd, ColorSpace, Dict, MozBlobBuilder, Name, PDFJS, Ref, URL, + Promise */ + +'use strict'; + +var globalScope = (typeof window === 'undefined') ? this : window; + +var isWorker = (typeof window === 'undefined'); + +var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; + +var TextRenderingMode = { + FILL: 0, + STROKE: 1, + FILL_STROKE: 2, + INVISIBLE: 3, + FILL_ADD_TO_PATH: 4, + STROKE_ADD_TO_PATH: 5, + FILL_STROKE_ADD_TO_PATH: 6, + ADD_TO_PATH: 7, + FILL_STROKE_MASK: 3, + ADD_TO_PATH_FLAG: 4 +}; + +var ImageKind = { + GRAYSCALE_1BPP: 1, + RGB_24BPP: 2, + RGBA_32BPP: 3 +}; + +var AnnotationType = { + WIDGET: 1, + TEXT: 2, + LINK: 3 +}; + +var StreamType = { + UNKNOWN: 0, + FLATE: 1, + LZW: 2, + DCT: 3, + JPX: 4, + JBIG: 5, + A85: 6, + AHX: 7, + CCF: 8, + RL: 9 +}; + +var FontType = { + UNKNOWN: 0, + TYPE1: 1, + TYPE1C: 2, + CIDFONTTYPE0: 3, + CIDFONTTYPE0C: 4, + TRUETYPE: 5, + CIDFONTTYPE2: 6, + TYPE3: 7, + OPENTYPE: 8, + TYPE0: 9, + MMTYPE1: 10 +}; + +// The global PDFJS object exposes the API +// In production, it will be declared outside a global wrapper +// In development, it will be declared here +if (!globalScope.PDFJS) { + globalScope.PDFJS = {}; +} + +globalScope.PDFJS.pdfBug = false; + +PDFJS.VERBOSITY_LEVELS = { + errors: 0, + warnings: 1, + infos: 5 +}; + +// All the possible operations for an operator list. +var OPS = PDFJS.OPS = { + // Intentionally start from 1 so it is easy to spot bad operators that will be + // 0's. + dependency: 1, + setLineWidth: 2, + setLineCap: 3, + setLineJoin: 4, + setMiterLimit: 5, + setDash: 6, + setRenderingIntent: 7, + setFlatness: 8, + setGState: 9, + save: 10, + restore: 11, + transform: 12, + moveTo: 13, + lineTo: 14, + curveTo: 15, + curveTo2: 16, + curveTo3: 17, + closePath: 18, + rectangle: 19, + stroke: 20, + closeStroke: 21, + fill: 22, + eoFill: 23, + fillStroke: 24, + eoFillStroke: 25, + closeFillStroke: 26, + closeEOFillStroke: 27, + endPath: 28, + clip: 29, + eoClip: 30, + beginText: 31, + endText: 32, + setCharSpacing: 33, + setWordSpacing: 34, + setHScale: 35, + setLeading: 36, + setFont: 37, + setTextRenderingMode: 38, + setTextRise: 39, + moveText: 40, + setLeadingMoveText: 41, + setTextMatrix: 42, + nextLine: 43, + showText: 44, + showSpacedText: 45, + nextLineShowText: 46, + nextLineSetSpacingShowText: 47, + setCharWidth: 48, + setCharWidthAndBounds: 49, + setStrokeColorSpace: 50, + setFillColorSpace: 51, + setStrokeColor: 52, + setStrokeColorN: 53, + setFillColor: 54, + setFillColorN: 55, + setStrokeGray: 56, + setFillGray: 57, + setStrokeRGBColor: 58, + setFillRGBColor: 59, + setStrokeCMYKColor: 60, + setFillCMYKColor: 61, + shadingFill: 62, + beginInlineImage: 63, + beginImageData: 64, + endInlineImage: 65, + paintXObject: 66, + markPoint: 67, + markPointProps: 68, + beginMarkedContent: 69, + beginMarkedContentProps: 70, + endMarkedContent: 71, + beginCompat: 72, + endCompat: 73, + paintFormXObjectBegin: 74, + paintFormXObjectEnd: 75, + beginGroup: 76, + endGroup: 77, + beginAnnotations: 78, + endAnnotations: 79, + beginAnnotation: 80, + endAnnotation: 81, + paintJpegXObject: 82, + paintImageMaskXObject: 83, + paintImageMaskXObjectGroup: 84, + paintImageXObject: 85, + paintInlineImageXObject: 86, + paintInlineImageXObjectGroup: 87, + paintImageXObjectRepeat: 88, + paintImageMaskXObjectRepeat: 89, + paintSolidColorImageMask: 90, + constructPath: 91 +}; + +// A notice for devs. These are good for things that are helpful to devs, such +// as warning that Workers were disabled, which is important to devs but not +// end users. +function info(msg) { + if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.infos) { + console.log('Info: ' + msg); + } +} + +// Non-fatal warnings. +function warn(msg) { + if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.warnings) { + console.log('Warning: ' + msg); + } +} + +// Fatal errors that should trigger the fallback UI and halt execution by +// throwing an exception. +function error(msg) { + if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.errors) { + console.log('Error: ' + msg); + console.log(backtrace()); + } + UnsupportedManager.notify(UNSUPPORTED_FEATURES.unknown); + throw new Error(msg); +} + +function backtrace() { + try { + throw new Error(); + } catch (e) { + return e.stack ? e.stack.split('\n').slice(2).join('\n') : ''; + } +} + +function assert(cond, msg) { + if (!cond) { + error(msg); + } +} + +var UNSUPPORTED_FEATURES = PDFJS.UNSUPPORTED_FEATURES = { + unknown: 'unknown', + forms: 'forms', + javaScript: 'javaScript', + smask: 'smask', + shadingPattern: 'shadingPattern', + font: 'font' +}; + +var UnsupportedManager = PDFJS.UnsupportedManager = + (function UnsupportedManagerClosure() { + var listeners = []; + return { + listen: function (cb) { + listeners.push(cb); + }, + notify: function (featureId) { + warn('Unsupported feature "' + featureId + '"'); + for (var i = 0, ii = listeners.length; i < ii; i++) { + listeners[i](featureId); + } + } + }; +})(); + +// Combines two URLs. The baseUrl shall be absolute URL. If the url is an +// absolute URL, it will be returned as is. +function combineUrl(baseUrl, url) { + if (!url) { + return baseUrl; + } + if (/^[a-z][a-z0-9+\-.]*:/i.test(url)) { + return url; + } + var i; + if (url.charAt(0) === '/') { + // absolute path + i = baseUrl.indexOf('://'); + if (url.charAt(1) === '/') { + ++i; + } else { + i = baseUrl.indexOf('/', i + 3); + } + return baseUrl.substring(0, i) + url; + } else { + // relative path + var pathLength = baseUrl.length; + i = baseUrl.lastIndexOf('#'); + pathLength = i >= 0 ? i : pathLength; + i = baseUrl.lastIndexOf('?', pathLength); + pathLength = i >= 0 ? i : pathLength; + var prefixLength = baseUrl.lastIndexOf('/', pathLength); + return baseUrl.substring(0, prefixLength + 1) + url; + } +} + +// Validates if URL is safe and allowed, e.g. to avoid XSS. +function isValidUrl(url, allowRelative) { + if (!url) { + return false; + } + // RFC 3986 (http://tools.ietf.org/html/rfc3986#section-3.1) + // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + var protocol = /^[a-z][a-z0-9+\-.]*(?=:)/i.exec(url); + if (!protocol) { + return allowRelative; + } + protocol = protocol[0].toLowerCase(); + switch (protocol) { + case 'http': + case 'https': + case 'ftp': + case 'mailto': + case 'tel': + return true; + default: + return false; + } +} +PDFJS.isValidUrl = isValidUrl; + +function shadow(obj, prop, value) { + Object.defineProperty(obj, prop, { value: value, + enumerable: true, + configurable: true, + writable: false }); + return value; +} +PDFJS.shadow = shadow; + +var PasswordResponses = PDFJS.PasswordResponses = { + NEED_PASSWORD: 1, + INCORRECT_PASSWORD: 2 +}; + +var PasswordException = (function PasswordExceptionClosure() { + function PasswordException(msg, code) { + this.name = 'PasswordException'; + this.message = msg; + this.code = code; + } + + PasswordException.prototype = new Error(); + PasswordException.constructor = PasswordException; + + return PasswordException; +})(); +PDFJS.PasswordException = PasswordException; + +var UnknownErrorException = (function UnknownErrorExceptionClosure() { + function UnknownErrorException(msg, details) { + this.name = 'UnknownErrorException'; + this.message = msg; + this.details = details; + } + + UnknownErrorException.prototype = new Error(); + UnknownErrorException.constructor = UnknownErrorException; + + return UnknownErrorException; +})(); +PDFJS.UnknownErrorException = UnknownErrorException; + +var InvalidPDFException = (function InvalidPDFExceptionClosure() { + function InvalidPDFException(msg) { + this.name = 'InvalidPDFException'; + this.message = msg; + } + + InvalidPDFException.prototype = new Error(); + InvalidPDFException.constructor = InvalidPDFException; + + return InvalidPDFException; +})(); +PDFJS.InvalidPDFException = InvalidPDFException; + +var MissingPDFException = (function MissingPDFExceptionClosure() { + function MissingPDFException(msg) { + this.name = 'MissingPDFException'; + this.message = msg; + } + + MissingPDFException.prototype = new Error(); + MissingPDFException.constructor = MissingPDFException; + + return MissingPDFException; +})(); +PDFJS.MissingPDFException = MissingPDFException; + +var UnexpectedResponseException = + (function UnexpectedResponseExceptionClosure() { + function UnexpectedResponseException(msg, status) { + this.name = 'UnexpectedResponseException'; + this.message = msg; + this.status = status; + } + + UnexpectedResponseException.prototype = new Error(); + UnexpectedResponseException.constructor = UnexpectedResponseException; + + return UnexpectedResponseException; +})(); +PDFJS.UnexpectedResponseException = UnexpectedResponseException; + +var NotImplementedException = (function NotImplementedExceptionClosure() { + function NotImplementedException(msg) { + this.message = msg; + } + + NotImplementedException.prototype = new Error(); + NotImplementedException.prototype.name = 'NotImplementedException'; + NotImplementedException.constructor = NotImplementedException; + + return NotImplementedException; +})(); + +var MissingDataException = (function MissingDataExceptionClosure() { + function MissingDataException(begin, end) { + this.begin = begin; + this.end = end; + this.message = 'Missing data [' + begin + ', ' + end + ')'; + } + + MissingDataException.prototype = new Error(); + MissingDataException.prototype.name = 'MissingDataException'; + MissingDataException.constructor = MissingDataException; + + return MissingDataException; +})(); + +var XRefParseException = (function XRefParseExceptionClosure() { + function XRefParseException(msg) { + this.message = msg; + } + + XRefParseException.prototype = new Error(); + XRefParseException.prototype.name = 'XRefParseException'; + XRefParseException.constructor = XRefParseException; + + return XRefParseException; +})(); + + +function bytesToString(bytes) { + assert(bytes !== null && typeof bytes === 'object' && + bytes.length !== undefined, 'Invalid argument for bytesToString'); + var length = bytes.length; + var MAX_ARGUMENT_COUNT = 8192; + if (length < MAX_ARGUMENT_COUNT) { + return String.fromCharCode.apply(null, bytes); + } + var strBuf = []; + for (var i = 0; i < length; i += MAX_ARGUMENT_COUNT) { + var chunkEnd = Math.min(i + MAX_ARGUMENT_COUNT, length); + var chunk = bytes.subarray(i, chunkEnd); + strBuf.push(String.fromCharCode.apply(null, chunk)); + } + return strBuf.join(''); +} + +function stringToBytes(str) { + assert(typeof str === 'string', 'Invalid argument for stringToBytes'); + var length = str.length; + var bytes = new Uint8Array(length); + for (var i = 0; i < length; ++i) { + bytes[i] = str.charCodeAt(i) & 0xFF; + } + return bytes; +} + +function string32(value) { + return String.fromCharCode((value >> 24) & 0xff, (value >> 16) & 0xff, + (value >> 8) & 0xff, value & 0xff); +} + +function log2(x) { + var n = 1, i = 0; + while (x > n) { + n <<= 1; + i++; + } + return i; +} + +function readInt8(data, start) { + return (data[start] << 24) >> 24; +} + +function readUint16(data, offset) { + return (data[offset] << 8) | data[offset + 1]; +} + +function readUint32(data, offset) { + return ((data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]) >>> 0; +} + +// Lazy test the endianness of the platform +// NOTE: This will be 'true' for simulated TypedArrays +function isLittleEndian() { + var buffer8 = new Uint8Array(2); + buffer8[0] = 1; + var buffer16 = new Uint16Array(buffer8.buffer); + return (buffer16[0] === 1); +} + +Object.defineProperty(PDFJS, 'isLittleEndian', { + configurable: true, + get: function PDFJS_isLittleEndian() { + return shadow(PDFJS, 'isLittleEndian', isLittleEndian()); + } +}); + + // Lazy test if the userAgant support CanvasTypedArrays +function hasCanvasTypedArrays() { + var canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + var ctx = canvas.getContext('2d'); + var imageData = ctx.createImageData(1, 1); + return (typeof imageData.data.buffer !== 'undefined'); +} + +Object.defineProperty(PDFJS, 'hasCanvasTypedArrays', { + configurable: true, + get: function PDFJS_hasCanvasTypedArrays() { + return shadow(PDFJS, 'hasCanvasTypedArrays', hasCanvasTypedArrays()); + } +}); + +var Uint32ArrayView = (function Uint32ArrayViewClosure() { + + function Uint32ArrayView(buffer, length) { + this.buffer = buffer; + this.byteLength = buffer.length; + this.length = length === undefined ? (this.byteLength >> 2) : length; + ensureUint32ArrayViewProps(this.length); + } + Uint32ArrayView.prototype = Object.create(null); + + var uint32ArrayViewSetters = 0; + function createUint32ArrayProp(index) { + return { + get: function () { + var buffer = this.buffer, offset = index << 2; + return (buffer[offset] | (buffer[offset + 1] << 8) | + (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)) >>> 0; + }, + set: function (value) { + var buffer = this.buffer, offset = index << 2; + buffer[offset] = value & 255; + buffer[offset + 1] = (value >> 8) & 255; + buffer[offset + 2] = (value >> 16) & 255; + buffer[offset + 3] = (value >>> 24) & 255; + } + }; + } + + function ensureUint32ArrayViewProps(length) { + while (uint32ArrayViewSetters < length) { + Object.defineProperty(Uint32ArrayView.prototype, + uint32ArrayViewSetters, + createUint32ArrayProp(uint32ArrayViewSetters)); + uint32ArrayViewSetters++; + } + } + + return Uint32ArrayView; +})(); + +var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; + +var Util = PDFJS.Util = (function UtilClosure() { + function Util() {} + + var rgbBuf = ['rgb(', 0, ',', 0, ',', 0, ')']; + + // makeCssRgb() can be called thousands of times. Using |rgbBuf| avoids + // creating many intermediate strings. + Util.makeCssRgb = function Util_makeCssRgb(r, g, b) { + rgbBuf[1] = r; + rgbBuf[3] = g; + rgbBuf[5] = b; + return rgbBuf.join(''); + }; + + // Concatenates two transformation matrices together and returns the result. + Util.transform = function Util_transform(m1, m2) { + return [ + m1[0] * m2[0] + m1[2] * m2[1], + m1[1] * m2[0] + m1[3] * m2[1], + m1[0] * m2[2] + m1[2] * m2[3], + m1[1] * m2[2] + m1[3] * m2[3], + m1[0] * m2[4] + m1[2] * m2[5] + m1[4], + m1[1] * m2[4] + m1[3] * m2[5] + m1[5] + ]; + }; + + // For 2d affine transforms + Util.applyTransform = function Util_applyTransform(p, m) { + var xt = p[0] * m[0] + p[1] * m[2] + m[4]; + var yt = p[0] * m[1] + p[1] * m[3] + m[5]; + return [xt, yt]; + }; + + Util.applyInverseTransform = function Util_applyInverseTransform(p, m) { + var d = m[0] * m[3] - m[1] * m[2]; + var xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d; + var yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d; + return [xt, yt]; + }; + + // Applies the transform to the rectangle and finds the minimum axially + // aligned bounding box. + Util.getAxialAlignedBoundingBox = + function Util_getAxialAlignedBoundingBox(r, m) { + + var p1 = Util.applyTransform(r, m); + var p2 = Util.applyTransform(r.slice(2, 4), m); + var p3 = Util.applyTransform([r[0], r[3]], m); + var p4 = Util.applyTransform([r[2], r[1]], m); + return [ + Math.min(p1[0], p2[0], p3[0], p4[0]), + Math.min(p1[1], p2[1], p3[1], p4[1]), + Math.max(p1[0], p2[0], p3[0], p4[0]), + Math.max(p1[1], p2[1], p3[1], p4[1]) + ]; + }; + + Util.inverseTransform = function Util_inverseTransform(m) { + var d = m[0] * m[3] - m[1] * m[2]; + return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d, + (m[2] * m[5] - m[4] * m[3]) / d, (m[4] * m[1] - m[5] * m[0]) / d]; + }; + + // Apply a generic 3d matrix M on a 3-vector v: + // | a b c | | X | + // | d e f | x | Y | + // | g h i | | Z | + // M is assumed to be serialized as [a,b,c,d,e,f,g,h,i], + // with v as [X,Y,Z] + Util.apply3dTransform = function Util_apply3dTransform(m, v) { + return [ + m[0] * v[0] + m[1] * v[1] + m[2] * v[2], + m[3] * v[0] + m[4] * v[1] + m[5] * v[2], + m[6] * v[0] + m[7] * v[1] + m[8] * v[2] + ]; + }; + + // This calculation uses Singular Value Decomposition. + // The SVD can be represented with formula A = USV. We are interested in the + // matrix S here because it represents the scale values. + Util.singularValueDecompose2dScale = + function Util_singularValueDecompose2dScale(m) { + + var transpose = [m[0], m[2], m[1], m[3]]; + + // Multiply matrix m with its transpose. + var a = m[0] * transpose[0] + m[1] * transpose[2]; + var b = m[0] * transpose[1] + m[1] * transpose[3]; + var c = m[2] * transpose[0] + m[3] * transpose[2]; + var d = m[2] * transpose[1] + m[3] * transpose[3]; + + // Solve the second degree polynomial to get roots. + var first = (a + d) / 2; + var second = Math.sqrt((a + d) * (a + d) - 4 * (a * d - c * b)) / 2; + var sx = first + second || 1; + var sy = first - second || 1; + + // Scale values are the square roots of the eigenvalues. + return [Math.sqrt(sx), Math.sqrt(sy)]; + }; + + // Normalize rectangle rect=[x1, y1, x2, y2] so that (x1,y1) < (x2,y2) + // For coordinate systems whose origin lies in the bottom-left, this + // means normalization to (BL,TR) ordering. For systems with origin in the + // top-left, this means (TL,BR) ordering. + Util.normalizeRect = function Util_normalizeRect(rect) { + var r = rect.slice(0); // clone rect + if (rect[0] > rect[2]) { + r[0] = rect[2]; + r[2] = rect[0]; + } + if (rect[1] > rect[3]) { + r[1] = rect[3]; + r[3] = rect[1]; + } + return r; + }; + + // Returns a rectangle [x1, y1, x2, y2] corresponding to the + // intersection of rect1 and rect2. If no intersection, returns 'false' + // The rectangle coordinates of rect1, rect2 should be [x1, y1, x2, y2] + Util.intersect = function Util_intersect(rect1, rect2) { + function compare(a, b) { + return a - b; + } + + // Order points along the axes + var orderedX = [rect1[0], rect1[2], rect2[0], rect2[2]].sort(compare), + orderedY = [rect1[1], rect1[3], rect2[1], rect2[3]].sort(compare), + result = []; + + rect1 = Util.normalizeRect(rect1); + rect2 = Util.normalizeRect(rect2); + + // X: first and second points belong to different rectangles? + if ((orderedX[0] === rect1[0] && orderedX[1] === rect2[0]) || + (orderedX[0] === rect2[0] && orderedX[1] === rect1[0])) { + // Intersection must be between second and third points + result[0] = orderedX[1]; + result[2] = orderedX[2]; + } else { + return false; + } + + // Y: first and second points belong to different rectangles? + if ((orderedY[0] === rect1[1] && orderedY[1] === rect2[1]) || + (orderedY[0] === rect2[1] && orderedY[1] === rect1[1])) { + // Intersection must be between second and third points + result[1] = orderedY[1]; + result[3] = orderedY[2]; + } else { + return false; + } + + return result; + }; + + Util.sign = function Util_sign(num) { + return num < 0 ? -1 : 1; + }; + + Util.appendToArray = function Util_appendToArray(arr1, arr2) { + Array.prototype.push.apply(arr1, arr2); + }; + + Util.prependToArray = function Util_prependToArray(arr1, arr2) { + Array.prototype.unshift.apply(arr1, arr2); + }; + + Util.extendObj = function extendObj(obj1, obj2) { + for (var key in obj2) { + obj1[key] = obj2[key]; + } + }; + + Util.getInheritableProperty = function Util_getInheritableProperty(dict, + name) { + while (dict && !dict.has(name)) { + dict = dict.get('Parent'); + } + if (!dict) { + return null; + } + return dict.get(name); + }; + + Util.inherit = function Util_inherit(sub, base, prototype) { + sub.prototype = Object.create(base.prototype); + sub.prototype.constructor = sub; + for (var prop in prototype) { + sub.prototype[prop] = prototype[prop]; + } + }; + + Util.loadScript = function Util_loadScript(src, callback) { + var script = document.createElement('script'); + var loaded = false; + script.setAttribute('src', src); + if (callback) { + script.onload = function() { + if (!loaded) { + callback(); + } + loaded = true; + }; + } + document.getElementsByTagName('head')[0].appendChild(script); + }; + + return Util; +})(); + +/** + * PDF page viewport created based on scale, rotation and offset. + * @class + * @alias PDFJS.PageViewport + */ +var PageViewport = PDFJS.PageViewport = (function PageViewportClosure() { + /** + * @constructor + * @private + * @param viewBox {Array} xMin, yMin, xMax and yMax coordinates. + * @param scale {number} scale of the viewport. + * @param rotation {number} rotations of the viewport in degrees. + * @param offsetX {number} offset X + * @param offsetY {number} offset Y + * @param dontFlip {boolean} if true, axis Y will not be flipped. + */ + function PageViewport(viewBox, scale, rotation, offsetX, offsetY, dontFlip) { + this.viewBox = viewBox; + this.scale = scale; + this.rotation = rotation; + this.offsetX = offsetX; + this.offsetY = offsetY; + + // creating transform to convert pdf coordinate system to the normal + // canvas like coordinates taking in account scale and rotation + var centerX = (viewBox[2] + viewBox[0]) / 2; + var centerY = (viewBox[3] + viewBox[1]) / 2; + var rotateA, rotateB, rotateC, rotateD; + rotation = rotation % 360; + rotation = rotation < 0 ? rotation + 360 : rotation; + switch (rotation) { + case 180: + rotateA = -1; rotateB = 0; rotateC = 0; rotateD = 1; + break; + case 90: + rotateA = 0; rotateB = 1; rotateC = 1; rotateD = 0; + break; + case 270: + rotateA = 0; rotateB = -1; rotateC = -1; rotateD = 0; + break; + //case 0: + default: + rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1; + break; + } + + if (dontFlip) { + rotateC = -rotateC; rotateD = -rotateD; + } + + var offsetCanvasX, offsetCanvasY; + var width, height; + if (rotateA === 0) { + offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX; + offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY; + width = Math.abs(viewBox[3] - viewBox[1]) * scale; + height = Math.abs(viewBox[2] - viewBox[0]) * scale; + } else { + offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; + offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; + width = Math.abs(viewBox[2] - viewBox[0]) * scale; + height = Math.abs(viewBox[3] - viewBox[1]) * scale; + } + // creating transform for the following operations: + // translate(-centerX, -centerY), rotate and flip vertically, + // scale, and translate(offsetCanvasX, offsetCanvasY) + this.transform = [ + rotateA * scale, + rotateB * scale, + rotateC * scale, + rotateD * scale, + offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, + offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY + ]; + + this.width = width; + this.height = height; + this.fontScale = scale; + } + PageViewport.prototype = /** @lends PDFJS.PageViewport.prototype */ { + /** + * Clones viewport with additional properties. + * @param args {Object} (optional) If specified, may contain the 'scale' or + * 'rotation' properties to override the corresponding properties in + * the cloned viewport. + * @returns {PDFJS.PageViewport} Cloned viewport. + */ + clone: function PageViewPort_clone(args) { + args = args || {}; + var scale = 'scale' in args ? args.scale : this.scale; + var rotation = 'rotation' in args ? args.rotation : this.rotation; + return new PageViewport(this.viewBox.slice(), scale, rotation, + this.offsetX, this.offsetY, args.dontFlip); + }, + /** + * Converts PDF point to the viewport coordinates. For examples, useful for + * converting PDF location into canvas pixel coordinates. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the viewport coordinate space. + * @see {@link convertToPdfPoint} + * @see {@link convertToViewportRectangle} + */ + convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) { + return Util.applyTransform([x, y], this.transform); + }, + /** + * Converts PDF rectangle to the viewport coordinates. + * @param rect {Array} xMin, yMin, xMax and yMax coordinates. + * @returns {Array} Contains corresponding coordinates of the rectangle + * in the viewport coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToViewportRectangle: + function PageViewport_convertToViewportRectangle(rect) { + var tl = Util.applyTransform([rect[0], rect[1]], this.transform); + var br = Util.applyTransform([rect[2], rect[3]], this.transform); + return [tl[0], tl[1], br[0], br[1]]; + }, + /** + * Converts viewport coordinates to the PDF location. For examples, useful + * for converting canvas pixel location into PDF one. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the PDF coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) { + return Util.applyInverseTransform([x, y], this.transform); + } + }; + return PageViewport; +})(); + +var PDFStringTranslateTable = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, + 0x2013, 0x192, 0x2044, 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C, + 0x201D, 0x2018, 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x141, 0x152, 0x160, + 0x178, 0x17D, 0x131, 0x142, 0x153, 0x161, 0x17E, 0, 0x20AC +]; + +function stringToPDFString(str) { + var i, n = str.length, strBuf = []; + if (str[0] === '\xFE' && str[1] === '\xFF') { + // UTF16BE BOM + for (i = 2; i < n; i += 2) { + strBuf.push(String.fromCharCode( + (str.charCodeAt(i) << 8) | str.charCodeAt(i + 1))); + } + } else { + for (i = 0; i < n; ++i) { + var code = PDFStringTranslateTable[str.charCodeAt(i)]; + strBuf.push(code ? String.fromCharCode(code) : str.charAt(i)); + } + } + return strBuf.join(''); +} + +function stringToUTF8String(str) { + return decodeURIComponent(escape(str)); +} + +function utf8StringToString(str) { + return unescape(encodeURIComponent(str)); +} + +function isEmptyObj(obj) { + for (var key in obj) { + return false; + } + return true; +} + +function isBool(v) { + return typeof v === 'boolean'; +} + +function isInt(v) { + return typeof v === 'number' && ((v | 0) === v); +} + +function isNum(v) { + return typeof v === 'number'; +} + +function isString(v) { + return typeof v === 'string'; +} + +function isName(v) { + return v instanceof Name; +} + +function isCmd(v, cmd) { + return v instanceof Cmd && (cmd === undefined || v.cmd === cmd); +} + +function isDict(v, type) { + if (!(v instanceof Dict)) { + return false; + } + if (!type) { + return true; + } + var dictType = v.get('Type'); + return isName(dictType) && dictType.name === type; +} + +function isArray(v) { + return v instanceof Array; +} + +function isStream(v) { + return typeof v === 'object' && v !== null && v.getBytes !== undefined; +} + +function isArrayBuffer(v) { + return typeof v === 'object' && v !== null && v.byteLength !== undefined; +} + +function isRef(v) { + return v instanceof Ref; +} + +/** + * Promise Capability object. + * + * @typedef {Object} PromiseCapability + * @property {Promise} promise - A promise object. + * @property {function} resolve - Fullfills the promise. + * @property {function} reject - Rejects the promise. + */ + +/** + * Creates a promise capability object. + * @alias PDFJS.createPromiseCapability + * + * @return {PromiseCapability} A capability object contains: + * - a Promise, resolve and reject methods. + */ +function createPromiseCapability() { + var capability = {}; + capability.promise = new Promise(function (resolve, reject) { + capability.resolve = resolve; + capability.reject = reject; + }); + return capability; +} + +PDFJS.createPromiseCapability = createPromiseCapability; + +/** + * Polyfill for Promises: + * The following promise implementation tries to generally implement the + * Promise/A+ spec. Some notable differences from other promise libaries are: + * - There currently isn't a seperate deferred and promise object. + * - Unhandled rejections eventually show an error if they aren't handled. + * + * Based off of the work in: + * https://bugzilla.mozilla.org/show_bug.cgi?id=810490 + */ +(function PromiseClosure() { + if (globalScope.Promise) { + // Promises existing in the DOM/Worker, checking presence of all/resolve + if (typeof globalScope.Promise.all !== 'function') { + globalScope.Promise.all = function (iterable) { + var count = 0, results = [], resolve, reject; + var promise = new globalScope.Promise(function (resolve_, reject_) { + resolve = resolve_; + reject = reject_; + }); + iterable.forEach(function (p, i) { + count++; + p.then(function (result) { + results[i] = result; + count--; + if (count === 0) { + resolve(results); + } + }, reject); + }); + if (count === 0) { + resolve(results); + } + return promise; + }; + } + if (typeof globalScope.Promise.resolve !== 'function') { + globalScope.Promise.resolve = function (value) { + return new globalScope.Promise(function (resolve) { resolve(value); }); + }; + } + if (typeof globalScope.Promise.reject !== 'function') { + globalScope.Promise.reject = function (reason) { + return new globalScope.Promise(function (resolve, reject) { + reject(reason); + }); + }; + } + if (typeof globalScope.Promise.prototype.catch !== 'function') { + globalScope.Promise.prototype.catch = function (onReject) { + return globalScope.Promise.prototype.then(undefined, onReject); + }; + } + return; + } + var STATUS_PENDING = 0; + var STATUS_RESOLVED = 1; + var STATUS_REJECTED = 2; + + // In an attempt to avoid silent exceptions, unhandled rejections are + // tracked and if they aren't handled in a certain amount of time an + // error is logged. + var REJECTION_TIMEOUT = 500; + + var HandlerManager = { + handlers: [], + running: false, + unhandledRejections: [], + pendingRejectionCheck: false, + + scheduleHandlers: function scheduleHandlers(promise) { + if (promise._status === STATUS_PENDING) { + return; + } + + this.handlers = this.handlers.concat(promise._handlers); + promise._handlers = []; + + if (this.running) { + return; + } + this.running = true; + + setTimeout(this.runHandlers.bind(this), 0); + }, + + runHandlers: function runHandlers() { + var RUN_TIMEOUT = 1; // ms + var timeoutAt = Date.now() + RUN_TIMEOUT; + while (this.handlers.length > 0) { + var handler = this.handlers.shift(); + + var nextStatus = handler.thisPromise._status; + var nextValue = handler.thisPromise._value; + + try { + if (nextStatus === STATUS_RESOLVED) { + if (typeof handler.onResolve === 'function') { + nextValue = handler.onResolve(nextValue); + } + } else if (typeof handler.onReject === 'function') { + nextValue = handler.onReject(nextValue); + nextStatus = STATUS_RESOLVED; + + if (handler.thisPromise._unhandledRejection) { + this.removeUnhandeledRejection(handler.thisPromise); + } + } + } catch (ex) { + nextStatus = STATUS_REJECTED; + nextValue = ex; + } + + handler.nextPromise._updateStatus(nextStatus, nextValue); + if (Date.now() >= timeoutAt) { + break; + } + } + + if (this.handlers.length > 0) { + setTimeout(this.runHandlers.bind(this), 0); + return; + } + + this.running = false; + }, + + addUnhandledRejection: function addUnhandledRejection(promise) { + this.unhandledRejections.push({ + promise: promise, + time: Date.now() + }); + this.scheduleRejectionCheck(); + }, + + removeUnhandeledRejection: function removeUnhandeledRejection(promise) { + promise._unhandledRejection = false; + for (var i = 0; i < this.unhandledRejections.length; i++) { + if (this.unhandledRejections[i].promise === promise) { + this.unhandledRejections.splice(i); + i--; + } + } + }, + + scheduleRejectionCheck: function scheduleRejectionCheck() { + if (this.pendingRejectionCheck) { + return; + } + this.pendingRejectionCheck = true; + setTimeout(function rejectionCheck() { + this.pendingRejectionCheck = false; + var now = Date.now(); + for (var i = 0; i < this.unhandledRejections.length; i++) { + if (now - this.unhandledRejections[i].time > REJECTION_TIMEOUT) { + var unhandled = this.unhandledRejections[i].promise._value; + var msg = 'Unhandled rejection: ' + unhandled; + if (unhandled.stack) { + msg += '\n' + unhandled.stack; + } + warn(msg); + this.unhandledRejections.splice(i); + i--; + } + } + if (this.unhandledRejections.length) { + this.scheduleRejectionCheck(); + } + }.bind(this), REJECTION_TIMEOUT); + } + }; + + function Promise(resolver) { + this._status = STATUS_PENDING; + this._handlers = []; + try { + resolver.call(this, this._resolve.bind(this), this._reject.bind(this)); + } catch (e) { + this._reject(e); + } + } + /** + * Builds a promise that is resolved when all the passed in promises are + * resolved. + * @param {array} array of data and/or promises to wait for. + * @return {Promise} New dependant promise. + */ + Promise.all = function Promise_all(promises) { + var resolveAll, rejectAll; + var deferred = new Promise(function (resolve, reject) { + resolveAll = resolve; + rejectAll = reject; + }); + var unresolved = promises.length; + var results = []; + if (unresolved === 0) { + resolveAll(results); + return deferred; + } + function reject(reason) { + if (deferred._status === STATUS_REJECTED) { + return; + } + results = []; + rejectAll(reason); + } + for (var i = 0, ii = promises.length; i < ii; ++i) { + var promise = promises[i]; + var resolve = (function(i) { + return function(value) { + if (deferred._status === STATUS_REJECTED) { + return; + } + results[i] = value; + unresolved--; + if (unresolved === 0) { + resolveAll(results); + } + }; + })(i); + if (Promise.isPromise(promise)) { + promise.then(resolve, reject); + } else { + resolve(promise); + } + } + return deferred; + }; + + /** + * Checks if the value is likely a promise (has a 'then' function). + * @return {boolean} true if value is thenable + */ + Promise.isPromise = function Promise_isPromise(value) { + return value && typeof value.then === 'function'; + }; + + /** + * Creates resolved promise + * @param value resolve value + * @returns {Promise} + */ + Promise.resolve = function Promise_resolve(value) { + return new Promise(function (resolve) { resolve(value); }); + }; + + /** + * Creates rejected promise + * @param reason rejection value + * @returns {Promise} + */ + Promise.reject = function Promise_reject(reason) { + return new Promise(function (resolve, reject) { reject(reason); }); + }; + + Promise.prototype = { + _status: null, + _value: null, + _handlers: null, + _unhandledRejection: null, + + _updateStatus: function Promise__updateStatus(status, value) { + if (this._status === STATUS_RESOLVED || + this._status === STATUS_REJECTED) { + return; + } + + if (status === STATUS_RESOLVED && + Promise.isPromise(value)) { + value.then(this._updateStatus.bind(this, STATUS_RESOLVED), + this._updateStatus.bind(this, STATUS_REJECTED)); + return; + } + + this._status = status; + this._value = value; + + if (status === STATUS_REJECTED && this._handlers.length === 0) { + this._unhandledRejection = true; + HandlerManager.addUnhandledRejection(this); + } + + HandlerManager.scheduleHandlers(this); + }, + + _resolve: function Promise_resolve(value) { + this._updateStatus(STATUS_RESOLVED, value); + }, + + _reject: function Promise_reject(reason) { + this._updateStatus(STATUS_REJECTED, reason); + }, + + then: function Promise_then(onResolve, onReject) { + var nextPromise = new Promise(function (resolve, reject) { + this.resolve = resolve; + this.reject = reject; + }); + this._handlers.push({ + thisPromise: this, + onResolve: onResolve, + onReject: onReject, + nextPromise: nextPromise + }); + HandlerManager.scheduleHandlers(this); + return nextPromise; + }, + + catch: function Promise_catch(onReject) { + return this.then(undefined, onReject); + } + }; + + globalScope.Promise = Promise; +})(); + +var StatTimer = (function StatTimerClosure() { + function rpad(str, pad, length) { + while (str.length < length) { + str += pad; + } + return str; + } + function StatTimer() { + this.started = {}; + this.times = []; + this.enabled = true; + } + StatTimer.prototype = { + time: function StatTimer_time(name) { + if (!this.enabled) { + return; + } + if (name in this.started) { + warn('Timer is already running for ' + name); + } + this.started[name] = Date.now(); + }, + timeEnd: function StatTimer_timeEnd(name) { + if (!this.enabled) { + return; + } + if (!(name in this.started)) { + warn('Timer has not been started for ' + name); + } + this.times.push({ + 'name': name, + 'start': this.started[name], + 'end': Date.now() + }); + // Remove timer from started so it can be called again. + delete this.started[name]; + }, + toString: function StatTimer_toString() { + var i, ii; + var times = this.times; + var out = ''; + // Find the longest name for padding purposes. + var longest = 0; + for (i = 0, ii = times.length; i < ii; ++i) { + var name = times[i]['name']; + if (name.length > longest) { + longest = name.length; + } + } + for (i = 0, ii = times.length; i < ii; ++i) { + var span = times[i]; + var duration = span.end - span.start; + out += rpad(span['name'], ' ', longest) + ' ' + duration + 'ms\n'; + } + return out; + } + }; + return StatTimer; +})(); + +PDFJS.createBlob = function createBlob(data, contentType) { + if (typeof Blob !== 'undefined') { + return new Blob([data], { type: contentType }); + } + // Blob builder is deprecated in FF14 and removed in FF18. + var bb = new MozBlobBuilder(); + bb.append(data); + return bb.getBlob(contentType); +}; + +PDFJS.createObjectURL = (function createObjectURLClosure() { + // Blob/createObjectURL is not available, falling back to data schema. + var digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + return function createObjectURL(data, contentType) { + if (!PDFJS.disableCreateObjectURL && + typeof URL !== 'undefined' && URL.createObjectURL) { + var blob = PDFJS.createBlob(data, contentType); + return URL.createObjectURL(blob); + } + + var buffer = 'data:' + contentType + ';base64,'; + for (var i = 0, ii = data.length; i < ii; i += 3) { + var b1 = data[i] & 0xFF; + var b2 = data[i + 1] & 0xFF; + var b3 = data[i + 2] & 0xFF; + var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); + var d3 = i + 1 < ii ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; + var d4 = i + 2 < ii ? (b3 & 0x3F) : 64; + buffer += digits[d1] + digits[d2] + digits[d3] + digits[d4]; + } + return buffer; + }; +})(); + +function MessageHandler(name, comObj) { + this.name = name; + this.comObj = comObj; + this.callbackIndex = 1; + this.postMessageTransfers = true; + var callbacksCapabilities = this.callbacksCapabilities = {}; + var ah = this.actionHandler = {}; + + ah['console_log'] = [function ahConsoleLog(data) { + console.log.apply(console, data); + }]; + ah['console_error'] = [function ahConsoleError(data) { + console.error.apply(console, data); + }]; + ah['_unsupported_feature'] = [function ah_unsupportedFeature(data) { + UnsupportedManager.notify(data); + }]; + + comObj.onmessage = function messageHandlerComObjOnMessage(event) { + var data = event.data; + if (data.isReply) { + var callbackId = data.callbackId; + if (data.callbackId in callbacksCapabilities) { + var callback = callbacksCapabilities[callbackId]; + delete callbacksCapabilities[callbackId]; + if ('error' in data) { + callback.reject(data.error); + } else { + callback.resolve(data.data); + } + } else { + error('Cannot resolve callback ' + callbackId); + } + } else if (data.action in ah) { + var action = ah[data.action]; + if (data.callbackId) { + Promise.resolve().then(function () { + return action[0].call(action[1], data.data); + }).then(function (result) { + comObj.postMessage({ + isReply: true, + callbackId: data.callbackId, + data: result + }); + }, function (reason) { + comObj.postMessage({ + isReply: true, + callbackId: data.callbackId, + error: reason + }); + }); + } else { + action[0].call(action[1], data.data); + } + } else { + error('Unknown action from worker: ' + data.action); + } + }; +} + +MessageHandler.prototype = { + on: function messageHandlerOn(actionName, handler, scope) { + var ah = this.actionHandler; + if (ah[actionName]) { + error('There is already an actionName called "' + actionName + '"'); + } + ah[actionName] = [handler, scope]; + }, + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * @param {String} actionName Action to call. + * @param {JSON} data JSON data to send. + * @param {Array} [transfers] Optional list of transfers/ArrayBuffers + */ + send: function messageHandlerSend(actionName, data, transfers) { + var message = { + action: actionName, + data: data + }; + this.postMessage(message, transfers); + }, + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * Expects that other side will callback with the response. + * @param {String} actionName Action to call. + * @param {JSON} data JSON data to send. + * @param {Array} [transfers] Optional list of transfers/ArrayBuffers. + * @returns {Promise} Promise to be resolved with response data. + */ + sendWithPromise: + function messageHandlerSendWithPromise(actionName, data, transfers) { + var callbackId = this.callbackIndex++; + var message = { + action: actionName, + data: data, + callbackId: callbackId + }; + var capability = createPromiseCapability(); + this.callbacksCapabilities[callbackId] = capability; + try { + this.postMessage(message, transfers); + } catch (e) { + capability.reject(e); + } + return capability.promise; + }, + /** + * Sends raw message to the comObj. + * @private + * @param message {Object} Raw message. + * @param transfers List of transfers/ArrayBuffers, or undefined. + */ + postMessage: function (message, transfers) { + if (transfers && this.postMessageTransfers) { + this.comObj.postMessage(message, transfers); + } else { + this.comObj.postMessage(message); + } + } +}; + +function loadJpegStream(id, imageUrl, objs) { + var img = new Image(); + img.onload = (function loadJpegStream_onloadClosure() { + objs.resolve(id, img); + }); + img.onerror = (function loadJpegStream_onerrorClosure() { + objs.resolve(id, null); + warn('Error during JPEG image loading'); + }); + img.src = imageUrl; +} + + + + +var NetworkManager = (function NetworkManagerClosure() { + + var OK_RESPONSE = 200; + var PARTIAL_CONTENT_RESPONSE = 206; + + function NetworkManager(url, args) { + this.url = url; + args = args || {}; + this.isHttp = /^https?:/i.test(url); + this.httpHeaders = (this.isHttp && args.httpHeaders) || {}; + this.withCredentials = args.withCredentials || false; + this.getXhr = args.getXhr || + function NetworkManager_getXhr() { + return new XMLHttpRequest(); + }; + + this.currXhrId = 0; + this.pendingRequests = {}; + this.loadedRequests = {}; + } + + function getArrayBuffer(xhr) { + var data = xhr.response; + if (typeof data !== 'string') { + return data; + } + var length = data.length; + var array = new Uint8Array(length); + for (var i = 0; i < length; i++) { + array[i] = data.charCodeAt(i) & 0xFF; + } + return array.buffer; + } + + NetworkManager.prototype = { + requestRange: function NetworkManager_requestRange(begin, end, listeners) { + var args = { + begin: begin, + end: end + }; + for (var prop in listeners) { + args[prop] = listeners[prop]; + } + return this.request(args); + }, + + requestFull: function NetworkManager_requestFull(listeners) { + return this.request(listeners); + }, + + request: function NetworkManager_request(args) { + var xhr = this.getXhr(); + var xhrId = this.currXhrId++; + var pendingRequest = this.pendingRequests[xhrId] = { + xhr: xhr + }; + + xhr.open('GET', this.url); + xhr.withCredentials = this.withCredentials; + for (var property in this.httpHeaders) { + var value = this.httpHeaders[property]; + if (typeof value === 'undefined') { + continue; + } + xhr.setRequestHeader(property, value); + } + if (this.isHttp && 'begin' in args && 'end' in args) { + var rangeStr = args.begin + '-' + (args.end - 1); + xhr.setRequestHeader('Range', 'bytes=' + rangeStr); + pendingRequest.expectedStatus = 206; + } else { + pendingRequest.expectedStatus = 200; + } + + if (args.onProgressiveData) { + // Some legacy browsers might throw an exception. + try { + xhr.responseType = 'moz-chunked-arraybuffer'; + } catch(e) {} + if (xhr.responseType === 'moz-chunked-arraybuffer') { + pendingRequest.onProgressiveData = args.onProgressiveData; + pendingRequest.mozChunked = true; + } else { + xhr.responseType = 'arraybuffer'; + } + } else { + xhr.responseType = 'arraybuffer'; + } + + if (args.onError) { + xhr.onerror = function(evt) { + args.onError(xhr.status); + }; + } + xhr.onreadystatechange = this.onStateChange.bind(this, xhrId); + xhr.onprogress = this.onProgress.bind(this, xhrId); + + pendingRequest.onHeadersReceived = args.onHeadersReceived; + pendingRequest.onDone = args.onDone; + pendingRequest.onError = args.onError; + pendingRequest.onProgress = args.onProgress; + + xhr.send(null); + + return xhrId; + }, + + onProgress: function NetworkManager_onProgress(xhrId, evt) { + var pendingRequest = this.pendingRequests[xhrId]; + if (!pendingRequest) { + // Maybe abortRequest was called... + return; + } + + if (pendingRequest.mozChunked) { + var chunk = getArrayBuffer(pendingRequest.xhr); + pendingRequest.onProgressiveData(chunk); + } + + var onProgress = pendingRequest.onProgress; + if (onProgress) { + onProgress(evt); + } + }, + + onStateChange: function NetworkManager_onStateChange(xhrId, evt) { + var pendingRequest = this.pendingRequests[xhrId]; + if (!pendingRequest) { + // Maybe abortRequest was called... + return; + } + + var xhr = pendingRequest.xhr; + if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) { + pendingRequest.onHeadersReceived(); + delete pendingRequest.onHeadersReceived; + } + + if (xhr.readyState !== 4) { + return; + } + + if (!(xhrId in this.pendingRequests)) { + // The XHR request might have been aborted in onHeadersReceived() + // callback, in which case we should abort request + return; + } + + delete this.pendingRequests[xhrId]; + + // success status == 0 can be on ftp, file and other protocols + if (xhr.status === 0 && this.isHttp) { + if (pendingRequest.onError) { + pendingRequest.onError(xhr.status); + } + return; + } + var xhrStatus = xhr.status || OK_RESPONSE; + + // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2: + // "A server MAY ignore the Range header". This means it's possible to + // get a 200 rather than a 206 response from a range request. + var ok_response_on_range_request = + xhrStatus === OK_RESPONSE && + pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE; + + if (!ok_response_on_range_request && + xhrStatus !== pendingRequest.expectedStatus) { + if (pendingRequest.onError) { + pendingRequest.onError(xhr.status); + } + return; + } + + this.loadedRequests[xhrId] = true; + + var chunk = getArrayBuffer(xhr); + if (xhrStatus === PARTIAL_CONTENT_RESPONSE) { + var rangeHeader = xhr.getResponseHeader('Content-Range'); + var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader); + var begin = parseInt(matches[1], 10); + pendingRequest.onDone({ + begin: begin, + chunk: chunk + }); + } else if (pendingRequest.onProgressiveData) { + pendingRequest.onDone(null); + } else { + pendingRequest.onDone({ + begin: 0, + chunk: chunk + }); + } + }, + + hasPendingRequests: function NetworkManager_hasPendingRequests() { + for (var xhrId in this.pendingRequests) { + return true; + } + return false; + }, + + getRequestXhr: function NetworkManager_getXhr(xhrId) { + return this.pendingRequests[xhrId].xhr; + }, + + isStreamingRequest: function NetworkManager_isStreamingRequest(xhrId) { + return !!(this.pendingRequests[xhrId].onProgressiveData); + }, + + isPendingRequest: function NetworkManager_isPendingRequest(xhrId) { + return xhrId in this.pendingRequests; + }, + + isLoadedRequest: function NetworkManager_isLoadedRequest(xhrId) { + return xhrId in this.loadedRequests; + }, + + abortAllRequests: function NetworkManager_abortAllRequests() { + for (var xhrId in this.pendingRequests) { + this.abortRequest(xhrId | 0); + } + }, + + abortRequest: function NetworkManager_abortRequest(xhrId) { + var xhr = this.pendingRequests[xhrId].xhr; + delete this.pendingRequests[xhrId]; + xhr.abort(); + } + }; + + return NetworkManager; +})(); + + +var ChunkedStream = (function ChunkedStreamClosure() { + function ChunkedStream(length, chunkSize, manager) { + this.bytes = new Uint8Array(length); + this.start = 0; + this.pos = 0; + this.end = length; + this.chunkSize = chunkSize; + this.loadedChunks = []; + this.numChunksLoaded = 0; + this.numChunks = Math.ceil(length / chunkSize); + this.manager = manager; + this.progressiveDataLength = 0; + this.lastSuccessfulEnsureByteChunk = -1; // a single-entry cache + } + + // required methods for a stream. if a particular stream does not + // implement these, an error should be thrown + ChunkedStream.prototype = { + + getMissingChunks: function ChunkedStream_getMissingChunks() { + var chunks = []; + for (var chunk = 0, n = this.numChunks; chunk < n; ++chunk) { + if (!this.loadedChunks[chunk]) { + chunks.push(chunk); + } + } + return chunks; + }, + + getBaseStreams: function ChunkedStream_getBaseStreams() { + return [this]; + }, + + allChunksLoaded: function ChunkedStream_allChunksLoaded() { + return this.numChunksLoaded === this.numChunks; + }, + + onReceiveData: function ChunkedStream_onReceiveData(begin, chunk) { + var end = begin + chunk.byteLength; + + assert(begin % this.chunkSize === 0, 'Bad begin offset: ' + begin); + // Using this.length is inaccurate here since this.start can be moved + // See ChunkedStream.moveStart() + var length = this.bytes.length; + assert(end % this.chunkSize === 0 || end === length, + 'Bad end offset: ' + end); + + this.bytes.set(new Uint8Array(chunk), begin); + var chunkSize = this.chunkSize; + var beginChunk = Math.floor(begin / chunkSize); + var endChunk = Math.floor((end - 1) / chunkSize) + 1; + var curChunk; + + for (curChunk = beginChunk; curChunk < endChunk; ++curChunk) { + if (!this.loadedChunks[curChunk]) { + this.loadedChunks[curChunk] = true; + ++this.numChunksLoaded; + } + } + }, + + onReceiveProgressiveData: + function ChunkedStream_onReceiveProgressiveData(data) { + var position = this.progressiveDataLength; + var beginChunk = Math.floor(position / this.chunkSize); + + this.bytes.set(new Uint8Array(data), position); + position += data.byteLength; + this.progressiveDataLength = position; + var endChunk = position >= this.end ? this.numChunks : + Math.floor(position / this.chunkSize); + var curChunk; + for (curChunk = beginChunk; curChunk < endChunk; ++curChunk) { + if (!this.loadedChunks[curChunk]) { + this.loadedChunks[curChunk] = true; + ++this.numChunksLoaded; + } + } + }, + + ensureByte: function ChunkedStream_ensureByte(pos) { + var chunk = Math.floor(pos / this.chunkSize); + if (chunk === this.lastSuccessfulEnsureByteChunk) { + return; + } + + if (!this.loadedChunks[chunk]) { + throw new MissingDataException(pos, pos + 1); + } + this.lastSuccessfulEnsureByteChunk = chunk; + }, + + ensureRange: function ChunkedStream_ensureRange(begin, end) { + if (begin >= end) { + return; + } + + if (end <= this.progressiveDataLength) { + return; + } + + var chunkSize = this.chunkSize; + var beginChunk = Math.floor(begin / chunkSize); + var endChunk = Math.floor((end - 1) / chunkSize) + 1; + for (var chunk = beginChunk; chunk < endChunk; ++chunk) { + if (!this.loadedChunks[chunk]) { + throw new MissingDataException(begin, end); + } + } + }, + + nextEmptyChunk: function ChunkedStream_nextEmptyChunk(beginChunk) { + var chunk, n; + for (chunk = beginChunk, n = this.numChunks; chunk < n; ++chunk) { + if (!this.loadedChunks[chunk]) { + return chunk; + } + } + // Wrap around to beginning + for (chunk = 0; chunk < beginChunk; ++chunk) { + if (!this.loadedChunks[chunk]) { + return chunk; + } + } + return null; + }, + + hasChunk: function ChunkedStream_hasChunk(chunk) { + return !!this.loadedChunks[chunk]; + }, + + get length() { + return this.end - this.start; + }, + + get isEmpty() { + return this.length === 0; + }, + + getByte: function ChunkedStream_getByte() { + var pos = this.pos; + if (pos >= this.end) { + return -1; + } + this.ensureByte(pos); + return this.bytes[this.pos++]; + }, + + getUint16: function ChunkedStream_getUint16() { + var b0 = this.getByte(); + var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } + return (b0 << 8) + b1; + }, + + getInt32: function ChunkedStream_getInt32() { + var b0 = this.getByte(); + var b1 = this.getByte(); + var b2 = this.getByte(); + var b3 = this.getByte(); + return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3; + }, + + // returns subarray of original buffer + // should only be read + getBytes: function ChunkedStream_getBytes(length) { + var bytes = this.bytes; + var pos = this.pos; + var strEnd = this.end; + + if (!length) { + this.ensureRange(pos, strEnd); + return bytes.subarray(pos, strEnd); + } + + var end = pos + length; + if (end > strEnd) { + end = strEnd; + } + this.ensureRange(pos, end); + + this.pos = end; + return bytes.subarray(pos, end); + }, + + peekByte: function ChunkedStream_peekByte() { + var peekedByte = this.getByte(); + this.pos--; + return peekedByte; + }, + + peekBytes: function ChunkedStream_peekBytes(length) { + var bytes = this.getBytes(length); + this.pos -= bytes.length; + return bytes; + }, + + getByteRange: function ChunkedStream_getBytes(begin, end) { + this.ensureRange(begin, end); + return this.bytes.subarray(begin, end); + }, + + skip: function ChunkedStream_skip(n) { + if (!n) { + n = 1; + } + this.pos += n; + }, + + reset: function ChunkedStream_reset() { + this.pos = this.start; + }, + + moveStart: function ChunkedStream_moveStart() { + this.start = this.pos; + }, + + makeSubStream: function ChunkedStream_makeSubStream(start, length, dict) { + this.ensureRange(start, start + length); + + function ChunkedStreamSubstream() {} + ChunkedStreamSubstream.prototype = Object.create(this); + ChunkedStreamSubstream.prototype.getMissingChunks = function() { + var chunkSize = this.chunkSize; + var beginChunk = Math.floor(this.start / chunkSize); + var endChunk = Math.floor((this.end - 1) / chunkSize) + 1; + var missingChunks = []; + for (var chunk = beginChunk; chunk < endChunk; ++chunk) { + if (!this.loadedChunks[chunk]) { + missingChunks.push(chunk); + } + } + return missingChunks; + }; + var subStream = new ChunkedStreamSubstream(); + subStream.pos = subStream.start = start; + subStream.end = start + length || this.end; + subStream.dict = dict; + return subStream; + }, + + isStream: true + }; + + return ChunkedStream; +})(); + +var ChunkedStreamManager = (function ChunkedStreamManagerClosure() { + + function ChunkedStreamManager(length, chunkSize, url, args) { + this.stream = new ChunkedStream(length, chunkSize, this); + this.length = length; + this.chunkSize = chunkSize; + this.url = url; + this.disableAutoFetch = args.disableAutoFetch; + var msgHandler = this.msgHandler = args.msgHandler; + + if (args.chunkedViewerLoading) { + msgHandler.on('OnDataRange', this.onReceiveData.bind(this)); + msgHandler.on('OnDataProgress', this.onProgress.bind(this)); + this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) { + msgHandler.send('RequestDataRange', { begin: begin, end: end }); + }; + } else { + + var getXhr = function getXhr() { + return new XMLHttpRequest(); + }; + this.networkManager = new NetworkManager(this.url, { + getXhr: getXhr, + httpHeaders: args.httpHeaders, + withCredentials: args.withCredentials + }); + this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) { + this.networkManager.requestRange(begin, end, { + onDone: this.onReceiveData.bind(this), + onProgress: this.onProgress.bind(this) + }); + }; + } + + this.currRequestId = 0; + + this.chunksNeededByRequest = {}; + this.requestsByChunk = {}; + this.callbacksByRequest = {}; + this.progressiveDataLength = 0; + + this._loadedStreamCapability = createPromiseCapability(); + + if (args.initialData) { + this.onReceiveData({chunk: args.initialData}); + } + } + + ChunkedStreamManager.prototype = { + onLoadedStream: function ChunkedStreamManager_getLoadedStream() { + return this._loadedStreamCapability.promise; + }, + + // Get all the chunks that are not yet loaded and groups them into + // contiguous ranges to load in as few requests as possible + requestAllChunks: function ChunkedStreamManager_requestAllChunks() { + var missingChunks = this.stream.getMissingChunks(); + this.requestChunks(missingChunks); + return this._loadedStreamCapability.promise; + }, + + requestChunks: function ChunkedStreamManager_requestChunks(chunks, + callback) { + var requestId = this.currRequestId++; + + var chunksNeeded; + var i, ii; + this.chunksNeededByRequest[requestId] = chunksNeeded = {}; + for (i = 0, ii = chunks.length; i < ii; i++) { + if (!this.stream.hasChunk(chunks[i])) { + chunksNeeded[chunks[i]] = true; + } + } + + if (isEmptyObj(chunksNeeded)) { + if (callback) { + callback(); + } + return; + } + + this.callbacksByRequest[requestId] = callback; + + var chunksToRequest = []; + for (var chunk in chunksNeeded) { + chunk = chunk | 0; + if (!(chunk in this.requestsByChunk)) { + this.requestsByChunk[chunk] = []; + chunksToRequest.push(chunk); + } + this.requestsByChunk[chunk].push(requestId); + } + + if (!chunksToRequest.length) { + return; + } + + var groupedChunksToRequest = this.groupChunks(chunksToRequest); + + for (i = 0; i < groupedChunksToRequest.length; ++i) { + var groupedChunk = groupedChunksToRequest[i]; + var begin = groupedChunk.beginChunk * this.chunkSize; + var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length); + this.sendRequest(begin, end); + } + }, + + getStream: function ChunkedStreamManager_getStream() { + return this.stream; + }, + + // Loads any chunks in the requested range that are not yet loaded + requestRange: function ChunkedStreamManager_requestRange( + begin, end, callback) { + + end = Math.min(end, this.length); + + var beginChunk = this.getBeginChunk(begin); + var endChunk = this.getEndChunk(end); + + var chunks = []; + for (var chunk = beginChunk; chunk < endChunk; ++chunk) { + chunks.push(chunk); + } + + this.requestChunks(chunks, callback); + }, + + requestRanges: function ChunkedStreamManager_requestRanges(ranges, + callback) { + ranges = ranges || []; + var chunksToRequest = []; + + for (var i = 0; i < ranges.length; i++) { + var beginChunk = this.getBeginChunk(ranges[i].begin); + var endChunk = this.getEndChunk(ranges[i].end); + for (var chunk = beginChunk; chunk < endChunk; ++chunk) { + if (chunksToRequest.indexOf(chunk) < 0) { + chunksToRequest.push(chunk); + } + } + } + + chunksToRequest.sort(function(a, b) { return a - b; }); + this.requestChunks(chunksToRequest, callback); + }, + + // Groups a sorted array of chunks into as few continguous larger + // chunks as possible + groupChunks: function ChunkedStreamManager_groupChunks(chunks) { + var groupedChunks = []; + var beginChunk = -1; + var prevChunk = -1; + for (var i = 0; i < chunks.length; ++i) { + var chunk = chunks[i]; + + if (beginChunk < 0) { + beginChunk = chunk; + } + + if (prevChunk >= 0 && prevChunk + 1 !== chunk) { + groupedChunks.push({ beginChunk: beginChunk, + endChunk: prevChunk + 1 }); + beginChunk = chunk; + } + if (i + 1 === chunks.length) { + groupedChunks.push({ beginChunk: beginChunk, + endChunk: chunk + 1 }); + } + + prevChunk = chunk; + } + return groupedChunks; + }, + + onProgress: function ChunkedStreamManager_onProgress(args) { + var bytesLoaded = (this.stream.numChunksLoaded * this.chunkSize + + args.loaded); + this.msgHandler.send('DocProgress', { + loaded: bytesLoaded, + total: this.length + }); + }, + + onReceiveData: function ChunkedStreamManager_onReceiveData(args) { + var chunk = args.chunk; + var isProgressive = args.begin === undefined; + var begin = isProgressive ? this.progressiveDataLength : args.begin; + var end = begin + chunk.byteLength; + + var beginChunk = Math.floor(begin / this.chunkSize); + var endChunk = end < this.length ? Math.floor(end / this.chunkSize) : + Math.ceil(end / this.chunkSize); + + if (isProgressive) { + this.stream.onReceiveProgressiveData(chunk); + this.progressiveDataLength = end; + } else { + this.stream.onReceiveData(begin, chunk); + } + + if (this.stream.allChunksLoaded()) { + this._loadedStreamCapability.resolve(this.stream); + } + + var loadedRequests = []; + var i, requestId; + for (chunk = beginChunk; chunk < endChunk; ++chunk) { + // The server might return more chunks than requested + var requestIds = this.requestsByChunk[chunk] || []; + delete this.requestsByChunk[chunk]; + + for (i = 0; i < requestIds.length; ++i) { + requestId = requestIds[i]; + var chunksNeeded = this.chunksNeededByRequest[requestId]; + if (chunk in chunksNeeded) { + delete chunksNeeded[chunk]; + } + + if (!isEmptyObj(chunksNeeded)) { + continue; + } + + loadedRequests.push(requestId); + } + } + + // If there are no pending requests, automatically fetch the next + // unfetched chunk of the PDF + if (!this.disableAutoFetch && isEmptyObj(this.requestsByChunk)) { + var nextEmptyChunk; + if (this.stream.numChunksLoaded === 1) { + // This is a special optimization so that after fetching the first + // chunk, rather than fetching the second chunk, we fetch the last + // chunk. + var lastChunk = this.stream.numChunks - 1; + if (!this.stream.hasChunk(lastChunk)) { + nextEmptyChunk = lastChunk; + } + } else { + nextEmptyChunk = this.stream.nextEmptyChunk(endChunk); + } + if (isInt(nextEmptyChunk)) { + this.requestChunks([nextEmptyChunk]); + } + } + + for (i = 0; i < loadedRequests.length; ++i) { + requestId = loadedRequests[i]; + var callback = this.callbacksByRequest[requestId]; + delete this.callbacksByRequest[requestId]; + if (callback) { + callback(); + } + } + + this.msgHandler.send('DocProgress', { + loaded: this.stream.numChunksLoaded * this.chunkSize, + total: this.length + }); + }, + + onError: function ChunkedStreamManager_onError(err) { + this._loadedStreamCapability.reject(err); + }, + + getBeginChunk: function ChunkedStreamManager_getBeginChunk(begin) { + var chunk = Math.floor(begin / this.chunkSize); + return chunk; + }, + + getEndChunk: function ChunkedStreamManager_getEndChunk(end) { + if (end % this.chunkSize === 0) { + return end / this.chunkSize; + } + + // 0 -> 0 + // 1 -> 1 + // 99 -> 1 + // 100 -> 1 + // 101 -> 2 + var chunk = Math.floor((end - 1) / this.chunkSize) + 1; + return chunk; + } + }; + + return ChunkedStreamManager; +})(); + + +// The maximum number of bytes fetched per range request +var RANGE_CHUNK_SIZE = 65536; + +// TODO(mack): Make use of PDFJS.Util.inherit() when it becomes available +var BasePdfManager = (function BasePdfManagerClosure() { + function BasePdfManager() { + throw new Error('Cannot initialize BaseManagerManager'); + } + + BasePdfManager.prototype = { + onLoadedStream: function BasePdfManager_onLoadedStream() { + throw new NotImplementedException(); + }, + + ensureDoc: function BasePdfManager_ensureDoc(prop, args) { + return this.ensure(this.pdfDocument, prop, args); + }, + + ensureXRef: function BasePdfManager_ensureXRef(prop, args) { + return this.ensure(this.pdfDocument.xref, prop, args); + }, + + ensureCatalog: function BasePdfManager_ensureCatalog(prop, args) { + return this.ensure(this.pdfDocument.catalog, prop, args); + }, + + getPage: function BasePdfManager_pagePage(pageIndex) { + return this.pdfDocument.getPage(pageIndex); + }, + + cleanup: function BasePdfManager_cleanup() { + return this.pdfDocument.cleanup(); + }, + + ensure: function BasePdfManager_ensure(obj, prop, args) { + return new NotImplementedException(); + }, + + requestRange: function BasePdfManager_ensure(begin, end) { + return new NotImplementedException(); + }, + + requestLoadedStream: function BasePdfManager_requestLoadedStream() { + return new NotImplementedException(); + }, + + sendProgressiveData: function BasePdfManager_sendProgressiveData(chunk) { + return new NotImplementedException(); + }, + + updatePassword: function BasePdfManager_updatePassword(password) { + this.pdfDocument.xref.password = this.password = password; + if (this._passwordChangedCapability) { + this._passwordChangedCapability.resolve(); + } + }, + + passwordChanged: function BasePdfManager_passwordChanged() { + this._passwordChangedCapability = createPromiseCapability(); + return this._passwordChangedCapability.promise; + }, + + terminate: function BasePdfManager_terminate() { + return new NotImplementedException(); + } + }; + + return BasePdfManager; +})(); + +var LocalPdfManager = (function LocalPdfManagerClosure() { + function LocalPdfManager(data, password) { + var stream = new Stream(data); + this.pdfDocument = new PDFDocument(this, stream, password); + this._loadedStreamCapability = createPromiseCapability(); + this._loadedStreamCapability.resolve(stream); + } + + LocalPdfManager.prototype = Object.create(BasePdfManager.prototype); + LocalPdfManager.prototype.constructor = LocalPdfManager; + + LocalPdfManager.prototype.ensure = + function LocalPdfManager_ensure(obj, prop, args) { + return new Promise(function (resolve, reject) { + try { + var value = obj[prop]; + var result; + if (typeof value === 'function') { + result = value.apply(obj, args); + } else { + result = value; + } + resolve(result); + } catch (e) { + reject(e); + } + }); + }; + + LocalPdfManager.prototype.requestRange = + function LocalPdfManager_requestRange(begin, end) { + return Promise.resolve(); + }; + + LocalPdfManager.prototype.requestLoadedStream = + function LocalPdfManager_requestLoadedStream() { + }; + + LocalPdfManager.prototype.onLoadedStream = + function LocalPdfManager_getLoadedStream() { + return this._loadedStreamCapability.promise; + }; + + LocalPdfManager.prototype.terminate = + function LocalPdfManager_terminate() { + return; + }; + + return LocalPdfManager; +})(); + +var NetworkPdfManager = (function NetworkPdfManagerClosure() { + function NetworkPdfManager(args, msgHandler) { + + this.msgHandler = msgHandler; + + var params = { + msgHandler: msgHandler, + httpHeaders: args.httpHeaders, + withCredentials: args.withCredentials, + chunkedViewerLoading: args.chunkedViewerLoading, + disableAutoFetch: args.disableAutoFetch, + initialData: args.initialData + }; + this.streamManager = new ChunkedStreamManager(args.length, RANGE_CHUNK_SIZE, + args.url, params); + + this.pdfDocument = new PDFDocument(this, this.streamManager.getStream(), + args.password); + } + + NetworkPdfManager.prototype = Object.create(BasePdfManager.prototype); + NetworkPdfManager.prototype.constructor = NetworkPdfManager; + + NetworkPdfManager.prototype.ensure = + function NetworkPdfManager_ensure(obj, prop, args) { + var pdfManager = this; + + return new Promise(function (resolve, reject) { + function ensureHelper() { + try { + var result; + var value = obj[prop]; + if (typeof value === 'function') { + result = value.apply(obj, args); + } else { + result = value; + } + resolve(result); + } catch(e) { + if (!(e instanceof MissingDataException)) { + reject(e); + return; + } + pdfManager.streamManager.requestRange(e.begin, e.end, ensureHelper); + } + } + + ensureHelper(); + }); + }; + + NetworkPdfManager.prototype.requestRange = + function NetworkPdfManager_requestRange(begin, end) { + return new Promise(function (resolve) { + this.streamManager.requestRange(begin, end, function() { + resolve(); + }); + }.bind(this)); + }; + + NetworkPdfManager.prototype.requestLoadedStream = + function NetworkPdfManager_requestLoadedStream() { + this.streamManager.requestAllChunks(); + }; + + NetworkPdfManager.prototype.sendProgressiveData = + function NetworkPdfManager_sendProgressiveData(chunk) { + this.streamManager.onReceiveData({ chunk: chunk }); + }; + + NetworkPdfManager.prototype.onLoadedStream = + function NetworkPdfManager_getLoadedStream() { + return this.streamManager.onLoadedStream(); + }; + + NetworkPdfManager.prototype.terminate = + function NetworkPdfManager_terminate() { + this.streamManager.networkManager.abortAllRequests(); + }; + + return NetworkPdfManager; +})(); + + +var Page = (function PageClosure() { + + var LETTER_SIZE_MEDIABOX = [0, 0, 612, 792]; + + function Page(pdfManager, xref, pageIndex, pageDict, ref, fontCache) { + this.pdfManager = pdfManager; + this.pageIndex = pageIndex; + this.pageDict = pageDict; + this.xref = xref; + this.ref = ref; + this.fontCache = fontCache; + this.idCounters = { + obj: 0 + }; + this.resourcesPromise = null; + } + + Page.prototype = { + getPageProp: function Page_getPageProp(key) { + return this.pageDict.get(key); + }, + + getInheritedPageProp: function Page_inheritPageProp(key) { + var dict = this.pageDict; + var value = dict.get(key); + while (value === undefined) { + dict = dict.get('Parent'); + if (!dict) { + break; + } + value = dict.get(key); + } + return value; + }, + + get content() { + return this.getPageProp('Contents'); + }, + + get resources() { + var value = this.getInheritedPageProp('Resources'); + // For robustness: The spec states that a \Resources entry has to be + // present, but can be empty. Some document omit it still. In this case + // return an empty dictionary: + if (value === undefined) { + value = Dict.empty; + } + return shadow(this, 'resources', value); + }, + + get mediaBox() { + var obj = this.getInheritedPageProp('MediaBox'); + // Reset invalid media box to letter size. + if (!isArray(obj) || obj.length !== 4) { + obj = LETTER_SIZE_MEDIABOX; + } + return shadow(this, 'mediaBox', obj); + }, + + get view() { + var mediaBox = this.mediaBox; + var cropBox = this.getInheritedPageProp('CropBox'); + if (!isArray(cropBox) || cropBox.length !== 4) { + return shadow(this, 'view', mediaBox); + } + + // From the spec, 6th ed., p.963: + // "The crop, bleed, trim, and art boxes should not ordinarily + // extend beyond the boundaries of the media box. If they do, they are + // effectively reduced to their intersection with the media box." + cropBox = Util.intersect(cropBox, mediaBox); + if (!cropBox) { + return shadow(this, 'view', mediaBox); + } + return shadow(this, 'view', cropBox); + }, + + get annotationRefs() { + return shadow(this, 'annotationRefs', + this.getInheritedPageProp('Annots')); + }, + + get rotate() { + var rotate = this.getInheritedPageProp('Rotate') || 0; + // Normalize rotation so it's a multiple of 90 and between 0 and 270 + if (rotate % 90 !== 0) { + rotate = 0; + } else if (rotate >= 360) { + rotate = rotate % 360; + } else if (rotate < 0) { + // The spec doesn't cover negatives, assume its counterclockwise + // rotation. The following is the other implementation of modulo. + rotate = ((rotate % 360) + 360) % 360; + } + return shadow(this, 'rotate', rotate); + }, + + getContentStream: function Page_getContentStream() { + var content = this.content; + var stream; + if (isArray(content)) { + // fetching items + var xref = this.xref; + var i, n = content.length; + var streams = []; + for (i = 0; i < n; ++i) { + streams.push(xref.fetchIfRef(content[i])); + } + stream = new StreamsSequenceStream(streams); + } else if (isStream(content)) { + stream = content; + } else { + // replacing non-existent page content with empty one + stream = new NullStream(); + } + return stream; + }, + + loadResources: function Page_loadResources(keys) { + if (!this.resourcesPromise) { + // TODO: add async getInheritedPageProp and remove this. + this.resourcesPromise = this.pdfManager.ensure(this, 'resources'); + } + return this.resourcesPromise.then(function resourceSuccess() { + var objectLoader = new ObjectLoader(this.resources.map, + keys, + this.xref); + return objectLoader.load(); + }.bind(this)); + }, + + getOperatorList: function Page_getOperatorList(handler, intent) { + var self = this; + + var pdfManager = this.pdfManager; + var contentStreamPromise = pdfManager.ensure(this, 'getContentStream', + []); + var resourcesPromise = this.loadResources([ + 'ExtGState', + 'ColorSpace', + 'Pattern', + 'Shading', + 'XObject', + 'Font' + // ProcSet + // Properties + ]); + + var partialEvaluator = new PartialEvaluator(pdfManager, this.xref, + handler, this.pageIndex, + 'p' + this.pageIndex + '_', + this.idCounters, + this.fontCache); + + var dataPromises = Promise.all([contentStreamPromise, resourcesPromise]); + var pageListPromise = dataPromises.then(function(data) { + var contentStream = data[0]; + var opList = new OperatorList(intent, handler, self.pageIndex); + + handler.send('StartRenderPage', { + transparency: partialEvaluator.hasBlendModes(self.resources), + pageIndex: self.pageIndex, + intent: intent + }); + return partialEvaluator.getOperatorList(contentStream, self.resources, + opList).then(function () { + return opList; + }); + }); + + var annotationsPromise = pdfManager.ensure(this, 'annotations'); + return Promise.all([pageListPromise, annotationsPromise]).then( + function(datas) { + var pageOpList = datas[0]; + var annotations = datas[1]; + + if (annotations.length === 0) { + pageOpList.flush(true); + return pageOpList; + } + + var annotationsReadyPromise = Annotation.appendToOperatorList( + annotations, pageOpList, pdfManager, partialEvaluator, intent); + return annotationsReadyPromise.then(function () { + pageOpList.flush(true); + return pageOpList; + }); + }); + }, + + extractTextContent: function Page_extractTextContent() { + var handler = { + on: function nullHandlerOn() {}, + send: function nullHandlerSend() {} + }; + + var self = this; + + var pdfManager = this.pdfManager; + var contentStreamPromise = pdfManager.ensure(this, 'getContentStream', + []); + + var resourcesPromise = this.loadResources([ + 'ExtGState', + 'XObject', + 'Font' + ]); + + var dataPromises = Promise.all([contentStreamPromise, + resourcesPromise]); + return dataPromises.then(function(data) { + var contentStream = data[0]; + var partialEvaluator = new PartialEvaluator(pdfManager, self.xref, + handler, self.pageIndex, + 'p' + self.pageIndex + '_', + self.idCounters, + self.fontCache); + + return partialEvaluator.getTextContent(contentStream, + self.resources); + }); + }, + + getAnnotationsData: function Page_getAnnotationsData() { + var annotations = this.annotations; + var annotationsData = []; + for (var i = 0, n = annotations.length; i < n; ++i) { + annotationsData.push(annotations[i].getData()); + } + return annotationsData; + }, + + get annotations() { + var annotations = []; + var annotationRefs = (this.annotationRefs || []); + for (var i = 0, n = annotationRefs.length; i < n; ++i) { + var annotationRef = annotationRefs[i]; + var annotation = Annotation.fromRef(this.xref, annotationRef); + if (annotation) { + annotations.push(annotation); + } + } + return shadow(this, 'annotations', annotations); + } + }; + + return Page; +})(); + +/** + * The `PDFDocument` holds all the data of the PDF file. Compared to the + * `PDFDoc`, this one doesn't have any job management code. + * Right now there exists one PDFDocument on the main thread + one object + * for each worker. If there is no worker support enabled, there are two + * `PDFDocument` objects on the main thread created. + */ +var PDFDocument = (function PDFDocumentClosure() { + var FINGERPRINT_FIRST_BYTES = 1024; + var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' + + '\x00\x00\x00\x00\x00\x00\x00\x00\x00'; + + function PDFDocument(pdfManager, arg, password) { + if (isStream(arg)) { + init.call(this, pdfManager, arg, password); + } else if (isArrayBuffer(arg)) { + init.call(this, pdfManager, new Stream(arg), password); + } else { + error('PDFDocument: Unknown argument type'); + } + } + + function init(pdfManager, stream, password) { + assert(stream.length > 0, 'stream must have data'); + this.pdfManager = pdfManager; + this.stream = stream; + var xref = new XRef(this.stream, password, pdfManager); + this.xref = xref; + } + + function find(stream, needle, limit, backwards) { + var pos = stream.pos; + var end = stream.end; + var strBuf = []; + if (pos + limit > end) { + limit = end - pos; + } + for (var n = 0; n < limit; ++n) { + strBuf.push(String.fromCharCode(stream.getByte())); + } + var str = strBuf.join(''); + stream.pos = pos; + var index = backwards ? str.lastIndexOf(needle) : str.indexOf(needle); + if (index === -1) { + return false; /* not found */ + } + stream.pos += index; + return true; /* found */ + } + + var DocumentInfoValidators = { + get entries() { + // Lazily build this since all the validation functions below are not + // defined until after this file loads. + return shadow(this, 'entries', { + Title: isString, + Author: isString, + Subject: isString, + Keywords: isString, + Creator: isString, + Producer: isString, + CreationDate: isString, + ModDate: isString, + Trapped: isName + }); + } + }; + + PDFDocument.prototype = { + parse: function PDFDocument_parse(recoveryMode) { + this.setup(recoveryMode); + try { + // checking if AcroForm is present + this.acroForm = this.catalog.catDict.get('AcroForm'); + if (this.acroForm) { + this.xfa = this.acroForm.get('XFA'); + var fields = this.acroForm.get('Fields'); + if ((!fields || !isArray(fields) || fields.length === 0) && + !this.xfa) { + // no fields and no XFA -- not a form (?) + this.acroForm = null; + } + } + } catch (ex) { + info('Something wrong with AcroForm entry'); + this.acroForm = null; + } + }, + + get linearization() { + var linearization = null; + if (this.stream.length) { + try { + linearization = Linearization.create(this.stream); + } catch (err) { + if (err instanceof MissingDataException) { + throw err; + } + info(err); + } + } + // shadow the prototype getter with a data property + return shadow(this, 'linearization', linearization); + }, + get startXRef() { + var stream = this.stream; + var startXRef = 0; + var linearization = this.linearization; + if (linearization) { + // Find end of first obj. + stream.reset(); + if (find(stream, 'endobj', 1024)) { + startXRef = stream.pos + 6; + } + } else { + // Find startxref by jumping backward from the end of the file. + var step = 1024; + var found = false, pos = stream.end; + while (!found && pos > 0) { + pos -= step - 'startxref'.length; + if (pos < 0) { + pos = 0; + } + stream.pos = pos; + found = find(stream, 'startxref', step, true); + } + if (found) { + stream.skip(9); + var ch; + do { + ch = stream.getByte(); + } while (Lexer.isSpace(ch)); + var str = ''; + while (ch >= 0x20 && ch <= 0x39) { // < '9' + str += String.fromCharCode(ch); + ch = stream.getByte(); + } + startXRef = parseInt(str, 10); + if (isNaN(startXRef)) { + startXRef = 0; + } + } + } + // shadow the prototype getter with a data property + return shadow(this, 'startXRef', startXRef); + }, + get mainXRefEntriesOffset() { + var mainXRefEntriesOffset = 0; + var linearization = this.linearization; + if (linearization) { + mainXRefEntriesOffset = linearization.mainXRefEntriesOffset; + } + // shadow the prototype getter with a data property + return shadow(this, 'mainXRefEntriesOffset', mainXRefEntriesOffset); + }, + // Find the header, remove leading garbage and setup the stream + // starting from the header. + checkHeader: function PDFDocument_checkHeader() { + var stream = this.stream; + stream.reset(); + if (find(stream, '%PDF-', 1024)) { + // Found the header, trim off any garbage before it. + stream.moveStart(); + // Reading file format version + var MAX_VERSION_LENGTH = 12; + var version = '', ch; + while ((ch = stream.getByte()) > 0x20) { // SPACE + if (version.length >= MAX_VERSION_LENGTH) { + break; + } + version += String.fromCharCode(ch); + } + // removing "%PDF-"-prefix + this.pdfFormatVersion = version.substring(5); + return; + } + // May not be a PDF file, continue anyway. + }, + parseStartXRef: function PDFDocument_parseStartXRef() { + var startXRef = this.startXRef; + this.xref.setStartXRef(startXRef); + }, + setup: function PDFDocument_setup(recoveryMode) { + this.xref.parse(recoveryMode); + this.catalog = new Catalog(this.pdfManager, this.xref); + }, + get numPages() { + var linearization = this.linearization; + var num = linearization ? linearization.numPages : this.catalog.numPages; + // shadow the prototype getter + return shadow(this, 'numPages', num); + }, + get documentInfo() { + var docInfo = { + PDFFormatVersion: this.pdfFormatVersion, + IsAcroFormPresent: !!this.acroForm, + IsXFAPresent: !!this.xfa + }; + var infoDict; + try { + infoDict = this.xref.trailer.get('Info'); + } catch (err) { + info('The document information dictionary is invalid.'); + } + if (infoDict) { + var validEntries = DocumentInfoValidators.entries; + // Only fill the document info with valid entries from the spec. + for (var key in validEntries) { + if (infoDict.has(key)) { + var value = infoDict.get(key); + // Make sure the value conforms to the spec. + if (validEntries[key](value)) { + docInfo[key] = (typeof value !== 'string' ? + value : stringToPDFString(value)); + } else { + info('Bad value in document info for "' + key + '"'); + } + } + } + } + return shadow(this, 'documentInfo', docInfo); + }, + get fingerprint() { + var xref = this.xref, idArray, hash, fileID = ''; + + if (xref.trailer.has('ID')) { + idArray = xref.trailer.get('ID'); + } + if (idArray && isArray(idArray) && idArray[0] !== EMPTY_FINGERPRINT) { + hash = stringToBytes(idArray[0]); + } else { + if (this.stream.ensureRange) { + this.stream.ensureRange(0, + Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end)); + } + hash = calculateMD5(this.stream.bytes.subarray(0, + FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES); + } + + for (var i = 0, n = hash.length; i < n; i++) { + var hex = hash[i].toString(16); + fileID += hex.length === 1 ? '0' + hex : hex; + } + + return shadow(this, 'fingerprint', fileID); + }, + + getPage: function PDFDocument_getPage(pageIndex) { + return this.catalog.getPage(pageIndex); + }, + + cleanup: function PDFDocument_cleanup() { + return this.catalog.cleanup(); + } + }; + + return PDFDocument; +})(); + + +var Name = (function NameClosure() { + function Name(name) { + this.name = name; + } + + Name.prototype = {}; + + var nameCache = {}; + + Name.get = function Name_get(name) { + var nameValue = nameCache[name]; + return (nameValue ? nameValue : (nameCache[name] = new Name(name))); + }; + + return Name; +})(); + +var Cmd = (function CmdClosure() { + function Cmd(cmd) { + this.cmd = cmd; + } + + Cmd.prototype = {}; + + var cmdCache = {}; + + Cmd.get = function Cmd_get(cmd) { + var cmdValue = cmdCache[cmd]; + return (cmdValue ? cmdValue : (cmdCache[cmd] = new Cmd(cmd))); + }; + + return Cmd; +})(); + +var Dict = (function DictClosure() { + var nonSerializable = function nonSerializableClosure() { + return nonSerializable; // creating closure on some variable + }; + + var GETALL_DICTIONARY_TYPES_WHITELIST = { + 'Background': true, + 'ExtGState': true, + 'Halftone': true, + 'Layout': true, + 'Mask': true, + 'Pagination': true, + 'Printing': true + }; + + function isRecursionAllowedFor(dict) { + if (!isName(dict.Type)) { + return true; + } + var dictType = dict.Type.name; + return GETALL_DICTIONARY_TYPES_WHITELIST[dictType] === true; + } + + // xref is optional + function Dict(xref) { + // Map should only be used internally, use functions below to access. + this.map = Object.create(null); + this.xref = xref; + this.objId = null; + this.__nonSerializable__ = nonSerializable; // disable cloning of the Dict + } + + Dict.prototype = { + assignXref: function Dict_assignXref(newXref) { + this.xref = newXref; + }, + + // automatically dereferences Ref objects + get: function Dict_get(key1, key2, key3) { + var value; + var xref = this.xref; + if (typeof (value = this.map[key1]) !== 'undefined' || key1 in this.map || + typeof key2 === 'undefined') { + return xref ? xref.fetchIfRef(value) : value; + } + if (typeof (value = this.map[key2]) !== 'undefined' || key2 in this.map || + typeof key3 === 'undefined') { + return xref ? xref.fetchIfRef(value) : value; + } + value = this.map[key3] || null; + return xref ? xref.fetchIfRef(value) : value; + }, + + // Same as get(), but returns a promise and uses fetchIfRefAsync(). + getAsync: function Dict_getAsync(key1, key2, key3) { + var value; + var xref = this.xref; + if (typeof (value = this.map[key1]) !== 'undefined' || key1 in this.map || + typeof key2 === 'undefined') { + if (xref) { + return xref.fetchIfRefAsync(value); + } + return Promise.resolve(value); + } + if (typeof (value = this.map[key2]) !== 'undefined' || key2 in this.map || + typeof key3 === 'undefined') { + if (xref) { + return xref.fetchIfRefAsync(value); + } + return Promise.resolve(value); + } + value = this.map[key3] || null; + if (xref) { + return xref.fetchIfRefAsync(value); + } + return Promise.resolve(value); + }, + + // no dereferencing + getRaw: function Dict_getRaw(key) { + return this.map[key]; + }, + + // creates new map and dereferences all Refs + getAll: function Dict_getAll() { + var all = Object.create(null); + var queue = null; + var key, obj; + for (key in this.map) { + obj = this.get(key); + if (obj instanceof Dict) { + if (isRecursionAllowedFor(obj)) { + (queue || (queue = [])).push({target: all, key: key, obj: obj}); + } else { + all[key] = this.getRaw(key); + } + } else { + all[key] = obj; + } + } + if (!queue) { + return all; + } + + // trying to take cyclic references into the account + var processed = Object.create(null); + while (queue.length > 0) { + var item = queue.shift(); + var itemObj = item.obj; + var objId = itemObj.objId; + if (objId && objId in processed) { + item.target[item.key] = processed[objId]; + continue; + } + var dereferenced = Object.create(null); + for (key in itemObj.map) { + obj = itemObj.get(key); + if (obj instanceof Dict) { + if (isRecursionAllowedFor(obj)) { + queue.push({target: dereferenced, key: key, obj: obj}); + } else { + dereferenced[key] = itemObj.getRaw(key); + } + } else { + dereferenced[key] = obj; + } + } + if (objId) { + processed[objId] = dereferenced; + } + item.target[item.key] = dereferenced; + } + return all; + }, + + getKeys: function Dict_getKeys() { + return Object.keys(this.map); + }, + + set: function Dict_set(key, value) { + this.map[key] = value; + }, + + has: function Dict_has(key) { + return key in this.map; + }, + + forEach: function Dict_forEach(callback) { + for (var key in this.map) { + callback(key, this.get(key)); + } + } + }; + + Dict.empty = new Dict(null); + + return Dict; +})(); + +var Ref = (function RefClosure() { + function Ref(num, gen) { + this.num = num; + this.gen = gen; + } + + Ref.prototype = { + toString: function Ref_toString() { + // This function is hot, so we make the string as compact as possible. + // |this.gen| is almost always zero, so we treat that case specially. + var str = this.num + 'R'; + if (this.gen !== 0) { + str += this.gen; + } + return str; + } + }; + + return Ref; +})(); + +// The reference is identified by number and generation. +// This structure stores only one instance of the reference. +var RefSet = (function RefSetClosure() { + function RefSet() { + this.dict = {}; + } + + RefSet.prototype = { + has: function RefSet_has(ref) { + return ref.toString() in this.dict; + }, + + put: function RefSet_put(ref) { + this.dict[ref.toString()] = true; + }, + + remove: function RefSet_remove(ref) { + delete this.dict[ref.toString()]; + } + }; + + return RefSet; +})(); + +var RefSetCache = (function RefSetCacheClosure() { + function RefSetCache() { + this.dict = Object.create(null); + } + + RefSetCache.prototype = { + get: function RefSetCache_get(ref) { + return this.dict[ref.toString()]; + }, + + has: function RefSetCache_has(ref) { + return ref.toString() in this.dict; + }, + + put: function RefSetCache_put(ref, obj) { + this.dict[ref.toString()] = obj; + }, + + putAlias: function RefSetCache_putAlias(ref, aliasRef) { + this.dict[ref.toString()] = this.get(aliasRef); + }, + + forEach: function RefSetCache_forEach(fn, thisArg) { + for (var i in this.dict) { + fn.call(thisArg, this.dict[i]); + } + }, + + clear: function RefSetCache_clear() { + this.dict = Object.create(null); + } + }; + + return RefSetCache; +})(); + +var Catalog = (function CatalogClosure() { + function Catalog(pdfManager, xref) { + this.pdfManager = pdfManager; + this.xref = xref; + this.catDict = xref.getCatalogObj(); + this.fontCache = new RefSetCache(); + assert(isDict(this.catDict), + 'catalog object is not a dictionary'); + + this.pagePromises = []; + } + + Catalog.prototype = { + get metadata() { + var streamRef = this.catDict.getRaw('Metadata'); + if (!isRef(streamRef)) { + return shadow(this, 'metadata', null); + } + + var encryptMetadata = (!this.xref.encrypt ? false : + this.xref.encrypt.encryptMetadata); + + var stream = this.xref.fetch(streamRef, !encryptMetadata); + var metadata; + if (stream && isDict(stream.dict)) { + var type = stream.dict.get('Type'); + var subtype = stream.dict.get('Subtype'); + + if (isName(type) && isName(subtype) && + type.name === 'Metadata' && subtype.name === 'XML') { + // XXX: This should examine the charset the XML document defines, + // however since there are currently no real means to decode + // arbitrary charsets, let's just hope that the author of the PDF + // was reasonable enough to stick with the XML default charset, + // which is UTF-8. + try { + metadata = stringToUTF8String(bytesToString(stream.getBytes())); + } catch (e) { + info('Skipping invalid metadata.'); + } + } + } + + return shadow(this, 'metadata', metadata); + }, + get toplevelPagesDict() { + var pagesObj = this.catDict.get('Pages'); + assert(isDict(pagesObj), 'invalid top-level pages dictionary'); + // shadow the prototype getter + return shadow(this, 'toplevelPagesDict', pagesObj); + }, + get documentOutline() { + var obj = null; + try { + obj = this.readDocumentOutline(); + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + warn('Unable to read document outline'); + } + return shadow(this, 'documentOutline', obj); + }, + readDocumentOutline: function Catalog_readDocumentOutline() { + var xref = this.xref; + var obj = this.catDict.get('Outlines'); + var root = { items: [] }; + if (isDict(obj)) { + obj = obj.getRaw('First'); + var processed = new RefSet(); + if (isRef(obj)) { + var queue = [{obj: obj, parent: root}]; + // to avoid recursion keeping track of the items + // in the processed dictionary + processed.put(obj); + while (queue.length > 0) { + var i = queue.shift(); + var outlineDict = xref.fetchIfRef(i.obj); + if (outlineDict === null) { + continue; + } + if (!outlineDict.has('Title')) { + error('Invalid outline item'); + } + var dest = outlineDict.get('A'); + if (dest) { + dest = dest.get('D'); + } else if (outlineDict.has('Dest')) { + dest = outlineDict.getRaw('Dest'); + if (isName(dest)) { + dest = dest.name; + } + } + var title = outlineDict.get('Title'); + var outlineItem = { + dest: dest, + title: stringToPDFString(title), + color: outlineDict.get('C') || [0, 0, 0], + count: outlineDict.get('Count'), + bold: !!(outlineDict.get('F') & 2), + italic: !!(outlineDict.get('F') & 1), + items: [] + }; + i.parent.items.push(outlineItem); + obj = outlineDict.getRaw('First'); + if (isRef(obj) && !processed.has(obj)) { + queue.push({obj: obj, parent: outlineItem}); + processed.put(obj); + } + obj = outlineDict.getRaw('Next'); + if (isRef(obj) && !processed.has(obj)) { + queue.push({obj: obj, parent: i.parent}); + processed.put(obj); + } + } + } + } + return (root.items.length > 0 ? root.items : null); + }, + get numPages() { + var obj = this.toplevelPagesDict.get('Count'); + assert( + isInt(obj), + 'page count in top level pages object is not an integer' + ); + // shadow the prototype getter + return shadow(this, 'num', obj); + }, + get destinations() { + function fetchDestination(dest) { + return isDict(dest) ? dest.get('D') : dest; + } + + var xref = this.xref; + var dests = {}, nameTreeRef, nameDictionaryRef; + var obj = this.catDict.get('Names'); + if (obj && obj.has('Dests')) { + nameTreeRef = obj.getRaw('Dests'); + } else if (this.catDict.has('Dests')) { + nameDictionaryRef = this.catDict.get('Dests'); + } + + if (nameDictionaryRef) { + // reading simple destination dictionary + obj = nameDictionaryRef; + obj.forEach(function catalogForEach(key, value) { + if (!value) { + return; + } + dests[key] = fetchDestination(value); + }); + } + if (nameTreeRef) { + var nameTree = new NameTree(nameTreeRef, xref); + var names = nameTree.getAll(); + for (var name in names) { + if (!names.hasOwnProperty(name)) { + continue; + } + dests[name] = fetchDestination(names[name]); + } + } + return shadow(this, 'destinations', dests); + }, + getDestination: function Catalog_getDestination(destinationId) { + function fetchDestination(dest) { + return isDict(dest) ? dest.get('D') : dest; + } + + var xref = this.xref; + var dest, nameTreeRef, nameDictionaryRef; + var obj = this.catDict.get('Names'); + if (obj && obj.has('Dests')) { + nameTreeRef = obj.getRaw('Dests'); + } else if (this.catDict.has('Dests')) { + nameDictionaryRef = this.catDict.get('Dests'); + } + + if (nameDictionaryRef) { + // reading simple destination dictionary + obj = nameDictionaryRef; + obj.forEach(function catalogForEach(key, value) { + if (!value) { + return; + } + if (key === destinationId) { + dest = fetchDestination(value); + } + }); + } + if (nameTreeRef) { + var nameTree = new NameTree(nameTreeRef, xref); + dest = fetchDestination(nameTree.get(destinationId)); + } + return dest; + }, + get attachments() { + var xref = this.xref; + var attachments = null, nameTreeRef; + var obj = this.catDict.get('Names'); + if (obj) { + nameTreeRef = obj.getRaw('EmbeddedFiles'); + } + + if (nameTreeRef) { + var nameTree = new NameTree(nameTreeRef, xref); + var names = nameTree.getAll(); + for (var name in names) { + if (!names.hasOwnProperty(name)) { + continue; + } + var fs = new FileSpec(names[name], xref); + if (!attachments) { + attachments = {}; + } + attachments[stringToPDFString(name)] = fs.serializable; + } + } + return shadow(this, 'attachments', attachments); + }, + get javaScript() { + var xref = this.xref; + var obj = this.catDict.get('Names'); + + var javaScript = []; + if (obj && obj.has('JavaScript')) { + var nameTree = new NameTree(obj.getRaw('JavaScript'), xref); + var names = nameTree.getAll(); + for (var name in names) { + if (!names.hasOwnProperty(name)) { + continue; + } + // We don't really use the JavaScript right now. This code is + // defensive so we don't cause errors on document load. + var jsDict = names[name]; + if (!isDict(jsDict)) { + continue; + } + var type = jsDict.get('S'); + if (!isName(type) || type.name !== 'JavaScript') { + continue; + } + var js = jsDict.get('JS'); + if (!isString(js) && !isStream(js)) { + continue; + } + if (isStream(js)) { + js = bytesToString(js.getBytes()); + } + javaScript.push(stringToPDFString(js)); + } + } + + // Append OpenAction actions to javaScript array + var openactionDict = this.catDict.get('OpenAction'); + if (isDict(openactionDict)) { + var objType = openactionDict.get('Type'); + var actionType = openactionDict.get('S'); + var action = openactionDict.get('N'); + var isPrintAction = (isName(objType) && objType.name === 'Action' && + isName(actionType) && actionType.name === 'Named' && + isName(action) && action.name === 'Print'); + + if (isPrintAction) { + javaScript.push('print(true);'); + } + } + + return shadow(this, 'javaScript', javaScript); + }, + + cleanup: function Catalog_cleanup() { + var promises = []; + this.fontCache.forEach(function (promise) { + promises.push(promise); + }); + return Promise.all(promises).then(function (translatedFonts) { + for (var i = 0, ii = translatedFonts.length; i < ii; i++) { + var font = translatedFonts[i].dict; + delete font.translated; + } + this.fontCache.clear(); + }.bind(this)); + }, + + getPage: function Catalog_getPage(pageIndex) { + if (!(pageIndex in this.pagePromises)) { + this.pagePromises[pageIndex] = this.getPageDict(pageIndex).then( + function (a) { + var dict = a[0]; + var ref = a[1]; + return new Page(this.pdfManager, this.xref, pageIndex, dict, ref, + this.fontCache); + }.bind(this) + ); + } + return this.pagePromises[pageIndex]; + }, + + getPageDict: function Catalog_getPageDict(pageIndex) { + var capability = createPromiseCapability(); + var nodesToVisit = [this.catDict.getRaw('Pages')]; + var currentPageIndex = 0; + var xref = this.xref; + var checkAllKids = false; + + function next() { + while (nodesToVisit.length) { + var currentNode = nodesToVisit.pop(); + + if (isRef(currentNode)) { + xref.fetchAsync(currentNode).then(function (obj) { + if (isDict(obj, 'Page') || (isDict(obj) && !obj.has('Kids'))) { + if (pageIndex === currentPageIndex) { + capability.resolve([obj, currentNode]); + } else { + currentPageIndex++; + next(); + } + return; + } + nodesToVisit.push(obj); + next(); + }, capability.reject); + return; + } + + // Must be a child page dictionary. + assert( + isDict(currentNode), + 'page dictionary kid reference points to wrong type of object' + ); + var count = currentNode.get('Count'); + // If the current node doesn't have any children, avoid getting stuck + // in an empty node further down in the tree (see issue5644.pdf). + if (count === 0) { + checkAllKids = true; + } + // Skip nodes where the page can't be. + if (currentPageIndex + count <= pageIndex) { + currentPageIndex += count; + continue; + } + + var kids = currentNode.get('Kids'); + assert(isArray(kids), 'page dictionary kids object is not an array'); + if (!checkAllKids && count === kids.length) { + // Nodes that don't have the page have been skipped and this is the + // bottom of the tree which means the page requested must be a + // descendant of this pages node. Ideally we would just resolve the + // promise with the page ref here, but there is the case where more + // pages nodes could link to single a page (see issue 3666 pdf). To + // handle this push it back on the queue so if it is a pages node it + // will be descended into. + nodesToVisit = [kids[pageIndex - currentPageIndex]]; + currentPageIndex = pageIndex; + continue; + } else { + for (var last = kids.length - 1; last >= 0; last--) { + nodesToVisit.push(kids[last]); + } + } + } + capability.reject('Page index ' + pageIndex + ' not found.'); + } + next(); + return capability.promise; + }, + + getPageIndex: function Catalog_getPageIndex(ref) { + // The page tree nodes have the count of all the leaves below them. To get + // how many pages are before we just have to walk up the tree and keep + // adding the count of siblings to the left of the node. + var xref = this.xref; + function pagesBeforeRef(kidRef) { + var total = 0; + var parentRef; + return xref.fetchAsync(kidRef).then(function (node) { + if (!node) { + return null; + } + parentRef = node.getRaw('Parent'); + return node.getAsync('Parent'); + }).then(function (parent) { + if (!parent) { + return null; + } + return parent.getAsync('Kids'); + }).then(function (kids) { + if (!kids) { + return null; + } + var kidPromises = []; + var found = false; + for (var i = 0; i < kids.length; i++) { + var kid = kids[i]; + assert(isRef(kid), 'kids must be a ref'); + if (kid.num === kidRef.num) { + found = true; + break; + } + kidPromises.push(xref.fetchAsync(kid).then(function (kid) { + if (kid.has('Count')) { + var count = kid.get('Count'); + total += count; + } else { // page leaf node + total++; + } + })); + } + if (!found) { + error('kid ref not found in parents kids'); + } + return Promise.all(kidPromises).then(function () { + return [total, parentRef]; + }); + }); + } + + var total = 0; + function next(ref) { + return pagesBeforeRef(ref).then(function (args) { + if (!args) { + return total; + } + var count = args[0]; + var parentRef = args[1]; + total += count; + return next(parentRef); + }); + } + + return next(ref); + } + }; + + return Catalog; +})(); + +var XRef = (function XRefClosure() { + function XRef(stream, password) { + this.stream = stream; + this.entries = []; + this.xrefstms = {}; + // prepare the XRef cache + this.cache = []; + this.password = password; + this.stats = { + streamTypes: [], + fontTypes: [] + }; + } + + XRef.prototype = { + setStartXRef: function XRef_setStartXRef(startXRef) { + // Store the starting positions of xref tables as we process them + // so we can recover from missing data errors + this.startXRefQueue = [startXRef]; + }, + + parse: function XRef_parse(recoveryMode) { + var trailerDict; + if (!recoveryMode) { + trailerDict = this.readXRef(); + } else { + warn('Indexing all PDF objects'); + trailerDict = this.indexObjects(); + } + trailerDict.assignXref(this); + this.trailer = trailerDict; + var encrypt = trailerDict.get('Encrypt'); + if (encrypt) { + var ids = trailerDict.get('ID'); + var fileId = (ids && ids.length) ? ids[0] : ''; + this.encrypt = new CipherTransformFactory(encrypt, fileId, + this.password); + } + + // get the root dictionary (catalog) object + if (!(this.root = trailerDict.get('Root'))) { + error('Invalid root reference'); + } + }, + + processXRefTable: function XRef_processXRefTable(parser) { + if (!('tableState' in this)) { + // Stores state of the table as we process it so we can resume + // from middle of table in case of missing data error + this.tableState = { + entryNum: 0, + streamPos: parser.lexer.stream.pos, + parserBuf1: parser.buf1, + parserBuf2: parser.buf2 + }; + } + + var obj = this.readXRefTable(parser); + + // Sanity check + if (!isCmd(obj, 'trailer')) { + error('Invalid XRef table: could not find trailer dictionary'); + } + // Read trailer dictionary, e.g. + // trailer + // << /Size 22 + // /Root 20R + // /Info 10R + // /ID [ <81b14aafa313db63dbd6f981e49f94f4> ] + // >> + // The parser goes through the entire stream << ... >> and provides + // a getter interface for the key-value table + var dict = parser.getObj(); + + // The pdflib PDF generator can generate a nested trailer dictionary + if (!isDict(dict) && dict.dict) { + dict = dict.dict; + } + if (!isDict(dict)) { + error('Invalid XRef table: could not parse trailer dictionary'); + } + delete this.tableState; + + return dict; + }, + + readXRefTable: function XRef_readXRefTable(parser) { + // Example of cross-reference table: + // xref + // 0 1 <-- subsection header (first obj #, obj count) + // 0000000000 65535 f <-- actual object (offset, generation #, f/n) + // 23 2 <-- subsection header ... and so on ... + // 0000025518 00002 n + // 0000025635 00000 n + // trailer + // ... + + var stream = parser.lexer.stream; + var tableState = this.tableState; + stream.pos = tableState.streamPos; + parser.buf1 = tableState.parserBuf1; + parser.buf2 = tableState.parserBuf2; + + // Outer loop is over subsection headers + var obj; + + while (true) { + if (!('firstEntryNum' in tableState) || !('entryCount' in tableState)) { + if (isCmd(obj = parser.getObj(), 'trailer')) { + break; + } + tableState.firstEntryNum = obj; + tableState.entryCount = parser.getObj(); + } + + var first = tableState.firstEntryNum; + var count = tableState.entryCount; + if (!isInt(first) || !isInt(count)) { + error('Invalid XRef table: wrong types in subsection header'); + } + // Inner loop is over objects themselves + for (var i = tableState.entryNum; i < count; i++) { + tableState.streamPos = stream.pos; + tableState.entryNum = i; + tableState.parserBuf1 = parser.buf1; + tableState.parserBuf2 = parser.buf2; + + var entry = {}; + entry.offset = parser.getObj(); + entry.gen = parser.getObj(); + var type = parser.getObj(); + + if (isCmd(type, 'f')) { + entry.free = true; + } else if (isCmd(type, 'n')) { + entry.uncompressed = true; + } + + // Validate entry obj + if (!isInt(entry.offset) || !isInt(entry.gen) || + !(entry.free || entry.uncompressed)) { + error('Invalid entry in XRef subsection: ' + first + ', ' + count); + } + + if (!this.entries[i + first]) { + this.entries[i + first] = entry; + } + } + + tableState.entryNum = 0; + tableState.streamPos = stream.pos; + tableState.parserBuf1 = parser.buf1; + tableState.parserBuf2 = parser.buf2; + delete tableState.firstEntryNum; + delete tableState.entryCount; + } + + // Per issue 3248: hp scanners generate bad XRef + if (first === 1 && this.entries[1] && this.entries[1].free) { + // shifting the entries + this.entries.shift(); + } + + // Sanity check: as per spec, first object must be free + if (this.entries[0] && !this.entries[0].free) { + error('Invalid XRef table: unexpected first object'); + } + return obj; + }, + + processXRefStream: function XRef_processXRefStream(stream) { + if (!('streamState' in this)) { + // Stores state of the stream as we process it so we can resume + // from middle of stream in case of missing data error + var streamParameters = stream.dict; + var byteWidths = streamParameters.get('W'); + var range = streamParameters.get('Index'); + if (!range) { + range = [0, streamParameters.get('Size')]; + } + + this.streamState = { + entryRanges: range, + byteWidths: byteWidths, + entryNum: 0, + streamPos: stream.pos + }; + } + this.readXRefStream(stream); + delete this.streamState; + + return stream.dict; + }, + + readXRefStream: function XRef_readXRefStream(stream) { + var i, j; + var streamState = this.streamState; + stream.pos = streamState.streamPos; + + var byteWidths = streamState.byteWidths; + var typeFieldWidth = byteWidths[0]; + var offsetFieldWidth = byteWidths[1]; + var generationFieldWidth = byteWidths[2]; + + var entryRanges = streamState.entryRanges; + while (entryRanges.length > 0) { + var first = entryRanges[0]; + var n = entryRanges[1]; + + if (!isInt(first) || !isInt(n)) { + error('Invalid XRef range fields: ' + first + ', ' + n); + } + if (!isInt(typeFieldWidth) || !isInt(offsetFieldWidth) || + !isInt(generationFieldWidth)) { + error('Invalid XRef entry fields length: ' + first + ', ' + n); + } + for (i = streamState.entryNum; i < n; ++i) { + streamState.entryNum = i; + streamState.streamPos = stream.pos; + + var type = 0, offset = 0, generation = 0; + for (j = 0; j < typeFieldWidth; ++j) { + type = (type << 8) | stream.getByte(); + } + // if type field is absent, its default value is 1 + if (typeFieldWidth === 0) { + type = 1; + } + for (j = 0; j < offsetFieldWidth; ++j) { + offset = (offset << 8) | stream.getByte(); + } + for (j = 0; j < generationFieldWidth; ++j) { + generation = (generation << 8) | stream.getByte(); + } + var entry = {}; + entry.offset = offset; + entry.gen = generation; + switch (type) { + case 0: + entry.free = true; + break; + case 1: + entry.uncompressed = true; + break; + case 2: + break; + default: + error('Invalid XRef entry type: ' + type); + } + if (!this.entries[first + i]) { + this.entries[first + i] = entry; + } + } + + streamState.entryNum = 0; + streamState.streamPos = stream.pos; + entryRanges.splice(0, 2); + } + }, + + indexObjects: function XRef_indexObjects() { + // Simple scan through the PDF content to find objects, + // trailers and XRef streams. + function readToken(data, offset) { + var token = '', ch = data[offset]; + while (ch !== 13 && ch !== 10) { + if (++offset >= data.length) { + break; + } + token += String.fromCharCode(ch); + ch = data[offset]; + } + return token; + } + function skipUntil(data, offset, what) { + var length = what.length, dataLength = data.length; + var skipped = 0; + // finding byte sequence + while (offset < dataLength) { + var i = 0; + while (i < length && data[offset + i] === what[i]) { + ++i; + } + if (i >= length) { + break; // sequence found + } + offset++; + skipped++; + } + return skipped; + } + var trailerBytes = new Uint8Array([116, 114, 97, 105, 108, 101, 114]); + var startxrefBytes = new Uint8Array([115, 116, 97, 114, 116, 120, 114, + 101, 102]); + var endobjBytes = new Uint8Array([101, 110, 100, 111, 98, 106]); + var xrefBytes = new Uint8Array([47, 88, 82, 101, 102]); + + var stream = this.stream; + stream.pos = 0; + var buffer = stream.getBytes(); + var position = stream.start, length = buffer.length; + var trailers = [], xrefStms = []; + while (position < length) { + var ch = buffer[position]; + if (ch === 32 || ch === 9 || ch === 13 || ch === 10) { + ++position; + continue; + } + if (ch === 37) { // %-comment + do { + ++position; + if (position >= length) { + break; + } + ch = buffer[position]; + } while (ch !== 13 && ch !== 10); + continue; + } + var token = readToken(buffer, position); + var m; + if (token === 'xref') { + position += skipUntil(buffer, position, trailerBytes); + trailers.push(position); + position += skipUntil(buffer, position, startxrefBytes); + } else if ((m = /^(\d+)\s+(\d+)\s+obj\b/.exec(token))) { + if (typeof this.entries[m[1]] === 'undefined') { + this.entries[m[1]] = { + offset: position, + gen: m[2] | 0, + uncompressed: true + }; + } + var contentLength = skipUntil(buffer, position, endobjBytes) + 7; + var content = buffer.subarray(position, position + contentLength); + + // checking XRef stream suspect + // (it shall have '/XRef' and next char is not a letter) + var xrefTagOffset = skipUntil(content, 0, xrefBytes); + if (xrefTagOffset < contentLength && + content[xrefTagOffset + 5] < 64) { + xrefStms.push(position); + this.xrefstms[position] = 1; // don't read it recursively + } + + position += contentLength; + } else { + position += token.length + 1; + } + } + // reading XRef streams + var i, ii; + for (i = 0, ii = xrefStms.length; i < ii; ++i) { + this.startXRefQueue.push(xrefStms[i]); + this.readXRef(/* recoveryMode */ true); + } + // finding main trailer + var dict; + for (i = 0, ii = trailers.length; i < ii; ++i) { + stream.pos = trailers[i]; + var parser = new Parser(new Lexer(stream), true, this); + var obj = parser.getObj(); + if (!isCmd(obj, 'trailer')) { + continue; + } + // read the trailer dictionary + if (!isDict(dict = parser.getObj())) { + continue; + } + // taking the first one with 'ID' + if (dict.has('ID')) { + return dict; + } + } + // no tailer with 'ID', taking last one (if exists) + if (dict) { + return dict; + } + // nothing helps + // calling error() would reject worker with an UnknownErrorException. + throw new InvalidPDFException('Invalid PDF structure'); + }, + + readXRef: function XRef_readXRef(recoveryMode) { + var stream = this.stream; + + try { + while (this.startXRefQueue.length) { + var startXRef = this.startXRefQueue[0]; + + stream.pos = startXRef + stream.start; + + var parser = new Parser(new Lexer(stream), true, this); + var obj = parser.getObj(); + var dict; + + // Get dictionary + if (isCmd(obj, 'xref')) { + // Parse end-of-file XRef + dict = this.processXRefTable(parser); + if (!this.topDict) { + this.topDict = dict; + } + + // Recursively get other XRefs 'XRefStm', if any + obj = dict.get('XRefStm'); + if (isInt(obj)) { + var pos = obj; + // ignore previously loaded xref streams + // (possible infinite recursion) + if (!(pos in this.xrefstms)) { + this.xrefstms[pos] = 1; + this.startXRefQueue.push(pos); + } + } + } else if (isInt(obj)) { + // Parse in-stream XRef + if (!isInt(parser.getObj()) || + !isCmd(parser.getObj(), 'obj') || + !isStream(obj = parser.getObj())) { + error('Invalid XRef stream'); + } + dict = this.processXRefStream(obj); + if (!this.topDict) { + this.topDict = dict; + } + if (!dict) { + error('Failed to read XRef stream'); + } + } else { + error('Invalid XRef stream header'); + } + + // Recursively get previous dictionary, if any + obj = dict.get('Prev'); + if (isInt(obj)) { + this.startXRefQueue.push(obj); + } else if (isRef(obj)) { + // The spec says Prev must not be a reference, i.e. "/Prev NNN" + // This is a fallback for non-compliant PDFs, i.e. "/Prev NNN 0 R" + this.startXRefQueue.push(obj.num); + } + + this.startXRefQueue.shift(); + } + + return this.topDict; + } catch (e) { + if (e instanceof MissingDataException) { + throw e; + } + info('(while reading XRef): ' + e); + } + + if (recoveryMode) { + return; + } + throw new XRefParseException(); + }, + + getEntry: function XRef_getEntry(i) { + var xrefEntry = this.entries[i]; + if (xrefEntry && !xrefEntry.free && xrefEntry.offset) { + return xrefEntry; + } + return null; + }, + + fetchIfRef: function XRef_fetchIfRef(obj) { + if (!isRef(obj)) { + return obj; + } + return this.fetch(obj); + }, + + fetch: function XRef_fetch(ref, suppressEncryption) { + assert(isRef(ref), 'ref object is not a reference'); + var num = ref.num; + if (num in this.cache) { + var cacheEntry = this.cache[num]; + return cacheEntry; + } + + var xrefEntry = this.getEntry(num); + + // the referenced entry can be free + if (xrefEntry === null) { + return (this.cache[num] = null); + } + + if (xrefEntry.uncompressed) { + xrefEntry = this.fetchUncompressed(ref, xrefEntry, suppressEncryption); + } else { + xrefEntry = this.fetchCompressed(xrefEntry, suppressEncryption); + } + if (isDict(xrefEntry)){ + xrefEntry.objId = ref.toString(); + } else if (isStream(xrefEntry)) { + xrefEntry.dict.objId = ref.toString(); + } + return xrefEntry; + }, + + fetchUncompressed: function XRef_fetchUncompressed(ref, xrefEntry, + suppressEncryption) { + var gen = ref.gen; + var num = ref.num; + if (xrefEntry.gen !== gen) { + error('inconsistent generation in XRef'); + } + var stream = this.stream.makeSubStream(xrefEntry.offset + + this.stream.start); + var parser = new Parser(new Lexer(stream), true, this); + var obj1 = parser.getObj(); + var obj2 = parser.getObj(); + var obj3 = parser.getObj(); + if (!isInt(obj1) || parseInt(obj1, 10) !== num || + !isInt(obj2) || parseInt(obj2, 10) !== gen || + !isCmd(obj3)) { + error('bad XRef entry'); + } + if (!isCmd(obj3, 'obj')) { + // some bad PDFs use "obj1234" and really mean 1234 + if (obj3.cmd.indexOf('obj') === 0) { + num = parseInt(obj3.cmd.substring(3), 10); + if (!isNaN(num)) { + return num; + } + } + error('bad XRef entry'); + } + if (this.encrypt && !suppressEncryption) { + xrefEntry = parser.getObj(this.encrypt.createCipherTransform(num, gen)); + } else { + xrefEntry = parser.getObj(); + } + if (!isStream(xrefEntry)) { + this.cache[num] = xrefEntry; + } + return xrefEntry; + }, + + fetchCompressed: function XRef_fetchCompressed(xrefEntry, + suppressEncryption) { + var tableOffset = xrefEntry.offset; + var stream = this.fetch(new Ref(tableOffset, 0)); + if (!isStream(stream)) { + error('bad ObjStm stream'); + } + var first = stream.dict.get('First'); + var n = stream.dict.get('N'); + if (!isInt(first) || !isInt(n)) { + error('invalid first and n parameters for ObjStm stream'); + } + var parser = new Parser(new Lexer(stream), false, this); + parser.allowStreams = true; + var i, entries = [], num, nums = []; + // read the object numbers to populate cache + for (i = 0; i < n; ++i) { + num = parser.getObj(); + if (!isInt(num)) { + error('invalid object number in the ObjStm stream: ' + num); + } + nums.push(num); + var offset = parser.getObj(); + if (!isInt(offset)) { + error('invalid object offset in the ObjStm stream: ' + offset); + } + } + // read stream objects for cache + for (i = 0; i < n; ++i) { + entries.push(parser.getObj()); + num = nums[i]; + var entry = this.entries[num]; + if (entry && entry.offset === tableOffset && entry.gen === i) { + this.cache[num] = entries[i]; + } + } + xrefEntry = entries[xrefEntry.gen]; + if (xrefEntry === undefined) { + error('bad XRef entry for compressed object'); + } + return xrefEntry; + }, + + fetchIfRefAsync: function XRef_fetchIfRefAsync(obj) { + if (!isRef(obj)) { + return Promise.resolve(obj); + } + return this.fetchAsync(obj); + }, + + fetchAsync: function XRef_fetchAsync(ref, suppressEncryption) { + var streamManager = this.stream.manager; + var xref = this; + return new Promise(function tryFetch(resolve, reject) { + try { + resolve(xref.fetch(ref, suppressEncryption)); + } catch (e) { + if (e instanceof MissingDataException) { + streamManager.requestRange(e.begin, e.end, function () { + tryFetch(resolve, reject); + }); + return; + } + reject(e); + } + }); + }, + + getCatalogObj: function XRef_getCatalogObj() { + return this.root; + } + }; + + return XRef; +})(); + +/** + * A NameTree is like a Dict but has some advantageous properties, see the + * spec (7.9.6) for more details. + * TODO: implement all the Dict functions and make this more efficent. + */ +var NameTree = (function NameTreeClosure() { + function NameTree(root, xref) { + this.root = root; + this.xref = xref; + } + + NameTree.prototype = { + getAll: function NameTree_getAll() { + var dict = {}; + if (!this.root) { + return dict; + } + var xref = this.xref; + // reading name tree + var processed = new RefSet(); + processed.put(this.root); + var queue = [this.root]; + while (queue.length > 0) { + var i, n; + var obj = xref.fetchIfRef(queue.shift()); + if (!isDict(obj)) { + continue; + } + if (obj.has('Kids')) { + var kids = obj.get('Kids'); + for (i = 0, n = kids.length; i < n; i++) { + var kid = kids[i]; + if (processed.has(kid)) { + error('invalid destinations'); + } + queue.push(kid); + processed.put(kid); + } + continue; + } + var names = obj.get('Names'); + if (names) { + for (i = 0, n = names.length; i < n; i += 2) { + dict[names[i]] = xref.fetchIfRef(names[i + 1]); + } + } + } + return dict; + }, + + get: function NameTree_get(destinationId) { + if (!this.root) { + return null; + } + + var xref = this.xref; + var kidsOrNames = xref.fetchIfRef(this.root); + var loopCount = 0; + var MAX_NAMES_LEVELS = 10; + var l, r, m; + + // Perform a binary search to quickly find the entry that + // contains the named destination we are looking for. + while (kidsOrNames.has('Kids')) { + loopCount++; + if (loopCount > MAX_NAMES_LEVELS) { + warn('Search depth limit for named destionations has been reached.'); + return null; + } + + var kids = kidsOrNames.get('Kids'); + if (!isArray(kids)) { + return null; + } + + l = 0; + r = kids.length - 1; + while (l <= r) { + m = (l + r) >> 1; + var kid = xref.fetchIfRef(kids[m]); + var limits = kid.get('Limits'); + + if (destinationId < limits[0]) { + r = m - 1; + } else if (destinationId > limits[1]) { + l = m + 1; + } else { + kidsOrNames = xref.fetchIfRef(kids[m]); + break; + } + } + if (l > r) { + return null; + } + } + + // If we get here, then we have found the right entry. Now + // go through the named destinations in the Named dictionary + // until we find the exact destination we're looking for. + var names = kidsOrNames.get('Names'); + if (isArray(names)) { + // Perform a binary search to reduce the lookup time. + l = 0; + r = names.length - 2; + while (l <= r) { + // Check only even indices (0, 2, 4, ...) because the + // odd indices contain the actual D array. + m = (l + r) & ~1; + if (destinationId < names[m]) { + r = m - 2; + } else if (destinationId > names[m]) { + l = m + 2; + } else { + return xref.fetchIfRef(names[m + 1]); + } + } + } + return null; + } + }; + return NameTree; +})(); + +/** + * "A PDF file can refer to the contents of another file by using a File + * Specification (PDF 1.1)", see the spec (7.11) for more details. + * NOTE: Only embedded files are supported (as part of the attachments support) + * TODO: support the 'URL' file system (with caching if !/V), portable + * collections attributes and related files (/RF) + */ +var FileSpec = (function FileSpecClosure() { + function FileSpec(root, xref) { + if (!root || !isDict(root)) { + return; + } + this.xref = xref; + this.root = root; + if (root.has('FS')) { + this.fs = root.get('FS'); + } + this.description = root.has('Desc') ? + stringToPDFString(root.get('Desc')) : + ''; + if (root.has('RF')) { + warn('Related file specifications are not supported'); + } + this.contentAvailable = true; + if (!root.has('EF')) { + this.contentAvailable = false; + warn('Non-embedded file specifications are not supported'); + } + } + + function pickPlatformItem(dict) { + // Look for the filename in this order: + // UF, F, Unix, Mac, DOS + if (dict.has('UF')) { + return dict.get('UF'); + } else if (dict.has('F')) { + return dict.get('F'); + } else if (dict.has('Unix')) { + return dict.get('Unix'); + } else if (dict.has('Mac')) { + return dict.get('Mac'); + } else if (dict.has('DOS')) { + return dict.get('DOS'); + } else { + return null; + } + } + + FileSpec.prototype = { + get filename() { + if (!this._filename && this.root) { + var filename = pickPlatformItem(this.root) || 'unnamed'; + this._filename = stringToPDFString(filename). + replace(/\\\\/g, '\\'). + replace(/\\\//g, '/'). + replace(/\\/g, '/'); + } + return this._filename; + }, + get content() { + if (!this.contentAvailable) { + return null; + } + if (!this.contentRef && this.root) { + this.contentRef = pickPlatformItem(this.root.get('EF')); + } + var content = null; + if (this.contentRef) { + var xref = this.xref; + var fileObj = xref.fetchIfRef(this.contentRef); + if (fileObj && isStream(fileObj)) { + content = fileObj.getBytes(); + } else { + warn('Embedded file specification points to non-existing/invalid ' + + 'content'); + } + } else { + warn('Embedded file specification does not have a content'); + } + return content; + }, + get serializable() { + return { + filename: this.filename, + content: this.content + }; + } + }; + return FileSpec; +})(); + +/** + * A helper for loading missing data in object graphs. It traverses the graph + * depth first and queues up any objects that have missing data. Once it has + * has traversed as many objects that are available it attempts to bundle the + * missing data requests and then resume from the nodes that weren't ready. + * + * NOTE: It provides protection from circular references by keeping track of + * of loaded references. However, you must be careful not to load any graphs + * that have references to the catalog or other pages since that will cause the + * entire PDF document object graph to be traversed. + */ +var ObjectLoader = (function() { + function mayHaveChildren(value) { + return isRef(value) || isDict(value) || isArray(value) || isStream(value); + } + + function addChildren(node, nodesToVisit) { + var value; + if (isDict(node) || isStream(node)) { + var map; + if (isDict(node)) { + map = node.map; + } else { + map = node.dict.map; + } + for (var key in map) { + value = map[key]; + if (mayHaveChildren(value)) { + nodesToVisit.push(value); + } + } + } else if (isArray(node)) { + for (var i = 0, ii = node.length; i < ii; i++) { + value = node[i]; + if (mayHaveChildren(value)) { + nodesToVisit.push(value); + } + } + } + } + + function ObjectLoader(obj, keys, xref) { + this.obj = obj; + this.keys = keys; + this.xref = xref; + this.refSet = null; + } + + ObjectLoader.prototype = { + load: function ObjectLoader_load() { + var keys = this.keys; + this.capability = createPromiseCapability(); + // Don't walk the graph if all the data is already loaded. + if (!(this.xref.stream instanceof ChunkedStream) || + this.xref.stream.getMissingChunks().length === 0) { + this.capability.resolve(); + return this.capability.promise; + } + + this.refSet = new RefSet(); + // Setup the initial nodes to visit. + var nodesToVisit = []; + for (var i = 0; i < keys.length; i++) { + nodesToVisit.push(this.obj[keys[i]]); + } + + this.walk(nodesToVisit); + return this.capability.promise; + }, + + walk: function ObjectLoader_walk(nodesToVisit) { + var nodesToRevisit = []; + var pendingRequests = []; + // DFS walk of the object graph. + while (nodesToVisit.length) { + var currentNode = nodesToVisit.pop(); + + // Only references or chunked streams can cause missing data exceptions. + if (isRef(currentNode)) { + // Skip nodes that have already been visited. + if (this.refSet.has(currentNode)) { + continue; + } + try { + var ref = currentNode; + this.refSet.put(ref); + currentNode = this.xref.fetch(currentNode); + } catch (e) { + if (!(e instanceof MissingDataException)) { + throw e; + } + nodesToRevisit.push(currentNode); + pendingRequests.push({ begin: e.begin, end: e.end }); + } + } + if (currentNode && currentNode.getBaseStreams) { + var baseStreams = currentNode.getBaseStreams(); + var foundMissingData = false; + for (var i = 0; i < baseStreams.length; i++) { + var stream = baseStreams[i]; + if (stream.getMissingChunks && stream.getMissingChunks().length) { + foundMissingData = true; + pendingRequests.push({ + begin: stream.start, + end: stream.end + }); + } + } + if (foundMissingData) { + nodesToRevisit.push(currentNode); + } + } + + addChildren(currentNode, nodesToVisit); + } + + if (pendingRequests.length) { + this.xref.stream.manager.requestRanges(pendingRequests, + function pendingRequestCallback() { + nodesToVisit = nodesToRevisit; + for (var i = 0; i < nodesToRevisit.length; i++) { + var node = nodesToRevisit[i]; + // Remove any reference nodes from the currrent refset so they + // aren't skipped when we revist them. + if (isRef(node)) { + this.refSet.remove(node); + } + } + this.walk(nodesToVisit); + }.bind(this)); + return; + } + // Everything is loaded. + this.refSet = null; + this.capability.resolve(); + } + }; + + return ObjectLoader; +})(); + + +var ISOAdobeCharset = [ + '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', + 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', + 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', + 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', + 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', + 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', + 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent', + 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', + 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft', + 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl', + 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', + 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', + 'perthousand', 'questiondown', 'grave', 'acute', 'circumflex', 'tilde', + 'macron', 'breve', 'dotaccent', 'dieresis', 'ring', 'cedilla', + 'hungarumlaut', 'ogonek', 'caron', 'emdash', 'AE', 'ordfeminine', + 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae', 'dotlessi', 'lslash', + 'oslash', 'oe', 'germandbls', 'onesuperior', 'logicalnot', 'mu', + 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', 'onequarter', + 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters', 'twosuperior', + 'registered', 'minus', 'eth', 'multiply', 'threesuperior', 'copyright', + 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', 'Atilde', + 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave', 'Iacute', + 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', 'Ocircumflex', + 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', 'Ucircumflex', + 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron', 'aacute', + 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', + 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', + 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', 'odieresis', + 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', + 'ugrave', 'yacute', 'ydieresis', 'zcaron' +]; + +var ExpertCharset = [ + '.notdef', 'space', 'exclamsmall', 'Hungarumlautsmall', 'dollaroldstyle', + 'dollarsuperior', 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', + 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma', + 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', + 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', + 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', + 'colon', 'semicolon', 'commasuperior', 'threequartersemdash', + 'periodsuperior', 'questionsmall', 'asuperior', 'bsuperior', + 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior', + 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', + 'tsuperior', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', + 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', + 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', + 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', + 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', + 'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', + 'onefitted', 'rupiah', 'Tildesmall', 'exclamdownsmall', 'centoldstyle', + 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', + 'Brevesmall', 'Caronsmall', 'Dotaccentsmall', 'Macronsmall', + 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', + 'Cedillasmall', 'onequarter', 'onehalf', 'threequarters', + 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', + 'seveneighths', 'onethird', 'twothirds', 'zerosuperior', 'onesuperior', + 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', + 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', + 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', + 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', + 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', + 'periodinferior', 'commainferior', 'Agravesmall', 'Aacutesmall', + 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', + 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', + 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall', + 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', + 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', + 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall', + 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', + 'Ydieresissmall' +]; + +var ExpertSubsetCharset = [ + '.notdef', 'space', 'dollaroldstyle', 'dollarsuperior', + 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', + 'onedotenleader', 'comma', 'hyphen', 'period', 'fraction', + 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', + 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', + 'eightoldstyle', 'nineoldstyle', 'colon', 'semicolon', 'commasuperior', + 'threequartersemdash', 'periodsuperior', 'asuperior', 'bsuperior', + 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior', + 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', + 'tsuperior', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', + 'parenrightinferior', 'hyphensuperior', 'colonmonetary', 'onefitted', + 'rupiah', 'centoldstyle', 'figuredash', 'hypheninferior', 'onequarter', + 'onehalf', 'threequarters', 'oneeighth', 'threeeighths', 'fiveeighths', + 'seveneighths', 'onethird', 'twothirds', 'zerosuperior', 'onesuperior', + 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', + 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', + 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', + 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', + 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', + 'periodinferior', 'commainferior' +]; + + +var DEFAULT_ICON_SIZE = 22; // px +var SUPPORTED_TYPES = ['Link', 'Text', 'Widget']; + +var Annotation = (function AnnotationClosure() { + // 12.5.5: Algorithm: Appearance streams + function getTransformMatrix(rect, bbox, matrix) { + var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix); + var minX = bounds[0]; + var minY = bounds[1]; + var maxX = bounds[2]; + var maxY = bounds[3]; + + if (minX === maxX || minY === maxY) { + // From real-life file, bbox was [0, 0, 0, 0]. In this case, + // just apply the transform for rect + return [1, 0, 0, 1, rect[0], rect[1]]; + } + + var xRatio = (rect[2] - rect[0]) / (maxX - minX); + var yRatio = (rect[3] - rect[1]) / (maxY - minY); + return [ + xRatio, + 0, + 0, + yRatio, + rect[0] - minX * xRatio, + rect[1] - minY * yRatio + ]; + } + + function getDefaultAppearance(dict) { + var appearanceState = dict.get('AP'); + if (!isDict(appearanceState)) { + return; + } + + var appearance; + var appearances = appearanceState.get('N'); + if (isDict(appearances)) { + var as = dict.get('AS'); + if (as && appearances.has(as.name)) { + appearance = appearances.get(as.name); + } + } else { + appearance = appearances; + } + return appearance; + } + + function Annotation(params) { + var dict = params.dict; + var data = this.data = {}; + + data.subtype = dict.get('Subtype').name; + var rect = dict.get('Rect') || [0, 0, 0, 0]; + data.rect = Util.normalizeRect(rect); + data.annotationFlags = dict.get('F'); + + var color = dict.get('C'); + if (!color) { + // The PDF spec does not mention how a missing color array is interpreted. + // Adobe Reader seems to default to black in this case. + data.color = [0, 0, 0]; + } else if (isArray(color)) { + switch (color.length) { + case 0: + // Empty array denotes transparent border. + data.color = null; + break; + case 1: + // TODO: implement DeviceGray + break; + case 3: + data.color = color; + break; + case 4: + // TODO: implement DeviceCMYK + break; + } + } + + // Some types of annotations have border style dict which has more + // info than the border array + if (dict.has('BS')) { + var borderStyle = dict.get('BS'); + data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1; + } else { + var borderArray = dict.get('Border') || [0, 0, 1]; + data.borderWidth = borderArray[2] || 0; + + // TODO: implement proper support for annotations with line dash patterns. + var dashArray = borderArray[3]; + if (data.borderWidth > 0 && dashArray) { + if (!isArray(dashArray)) { + // Ignore the border if dashArray is not actually an array, + // this is consistent with the behaviour in Adobe Reader. + data.borderWidth = 0; + } else { + var dashArrayLength = dashArray.length; + if (dashArrayLength > 0) { + // According to the PDF specification: the elements in a dashArray + // shall be numbers that are nonnegative and not all equal to zero. + var isInvalid = false; + var numPositive = 0; + for (var i = 0; i < dashArrayLength; i++) { + var validNumber = (+dashArray[i] >= 0); + if (!validNumber) { + isInvalid = true; + break; + } else if (dashArray[i] > 0) { + numPositive++; + } + } + if (isInvalid || numPositive === 0) { + data.borderWidth = 0; + } + } + } + } + } + + this.appearance = getDefaultAppearance(dict); + data.hasAppearance = !!this.appearance; + data.id = params.ref.num; + } + + Annotation.prototype = { + + getData: function Annotation_getData() { + return this.data; + }, + + isInvisible: function Annotation_isInvisible() { + var data = this.data; + if (data && SUPPORTED_TYPES.indexOf(data.subtype) !== -1) { + return false; + } else { + return !!(data && + data.annotationFlags && // Default: not invisible + data.annotationFlags & 0x1); // Invisible + } + }, + + isViewable: function Annotation_isViewable() { + var data = this.data; + return !!(!this.isInvisible() && + data && + (!data.annotationFlags || + !(data.annotationFlags & 0x22)) && // Hidden or NoView + data.rect); // rectangle is necessary + }, + + isPrintable: function Annotation_isPrintable() { + var data = this.data; + return !!(!this.isInvisible() && + data && + data.annotationFlags && // Default: not printable + data.annotationFlags & 0x4 && // Print + !(data.annotationFlags & 0x2) && // Hidden + data.rect); // rectangle is necessary + }, + + loadResources: function Annotation_loadResources(keys) { + return new Promise(function (resolve, reject) { + this.appearance.dict.getAsync('Resources').then(function (resources) { + if (!resources) { + resolve(); + return; + } + var objectLoader = new ObjectLoader(resources.map, + keys, + resources.xref); + objectLoader.load().then(function() { + resolve(resources); + }, reject); + }, reject); + }.bind(this)); + }, + + getOperatorList: function Annotation_getOperatorList(evaluator) { + + if (!this.appearance) { + return Promise.resolve(new OperatorList()); + } + + var data = this.data; + + var appearanceDict = this.appearance.dict; + var resourcesPromise = this.loadResources([ + 'ExtGState', + 'ColorSpace', + 'Pattern', + 'Shading', + 'XObject', + 'Font' + // ProcSet + // Properties + ]); + var bbox = appearanceDict.get('BBox') || [0, 0, 1, 1]; + var matrix = appearanceDict.get('Matrix') || [1, 0, 0, 1, 0 ,0]; + var transform = getTransformMatrix(data.rect, bbox, matrix); + var self = this; + + return resourcesPromise.then(function(resources) { + var opList = new OperatorList(); + opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]); + return evaluator.getOperatorList(self.appearance, resources, opList). + then(function () { + opList.addOp(OPS.endAnnotation, []); + self.appearance.reset(); + return opList; + }); + }); + } + }; + + Annotation.getConstructor = + function Annotation_getConstructor(subtype, fieldType) { + + if (!subtype) { + return; + } + + // TODO(mack): Implement FreeText annotations + if (subtype === 'Link') { + return LinkAnnotation; + } else if (subtype === 'Text') { + return TextAnnotation; + } else if (subtype === 'Widget') { + if (!fieldType) { + return; + } + + if (fieldType === 'Tx') { + return TextWidgetAnnotation; + } else { + return WidgetAnnotation; + } + } else { + return Annotation; + } + }; + + Annotation.fromRef = function Annotation_fromRef(xref, ref) { + + var dict = xref.fetchIfRef(ref); + if (!isDict(dict)) { + return; + } + + var subtype = dict.get('Subtype'); + subtype = isName(subtype) ? subtype.name : ''; + if (!subtype) { + return; + } + + var fieldType = Util.getInheritableProperty(dict, 'FT'); + fieldType = isName(fieldType) ? fieldType.name : ''; + + var Constructor = Annotation.getConstructor(subtype, fieldType); + if (!Constructor) { + return; + } + + var params = { + dict: dict, + ref: ref, + }; + + var annotation = new Constructor(params); + + if (annotation.isViewable() || annotation.isPrintable()) { + return annotation; + } else { + if (SUPPORTED_TYPES.indexOf(subtype) === -1) { + warn('unimplemented annotation type: ' + subtype); + } + } + }; + + Annotation.appendToOperatorList = function Annotation_appendToOperatorList( + annotations, opList, pdfManager, partialEvaluator, intent) { + + function reject(e) { + annotationsReadyCapability.reject(e); + } + + var annotationsReadyCapability = createPromiseCapability(); + + var annotationPromises = []; + for (var i = 0, n = annotations.length; i < n; ++i) { + if (intent === 'display' && annotations[i].isViewable() || + intent === 'print' && annotations[i].isPrintable()) { + annotationPromises.push( + annotations[i].getOperatorList(partialEvaluator)); + } + } + Promise.all(annotationPromises).then(function(datas) { + opList.addOp(OPS.beginAnnotations, []); + for (var i = 0, n = datas.length; i < n; ++i) { + var annotOpList = datas[i]; + opList.addOpList(annotOpList); + } + opList.addOp(OPS.endAnnotations, []); + annotationsReadyCapability.resolve(); + }, reject); + + return annotationsReadyCapability.promise; + }; + + return Annotation; +})(); + +var WidgetAnnotation = (function WidgetAnnotationClosure() { + + function WidgetAnnotation(params) { + Annotation.call(this, params); + + var dict = params.dict; + var data = this.data; + + data.fieldValue = stringToPDFString( + Util.getInheritableProperty(dict, 'V') || ''); + data.alternativeText = stringToPDFString(dict.get('TU') || ''); + data.defaultAppearance = Util.getInheritableProperty(dict, 'DA') || ''; + var fieldType = Util.getInheritableProperty(dict, 'FT'); + data.fieldType = isName(fieldType) ? fieldType.name : ''; + data.fieldFlags = Util.getInheritableProperty(dict, 'Ff') || 0; + this.fieldResources = Util.getInheritableProperty(dict, 'DR') || Dict.empty; + + // Building the full field name by collecting the field and + // its ancestors 'T' data and joining them using '.'. + var fieldName = []; + var namedItem = dict; + var ref = params.ref; + while (namedItem) { + var parent = namedItem.get('Parent'); + var parentRef = namedItem.getRaw('Parent'); + var name = namedItem.get('T'); + if (name) { + fieldName.unshift(stringToPDFString(name)); + } else if (parent && ref) { + // The field name is absent, that means more than one field + // with the same name may exist. Replacing the empty name + // with the '`' plus index in the parent's 'Kids' array. + // This is not in the PDF spec but necessary to id the + // the input controls. + var kids = parent.get('Kids'); + var j, jj; + for (j = 0, jj = kids.length; j < jj; j++) { + var kidRef = kids[j]; + if (kidRef.num === ref.num && kidRef.gen === ref.gen) { + break; + } + } + fieldName.unshift('`' + j); + } + namedItem = parent; + ref = parentRef; + } + data.fullName = fieldName.join('.'); + } + + var parent = Annotation.prototype; + Util.inherit(WidgetAnnotation, Annotation, { + isViewable: function WidgetAnnotation_isViewable() { + if (this.data.fieldType === 'Sig') { + warn('unimplemented annotation type: Widget signature'); + return false; + } + + return parent.isViewable.call(this); + } + }); + + return WidgetAnnotation; +})(); + +var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { + function TextWidgetAnnotation(params) { + WidgetAnnotation.call(this, params); + + this.data.textAlignment = Util.getInheritableProperty(params.dict, 'Q'); + this.data.annotationType = AnnotationType.WIDGET; + this.data.hasHtml = !this.data.hasAppearance && !!this.data.fieldValue; + } + + Util.inherit(TextWidgetAnnotation, WidgetAnnotation, { + getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) { + if (this.appearance) { + return Annotation.prototype.getOperatorList.call(this, evaluator); + } + + var opList = new OperatorList(); + var data = this.data; + + // Even if there is an appearance stream, ignore it. This is the + // behaviour used by Adobe Reader. + if (!data.defaultAppearance) { + return Promise.resolve(opList); + } + + var stream = new Stream(stringToBytes(data.defaultAppearance)); + return evaluator.getOperatorList(stream, this.fieldResources, opList). + then(function () { + return opList; + }); + } + }); + + return TextWidgetAnnotation; +})(); + +var InteractiveAnnotation = (function InteractiveAnnotationClosure() { + function InteractiveAnnotation(params) { + Annotation.call(this, params); + + this.data.hasHtml = true; + } + + Util.inherit(InteractiveAnnotation, Annotation, { }); + + return InteractiveAnnotation; +})(); + +var TextAnnotation = (function TextAnnotationClosure() { + function TextAnnotation(params) { + InteractiveAnnotation.call(this, params); + + var dict = params.dict; + var data = this.data; + + var content = dict.get('Contents'); + var title = dict.get('T'); + data.annotationType = AnnotationType.TEXT; + data.content = stringToPDFString(content || ''); + data.title = stringToPDFString(title || ''); + + if (data.hasAppearance) { + data.name = 'NoIcon'; + } else { + data.rect[1] = data.rect[3] - DEFAULT_ICON_SIZE; + data.rect[2] = data.rect[0] + DEFAULT_ICON_SIZE; + data.name = dict.has('Name') ? dict.get('Name').name : 'Note'; + } + + if (dict.has('C')) { + data.hasBgColor = true; + } + } + + Util.inherit(TextAnnotation, InteractiveAnnotation, { }); + + return TextAnnotation; +})(); + +var LinkAnnotation = (function LinkAnnotationClosure() { + function LinkAnnotation(params) { + InteractiveAnnotation.call(this, params); + + var dict = params.dict; + var data = this.data; + data.annotationType = AnnotationType.LINK; + + var action = dict.get('A'); + if (action && isDict(action)) { + var linkType = action.get('S').name; + if (linkType === 'URI') { + var url = action.get('URI'); + if (isName(url)) { + // Some bad PDFs do not put parentheses around relative URLs. + url = '/' + url.name; + } else if (url) { + url = addDefaultProtocolToUrl(url); + } + // TODO: pdf spec mentions urls can be relative to a Base + // entry in the dictionary. + if (!isValidUrl(url, false)) { + url = ''; + } + // According to ISO 32000-1:2008, section 12.6.4.7, + // URI should to be encoded in 7-bit ASCII. + // Some bad PDFs may have URIs in UTF-8 encoding, see Bugzilla 1122280. + try { + data.url = stringToUTF8String(url); + } catch (e) { + // Fall back to a simple copy. + data.url = url; + } + } else if (linkType === 'GoTo') { + data.dest = action.get('D'); + } else if (linkType === 'GoToR') { + var urlDict = action.get('F'); + if (isDict(urlDict)) { + // We assume that the 'url' is a Filspec dictionary + // and fetch the url without checking any further + url = urlDict.get('F') || ''; + } + + // TODO: pdf reference says that GoToR + // can also have 'NewWindow' attribute + if (!isValidUrl(url, false)) { + url = ''; + } + data.url = url; + data.dest = action.get('D'); + } else if (linkType === 'Named') { + data.action = action.get('N').name; + } else { + warn('unrecognized link type: ' + linkType); + } + } else if (dict.has('Dest')) { + // simple destination link + var dest = dict.get('Dest'); + data.dest = isName(dest) ? dest.name : dest; + } + } + + // Lets URLs beginning with 'www.' default to using the 'http://' protocol. + function addDefaultProtocolToUrl(url) { + if (url && url.indexOf('www.') === 0) { + return ('http://' + url); + } + return url; + } + + Util.inherit(LinkAnnotation, InteractiveAnnotation, { }); + + return LinkAnnotation; +})(); + + +var PDFFunction = (function PDFFunctionClosure() { + var CONSTRUCT_SAMPLED = 0; + var CONSTRUCT_INTERPOLATED = 2; + var CONSTRUCT_STICHED = 3; + var CONSTRUCT_POSTSCRIPT = 4; + + return { + getSampleArray: function PDFFunction_getSampleArray(size, outputSize, bps, + str) { + var i, ii; + var length = 1; + for (i = 0, ii = size.length; i < ii; i++) { + length *= size[i]; + } + length *= outputSize; + + var array = new Array(length); + var codeSize = 0; + var codeBuf = 0; + // 32 is a valid bps so shifting won't work + var sampleMul = 1.0 / (Math.pow(2.0, bps) - 1); + + var strBytes = str.getBytes((length * bps + 7) / 8); + var strIdx = 0; + for (i = 0; i < length; i++) { + while (codeSize < bps) { + codeBuf <<= 8; + codeBuf |= strBytes[strIdx++]; + codeSize += 8; + } + codeSize -= bps; + array[i] = (codeBuf >> codeSize) * sampleMul; + codeBuf &= (1 << codeSize) - 1; + } + return array; + }, + + getIR: function PDFFunction_getIR(xref, fn) { + var dict = fn.dict; + if (!dict) { + dict = fn; + } + + var types = [this.constructSampled, + null, + this.constructInterpolated, + this.constructStiched, + this.constructPostScript]; + + var typeNum = dict.get('FunctionType'); + var typeFn = types[typeNum]; + if (!typeFn) { + error('Unknown type of function'); + } + + return typeFn.call(this, fn, dict, xref); + }, + + fromIR: function PDFFunction_fromIR(IR) { + var type = IR[0]; + switch (type) { + case CONSTRUCT_SAMPLED: + return this.constructSampledFromIR(IR); + case CONSTRUCT_INTERPOLATED: + return this.constructInterpolatedFromIR(IR); + case CONSTRUCT_STICHED: + return this.constructStichedFromIR(IR); + //case CONSTRUCT_POSTSCRIPT: + default: + return this.constructPostScriptFromIR(IR); + } + }, + + parse: function PDFFunction_parse(xref, fn) { + var IR = this.getIR(xref, fn); + return this.fromIR(IR); + }, + + parseArray: function PDFFunction_parseArray(xref, fnObj) { + if (!isArray(fnObj)) { + // not an array -- parsing as regular function + return this.parse(xref, fnObj); + } + + var fnArray = []; + for (var j = 0, jj = fnObj.length; j < jj; j++) { + var obj = xref.fetchIfRef(fnObj[j]); + fnArray.push(PDFFunction.parse(xref, obj)); + } + return function (src, srcOffset, dest, destOffset) { + for (var i = 0, ii = fnArray.length; i < ii; i++) { + fnArray[i](src, srcOffset, dest, destOffset + i); + } + }; + }, + + constructSampled: function PDFFunction_constructSampled(str, dict) { + function toMultiArray(arr) { + var inputLength = arr.length; + var out = []; + var index = 0; + for (var i = 0; i < inputLength; i += 2) { + out[index] = [arr[i], arr[i + 1]]; + ++index; + } + return out; + } + var domain = dict.get('Domain'); + var range = dict.get('Range'); + + if (!domain || !range) { + error('No domain or range'); + } + + var inputSize = domain.length / 2; + var outputSize = range.length / 2; + + domain = toMultiArray(domain); + range = toMultiArray(range); + + var size = dict.get('Size'); + var bps = dict.get('BitsPerSample'); + var order = dict.get('Order') || 1; + if (order !== 1) { + // No description how cubic spline interpolation works in PDF32000:2008 + // As in poppler, ignoring order, linear interpolation may work as good + info('No support for cubic spline interpolation: ' + order); + } + + var encode = dict.get('Encode'); + if (!encode) { + encode = []; + for (var i = 0; i < inputSize; ++i) { + encode.push(0); + encode.push(size[i] - 1); + } + } + encode = toMultiArray(encode); + + var decode = dict.get('Decode'); + if (!decode) { + decode = range; + } else { + decode = toMultiArray(decode); + } + + var samples = this.getSampleArray(size, outputSize, bps, str); + + return [ + CONSTRUCT_SAMPLED, inputSize, domain, encode, decode, samples, size, + outputSize, Math.pow(2, bps) - 1, range + ]; + }, + + constructSampledFromIR: function PDFFunction_constructSampledFromIR(IR) { + // See chapter 3, page 109 of the PDF reference + function interpolate(x, xmin, xmax, ymin, ymax) { + return ymin + ((x - xmin) * ((ymax - ymin) / (xmax - xmin))); + } + + return function constructSampledFromIRResult(src, srcOffset, + dest, destOffset) { + // See chapter 3, page 110 of the PDF reference. + var m = IR[1]; + var domain = IR[2]; + var encode = IR[3]; + var decode = IR[4]; + var samples = IR[5]; + var size = IR[6]; + var n = IR[7]; + //var mask = IR[8]; + var range = IR[9]; + + // Building the cube vertices: its part and sample index + // http://rjwagner49.com/Mathematics/Interpolation.pdf + var cubeVertices = 1 << m; + var cubeN = new Float64Array(cubeVertices); + var cubeVertex = new Uint32Array(cubeVertices); + var i, j; + for (j = 0; j < cubeVertices; j++) { + cubeN[j] = 1; + } + + var k = n, pos = 1; + // Map x_i to y_j for 0 <= i < m using the sampled function. + for (i = 0; i < m; ++i) { + // x_i' = min(max(x_i, Domain_2i), Domain_2i+1) + var domain_2i = domain[i][0]; + var domain_2i_1 = domain[i][1]; + var xi = Math.min(Math.max(src[srcOffset +i], domain_2i), + domain_2i_1); + + // e_i = Interpolate(x_i', Domain_2i, Domain_2i+1, + // Encode_2i, Encode_2i+1) + var e = interpolate(xi, domain_2i, domain_2i_1, + encode[i][0], encode[i][1]); + + // e_i' = min(max(e_i, 0), Size_i - 1) + var size_i = size[i]; + e = Math.min(Math.max(e, 0), size_i - 1); + + // Adjusting the cube: N and vertex sample index + var e0 = e < size_i - 1 ? Math.floor(e) : e - 1; // e1 = e0 + 1; + var n0 = e0 + 1 - e; // (e1 - e) / (e1 - e0); + var n1 = e - e0; // (e - e0) / (e1 - e0); + var offset0 = e0 * k; + var offset1 = offset0 + k; // e1 * k + for (j = 0; j < cubeVertices; j++) { + if (j & pos) { + cubeN[j] *= n1; + cubeVertex[j] += offset1; + } else { + cubeN[j] *= n0; + cubeVertex[j] += offset0; + } + } + + k *= size_i; + pos <<= 1; + } + + for (j = 0; j < n; ++j) { + // Sum all cube vertices' samples portions + var rj = 0; + for (i = 0; i < cubeVertices; i++) { + rj += samples[cubeVertex[i] + j] * cubeN[i]; + } + + // r_j' = Interpolate(r_j, 0, 2^BitsPerSample - 1, + // Decode_2j, Decode_2j+1) + rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]); + + // y_j = min(max(r_j, range_2j), range_2j+1) + dest[destOffset + j] = Math.min(Math.max(rj, range[j][0]), + range[j][1]); + } + }; + }, + + constructInterpolated: function PDFFunction_constructInterpolated(str, + dict) { + var c0 = dict.get('C0') || [0]; + var c1 = dict.get('C1') || [1]; + var n = dict.get('N'); + + if (!isArray(c0) || !isArray(c1)) { + error('Illegal dictionary for interpolated function'); + } + + var length = c0.length; + var diff = []; + for (var i = 0; i < length; ++i) { + diff.push(c1[i] - c0[i]); + } + + return [CONSTRUCT_INTERPOLATED, c0, diff, n]; + }, + + constructInterpolatedFromIR: + function PDFFunction_constructInterpolatedFromIR(IR) { + var c0 = IR[1]; + var diff = IR[2]; + var n = IR[3]; + + var length = diff.length; + + return function constructInterpolatedFromIRResult(src, srcOffset, + dest, destOffset) { + var x = n === 1 ? src[srcOffset] : Math.pow(src[srcOffset], n); + + for (var j = 0; j < length; ++j) { + dest[destOffset + j] = c0[j] + (x * diff[j]); + } + }; + }, + + constructStiched: function PDFFunction_constructStiched(fn, dict, xref) { + var domain = dict.get('Domain'); + + if (!domain) { + error('No domain'); + } + + var inputSize = domain.length / 2; + if (inputSize !== 1) { + error('Bad domain for stiched function'); + } + + var fnRefs = dict.get('Functions'); + var fns = []; + for (var i = 0, ii = fnRefs.length; i < ii; ++i) { + fns.push(PDFFunction.getIR(xref, xref.fetchIfRef(fnRefs[i]))); + } + + var bounds = dict.get('Bounds'); + var encode = dict.get('Encode'); + + return [CONSTRUCT_STICHED, domain, bounds, encode, fns]; + }, + + constructStichedFromIR: function PDFFunction_constructStichedFromIR(IR) { + var domain = IR[1]; + var bounds = IR[2]; + var encode = IR[3]; + var fnsIR = IR[4]; + var fns = []; + var tmpBuf = new Float32Array(1); + + for (var i = 0, ii = fnsIR.length; i < ii; i++) { + fns.push(PDFFunction.fromIR(fnsIR[i])); + } + + return function constructStichedFromIRResult(src, srcOffset, + dest, destOffset) { + var clip = function constructStichedFromIRClip(v, min, max) { + if (v > max) { + v = max; + } else if (v < min) { + v = min; + } + return v; + }; + + // clip to domain + var v = clip(src[srcOffset], domain[0], domain[1]); + // calulate which bound the value is in + for (var i = 0, ii = bounds.length; i < ii; ++i) { + if (v < bounds[i]) { + break; + } + } + + // encode value into domain of function + var dmin = domain[0]; + if (i > 0) { + dmin = bounds[i - 1]; + } + var dmax = domain[1]; + if (i < bounds.length) { + dmax = bounds[i]; + } + + var rmin = encode[2 * i]; + var rmax = encode[2 * i + 1]; + + tmpBuf[0] = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); + + // call the appropriate function + fns[i](tmpBuf, 0, dest, destOffset); + }; + }, + + constructPostScript: function PDFFunction_constructPostScript(fn, dict, + xref) { + var domain = dict.get('Domain'); + var range = dict.get('Range'); + + if (!domain) { + error('No domain.'); + } + + if (!range) { + error('No range.'); + } + + var lexer = new PostScriptLexer(fn); + var parser = new PostScriptParser(lexer); + var code = parser.parse(); + + return [CONSTRUCT_POSTSCRIPT, domain, range, code]; + }, + + constructPostScriptFromIR: function PDFFunction_constructPostScriptFromIR( + IR) { + var domain = IR[1]; + var range = IR[2]; + var code = IR[3]; + + var compiled = (new PostScriptCompiler()).compile(code, domain, range); + if (compiled) { + // Compiled function consists of simple expressions such as addition, + // subtraction, Math.max, and also contains 'var' and 'return' + // statements. See the generation in the PostScriptCompiler below. + /*jshint -W054 */ + return new Function('src', 'srcOffset', 'dest', 'destOffset', compiled); + } + + info('Unable to compile PS function'); + + var numOutputs = range.length >> 1; + var numInputs = domain.length >> 1; + var evaluator = new PostScriptEvaluator(code); + // Cache the values for a big speed up, the cache size is limited though + // since the number of possible values can be huge from a PS function. + var cache = {}; + // The MAX_CACHE_SIZE is set to ~4x the maximum number of distinct values + // seen in our tests. + var MAX_CACHE_SIZE = 2048 * 4; + var cache_available = MAX_CACHE_SIZE; + var tmpBuf = new Float32Array(numInputs); + + return function constructPostScriptFromIRResult(src, srcOffset, + dest, destOffset) { + var i, value; + var key = ''; + var input = tmpBuf; + for (i = 0; i < numInputs; i++) { + value = src[srcOffset + i]; + input[i] = value; + key += value + '_'; + } + + var cachedValue = cache[key]; + if (cachedValue !== undefined) { + dest.set(cachedValue, destOffset); + return; + } + + var output = new Float32Array(numOutputs); + var stack = evaluator.execute(input); + var stackIndex = stack.length - numOutputs; + for (i = 0; i < numOutputs; i++) { + value = stack[stackIndex + i]; + var bound = range[i * 2]; + if (value < bound) { + value = bound; + } else { + bound = range[i * 2 +1]; + if (value > bound) { + value = bound; + } + } + output[i] = value; + } + if (cache_available > 0) { + cache_available--; + cache[key] = output; + } + dest.set(output, destOffset); + }; + } + }; +})(); + +function isPDFFunction(v) { + var fnDict; + if (typeof v !== 'object') { + return false; + } else if (isDict(v)) { + fnDict = v; + } else if (isStream(v)) { + fnDict = v.dict; + } else { + return false; + } + return fnDict.has('FunctionType'); +} + +var PostScriptStack = (function PostScriptStackClosure() { + var MAX_STACK_SIZE = 100; + function PostScriptStack(initialStack) { + this.stack = !initialStack ? [] : + Array.prototype.slice.call(initialStack, 0); + } + + PostScriptStack.prototype = { + push: function PostScriptStack_push(value) { + if (this.stack.length >= MAX_STACK_SIZE) { + error('PostScript function stack overflow.'); + } + this.stack.push(value); + }, + pop: function PostScriptStack_pop() { + if (this.stack.length <= 0) { + error('PostScript function stack underflow.'); + } + return this.stack.pop(); + }, + copy: function PostScriptStack_copy(n) { + if (this.stack.length + n >= MAX_STACK_SIZE) { + error('PostScript function stack overflow.'); + } + var stack = this.stack; + for (var i = stack.length - n, j = n - 1; j >= 0; j--, i++) { + stack.push(stack[i]); + } + }, + index: function PostScriptStack_index(n) { + this.push(this.stack[this.stack.length - n - 1]); + }, + // rotate the last n stack elements p times + roll: function PostScriptStack_roll(n, p) { + var stack = this.stack; + var l = stack.length - n; + var r = stack.length - 1, c = l + (p - Math.floor(p / n) * n), i, j, t; + for (i = l, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = l, j = c - 1; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = c, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + } + }; + return PostScriptStack; +})(); +var PostScriptEvaluator = (function PostScriptEvaluatorClosure() { + function PostScriptEvaluator(operators) { + this.operators = operators; + } + PostScriptEvaluator.prototype = { + execute: function PostScriptEvaluator_execute(initialStack) { + var stack = new PostScriptStack(initialStack); + var counter = 0; + var operators = this.operators; + var length = operators.length; + var operator, a, b; + while (counter < length) { + operator = operators[counter++]; + if (typeof operator === 'number') { + // Operator is really an operand and should be pushed to the stack. + stack.push(operator); + continue; + } + switch (operator) { + // non standard ps operators + case 'jz': // jump if false + b = stack.pop(); + a = stack.pop(); + if (!a) { + counter = b; + } + break; + case 'j': // jump + a = stack.pop(); + counter = a; + break; + + // all ps operators in alphabetical order (excluding if/ifelse) + case 'abs': + a = stack.pop(); + stack.push(Math.abs(a)); + break; + case 'add': + b = stack.pop(); + a = stack.pop(); + stack.push(a + b); + break; + case 'and': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) { + stack.push(a && b); + } else { + stack.push(a & b); + } + break; + case 'atan': + a = stack.pop(); + stack.push(Math.atan(a)); + break; + case 'bitshift': + b = stack.pop(); + a = stack.pop(); + if (a > 0) { + stack.push(a << b); + } else { + stack.push(a >> b); + } + break; + case 'ceiling': + a = stack.pop(); + stack.push(Math.ceil(a)); + break; + case 'copy': + a = stack.pop(); + stack.copy(a); + break; + case 'cos': + a = stack.pop(); + stack.push(Math.cos(a)); + break; + case 'cvi': + a = stack.pop() | 0; + stack.push(a); + break; + case 'cvr': + // noop + break; + case 'div': + b = stack.pop(); + a = stack.pop(); + stack.push(a / b); + break; + case 'dup': + stack.copy(1); + break; + case 'eq': + b = stack.pop(); + a = stack.pop(); + stack.push(a === b); + break; + case 'exch': + stack.roll(2, 1); + break; + case 'exp': + b = stack.pop(); + a = stack.pop(); + stack.push(Math.pow(a, b)); + break; + case 'false': + stack.push(false); + break; + case 'floor': + a = stack.pop(); + stack.push(Math.floor(a)); + break; + case 'ge': + b = stack.pop(); + a = stack.pop(); + stack.push(a >= b); + break; + case 'gt': + b = stack.pop(); + a = stack.pop(); + stack.push(a > b); + break; + case 'idiv': + b = stack.pop(); + a = stack.pop(); + stack.push((a / b) | 0); + break; + case 'index': + a = stack.pop(); + stack.index(a); + break; + case 'le': + b = stack.pop(); + a = stack.pop(); + stack.push(a <= b); + break; + case 'ln': + a = stack.pop(); + stack.push(Math.log(a)); + break; + case 'log': + a = stack.pop(); + stack.push(Math.log(a) / Math.LN10); + break; + case 'lt': + b = stack.pop(); + a = stack.pop(); + stack.push(a < b); + break; + case 'mod': + b = stack.pop(); + a = stack.pop(); + stack.push(a % b); + break; + case 'mul': + b = stack.pop(); + a = stack.pop(); + stack.push(a * b); + break; + case 'ne': + b = stack.pop(); + a = stack.pop(); + stack.push(a !== b); + break; + case 'neg': + a = stack.pop(); + stack.push(-a); + break; + case 'not': + a = stack.pop(); + if (isBool(a)) { + stack.push(!a); + } else { + stack.push(~a); + } + break; + case 'or': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) { + stack.push(a || b); + } else { + stack.push(a | b); + } + break; + case 'pop': + stack.pop(); + break; + case 'roll': + b = stack.pop(); + a = stack.pop(); + stack.roll(a, b); + break; + case 'round': + a = stack.pop(); + stack.push(Math.round(a)); + break; + case 'sin': + a = stack.pop(); + stack.push(Math.sin(a)); + break; + case 'sqrt': + a = stack.pop(); + stack.push(Math.sqrt(a)); + break; + case 'sub': + b = stack.pop(); + a = stack.pop(); + stack.push(a - b); + break; + case 'true': + stack.push(true); + break; + case 'truncate': + a = stack.pop(); + a = a < 0 ? Math.ceil(a) : Math.floor(a); + stack.push(a); + break; + case 'xor': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) { + stack.push(a !== b); + } else { + stack.push(a ^ b); + } + break; + default: + error('Unknown operator ' + operator); + break; + } + } + return stack.stack; + } + }; + return PostScriptEvaluator; +})(); + +// Most of the PDFs functions consist of simple operations such as: +// roll, exch, sub, cvr, pop, index, dup, mul, if, gt, add. +// +// We can compile most of such programs, and at the same moment, we can +// optimize some expressions using basic math properties. Keeping track of +// min/max values will allow us to avoid extra Math.min/Math.max calls. +var PostScriptCompiler = (function PostScriptCompilerClosure() { + function AstNode(type) { + this.type = type; + } + AstNode.prototype.visit = function (visitor) { + throw new Error('abstract method'); + }; + + function AstArgument(index, min, max) { + AstNode.call(this, 'args'); + this.index = index; + this.min = min; + this.max = max; + } + AstArgument.prototype = Object.create(AstNode.prototype); + AstArgument.prototype.visit = function (visitor) { + visitor.visitArgument(this); + }; + + function AstLiteral(number) { + AstNode.call(this, 'literal'); + this.number = number; + this.min = number; + this.max = number; + } + AstLiteral.prototype = Object.create(AstNode.prototype); + AstLiteral.prototype.visit = function (visitor) { + visitor.visitLiteral(this); + }; + + function AstBinaryOperation(op, arg1, arg2, min, max) { + AstNode.call(this, 'binary'); + this.op = op; + this.arg1 = arg1; + this.arg2 = arg2; + this.min = min; + this.max = max; + } + AstBinaryOperation.prototype = Object.create(AstNode.prototype); + AstBinaryOperation.prototype.visit = function (visitor) { + visitor.visitBinaryOperation(this); + }; + + function AstMin(arg, max) { + AstNode.call(this, 'max'); + this.arg = arg; + this.min = arg.min; + this.max = max; + } + AstMin.prototype = Object.create(AstNode.prototype); + AstMin.prototype.visit = function (visitor) { + visitor.visitMin(this); + }; + + function AstVariable(index, min, max) { + AstNode.call(this, 'var'); + this.index = index; + this.min = min; + this.max = max; + } + AstVariable.prototype = Object.create(AstNode.prototype); + AstVariable.prototype.visit = function (visitor) { + visitor.visitVariable(this); + }; + + function AstVariableDefinition(variable, arg) { + AstNode.call(this, 'definition'); + this.variable = variable; + this.arg = arg; + } + AstVariableDefinition.prototype = Object.create(AstNode.prototype); + AstVariableDefinition.prototype.visit = function (visitor) { + visitor.visitVariableDefinition(this); + }; + + function ExpressionBuilderVisitor() { + this.parts = []; + } + ExpressionBuilderVisitor.prototype = { + visitArgument: function (arg) { + this.parts.push('Math.max(', arg.min, ', Math.min(', + arg.max, ', src[srcOffset + ', arg.index, ']))'); + }, + visitVariable: function (variable) { + this.parts.push('v', variable.index); + }, + visitLiteral: function (literal) { + this.parts.push(literal.number); + }, + visitBinaryOperation: function (operation) { + this.parts.push('('); + operation.arg1.visit(this); + this.parts.push(' ', operation.op, ' '); + operation.arg2.visit(this); + this.parts.push(')'); + }, + visitVariableDefinition: function (definition) { + this.parts.push('var '); + definition.variable.visit(this); + this.parts.push(' = '); + definition.arg.visit(this); + this.parts.push(';'); + }, + visitMin: function (max) { + this.parts.push('Math.min('); + max.arg.visit(this); + this.parts.push(', ', max.max, ')'); + }, + toString: function () { + return this.parts.join(''); + } + }; + + function buildAddOperation(num1, num2) { + if (num2.type === 'literal' && num2.number === 0) { + // optimization: second operand is 0 + return num1; + } + if (num1.type === 'literal' && num1.number === 0) { + // optimization: first operand is 0 + return num2; + } + if (num2.type === 'literal' && num1.type === 'literal') { + // optimization: operands operand are literals + return new AstLiteral(num1.number + num2.number); + } + return new AstBinaryOperation('+', num1, num2, + num1.min + num2.min, num1.max + num2.max); + } + + function buildMulOperation(num1, num2) { + if (num2.type === 'literal') { + // optimization: second operands is a literal... + if (num2.number === 0) { + return new AstLiteral(0); // and it's 0 + } else if (num2.number === 1) { + return num1; // and it's 1 + } else if (num1.type === 'literal') { + // ... and first operands is a literal too + return new AstLiteral(num1.number * num2.number); + } + } + if (num1.type === 'literal') { + // optimization: first operands is a literal... + if (num1.number === 0) { + return new AstLiteral(0); // and it's 0 + } else if (num1.number === 1) { + return num2; // and it's 1 + } + } + var min = Math.min(num1.min * num2.min, num1.min * num2.max, + num1.max * num2.min, num1.max * num2.max); + var max = Math.max(num1.min * num2.min, num1.min * num2.max, + num1.max * num2.min, num1.max * num2.max); + return new AstBinaryOperation('*', num1, num2, min, max); + } + + function buildSubOperation(num1, num2) { + if (num2.type === 'literal') { + // optimization: second operands is a literal... + if (num2.number === 0) { + return num1; // ... and it's 0 + } else if (num1.type === 'literal') { + // ... and first operands is a literal too + return new AstLiteral(num1.number - num2.number); + } + } + if (num2.type === 'binary' && num2.op === '-' && + num1.type === 'literal' && num1.number === 1 && + num2.arg1.type === 'literal' && num2.arg1.number === 1) { + // optimization for case: 1 - (1 - x) + return num2.arg2; + } + return new AstBinaryOperation('-', num1, num2, + num1.min - num2.max, num1.max - num2.min); + } + + function buildMinOperation(num1, max) { + if (num1.min >= max) { + // optimization: num1 min value is not less than required max + return new AstLiteral(max); // just returning max + } else if (num1.max <= max) { + // optimization: num1 max value is not greater than required max + return num1; // just returning an argument + } + return new AstMin(num1, max); + } + + function PostScriptCompiler() {} + PostScriptCompiler.prototype = { + compile: function PostScriptCompiler_compile(code, domain, range) { + var stack = []; + var i, ii; + var instructions = []; + var inputSize = domain.length >> 1, outputSize = range.length >> 1; + var lastRegister = 0; + var n, j, min, max; + var num1, num2, ast1, ast2, tmpVar, item; + for (i = 0; i < inputSize; i++) { + stack.push(new AstArgument(i, domain[i * 2], domain[i * 2 + 1])); + } + + for (i = 0, ii = code.length; i < ii; i++) { + item = code[i]; + if (typeof item === 'number') { + stack.push(new AstLiteral(item)); + continue; + } + + switch (item) { + case 'add': + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + stack.push(buildAddOperation(num1, num2)); + break; + case 'cvr': + if (stack.length < 1) { + return null; + } + break; + case 'mul': + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + stack.push(buildMulOperation(num1, num2)); + break; + case 'sub': + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + stack.push(buildSubOperation(num1, num2)); + break; + case 'exch': + if (stack.length < 2) { + return null; + } + ast1 = stack.pop(); ast2 = stack.pop(); + stack.push(ast1, ast2); + break; + case 'pop': + if (stack.length < 1) { + return null; + } + stack.pop(); + break; + case 'index': + if (stack.length < 1) { + return null; + } + num1 = stack.pop(); + if (num1.type !== 'literal') { + return null; + } + n = num1.number; + if (n < 0 || (n|0) !== n || stack.length < n) { + return null; + } + ast1 = stack[stack.length - n - 1]; + if (ast1.type === 'literal' || ast1.type === 'var') { + stack.push(ast1); + break; + } + tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max); + stack[stack.length - n - 1] = tmpVar; + stack.push(tmpVar); + instructions.push(new AstVariableDefinition(tmpVar, ast1)); + break; + case 'dup': + if (stack.length < 1) { + return null; + } + if (typeof code[i + 1] === 'number' && code[i + 2] === 'gt' && + code[i + 3] === i + 7 && code[i + 4] === 'jz' && + code[i + 5] === 'pop' && code[i + 6] === code[i + 1]) { + // special case of the commands sequence for the min operation + num1 = stack.pop(); + stack.push(buildMinOperation(num1, code[i + 1])); + i += 6; + break; + } + ast1 = stack[stack.length - 1]; + if (ast1.type === 'literal' || ast1.type === 'var') { + // we don't have to save into intermediate variable a literal or + // variable. + stack.push(ast1); + break; + } + tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max); + stack[stack.length - 1] = tmpVar; + stack.push(tmpVar); + instructions.push(new AstVariableDefinition(tmpVar, ast1)); + break; + case 'roll': + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + if (num2.type !== 'literal' || num1.type !== 'literal') { + // both roll operands must be numbers + return null; + } + j = num2.number; + n = num1.number; + if (n <= 0 || (n|0) !== n || (j|0) !== j || stack.length < n) { + // ... and integers + return null; + } + j = ((j % n) + n) % n; + if (j === 0) { + break; // just skipping -- there are nothing to rotate + } + Array.prototype.push.apply(stack, + stack.splice(stack.length - n, n - j)); + break; + default: + return null; // unsupported operator + } + } + + if (stack.length !== outputSize) { + return null; + } + + var result = []; + instructions.forEach(function (instruction) { + var statementBuilder = new ExpressionBuilderVisitor(); + instruction.visit(statementBuilder); + result.push(statementBuilder.toString()); + }); + stack.forEach(function (expr, i) { + var statementBuilder = new ExpressionBuilderVisitor(); + expr.visit(statementBuilder); + var min = range[i * 2], max = range[i * 2 + 1]; + var out = [statementBuilder.toString()]; + if (min > expr.min) { + out.unshift('Math.max(', min, ', '); + out.push(')'); + } + if (max < expr.max) { + out.unshift('Math.min(', max, ', '); + out.push(')'); + } + out.unshift('dest[destOffset + ', i, '] = '); + out.push(';'); + result.push(out.join('')); + }); + return result.join('\n'); + } + }; + + return PostScriptCompiler; +})(); + + +var ColorSpace = (function ColorSpaceClosure() { + // Constructor should define this.numComps, this.defaultColor, this.name + function ColorSpace() { + error('should not call ColorSpace constructor'); + } + + ColorSpace.prototype = { + /** + * Converts the color value to the RGB color. The color components are + * located in the src array starting from the srcOffset. Returns the array + * of the rgb components, each value ranging from [0,255]. + */ + getRgb: function ColorSpace_getRgb(src, srcOffset) { + var rgb = new Uint8Array(3); + this.getRgbItem(src, srcOffset, rgb, 0); + return rgb; + }, + /** + * Converts the color value to the RGB color, similar to the getRgb method. + * The result placed into the dest array starting from the destOffset. + */ + getRgbItem: function ColorSpace_getRgbItem(src, srcOffset, + dest, destOffset) { + error('Should not call ColorSpace.getRgbItem'); + }, + /** + * Converts the specified number of the color values to the RGB colors. + * The colors are located in the src array starting from the srcOffset. + * The result is placed into the dest array starting from the destOffset. + * The src array items shall be in [0,2^bits) range, the dest array items + * will be in [0,255] range. alpha01 indicates how many alpha components + * there are in the dest array; it will be either 0 (RGB array) or 1 (RGBA + * array). + */ + getRgbBuffer: function ColorSpace_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + error('Should not call ColorSpace.getRgbBuffer'); + }, + /** + * Determines the number of bytes required to store the result of the + * conversion done by the getRgbBuffer method. As in getRgbBuffer, + * |alpha01| is either 0 (RGB output) or 1 (RGBA output). + */ + getOutputLength: function ColorSpace_getOutputLength(inputLength, + alpha01) { + error('Should not call ColorSpace.getOutputLength'); + }, + /** + * Returns true if source data will be equal the result/output data. + */ + isPassthrough: function ColorSpace_isPassthrough(bits) { + return false; + }, + /** + * Fills in the RGB colors in the destination buffer. alpha01 indicates + * how many alpha components there are in the dest array; it will be either + * 0 (RGB array) or 1 (RGBA array). + */ + fillRgb: function ColorSpace_fillRgb(dest, originalWidth, + originalHeight, width, height, + actualHeight, bpc, comps, alpha01) { + var count = originalWidth * originalHeight; + var rgbBuf = null; + var numComponentColors = 1 << bpc; + var needsResizing = originalHeight !== height || originalWidth !== width; + var i, ii; + + if (this.isPassthrough(bpc)) { + rgbBuf = comps; + } else if (this.numComps === 1 && count > numComponentColors && + this.name !== 'DeviceGray' && this.name !== 'DeviceRGB') { + // Optimization: create a color map when there is just one component and + // we are converting more colors than the size of the color map. We + // don't build the map if the colorspace is gray or rgb since those + // methods are faster than building a map. This mainly offers big speed + // ups for indexed and alternate colorspaces. + // + // TODO it may be worth while to cache the color map. While running + // testing I never hit a cache so I will leave that out for now (perhaps + // we are reparsing colorspaces too much?). + var allColors = bpc <= 8 ? new Uint8Array(numComponentColors) : + new Uint16Array(numComponentColors); + var key; + for (i = 0; i < numComponentColors; i++) { + allColors[i] = i; + } + var colorMap = new Uint8Array(numComponentColors * 3); + this.getRgbBuffer(allColors, 0, numComponentColors, colorMap, 0, bpc, + /* alpha01 = */ 0); + + var destPos, rgbPos; + if (!needsResizing) { + // Fill in the RGB values directly into |dest|. + destPos = 0; + for (i = 0; i < count; ++i) { + key = comps[i] * 3; + dest[destPos++] = colorMap[key]; + dest[destPos++] = colorMap[key + 1]; + dest[destPos++] = colorMap[key + 2]; + destPos += alpha01; + } + } else { + rgbBuf = new Uint8Array(count * 3); + rgbPos = 0; + for (i = 0; i < count; ++i) { + key = comps[i] * 3; + rgbBuf[rgbPos++] = colorMap[key]; + rgbBuf[rgbPos++] = colorMap[key + 1]; + rgbBuf[rgbPos++] = colorMap[key + 2]; + } + } + } else { + if (!needsResizing) { + // Fill in the RGB values directly into |dest|. + this.getRgbBuffer(comps, 0, width * actualHeight, dest, 0, bpc, + alpha01); + } else { + rgbBuf = new Uint8Array(count * 3); + this.getRgbBuffer(comps, 0, count, rgbBuf, 0, bpc, + /* alpha01 = */ 0); + } + } + + if (rgbBuf) { + if (needsResizing) { + PDFImage.resize(rgbBuf, bpc, 3, originalWidth, originalHeight, width, + height, dest, alpha01); + } else { + rgbPos = 0; + destPos = 0; + for (i = 0, ii = width * actualHeight; i < ii; i++) { + dest[destPos++] = rgbBuf[rgbPos++]; + dest[destPos++] = rgbBuf[rgbPos++]; + dest[destPos++] = rgbBuf[rgbPos++]; + destPos += alpha01; + } + } + } + }, + /** + * True if the colorspace has components in the default range of [0, 1]. + * This should be true for all colorspaces except for lab color spaces + * which are [0,100], [-128, 127], [-128, 127]. + */ + usesZeroToOneRange: true + }; + + ColorSpace.parse = function ColorSpace_parse(cs, xref, res) { + var IR = ColorSpace.parseToIR(cs, xref, res); + if (IR instanceof AlternateCS) { + return IR; + } + return ColorSpace.fromIR(IR); + }; + + ColorSpace.fromIR = function ColorSpace_fromIR(IR) { + var name = isArray(IR) ? IR[0] : IR; + var whitePoint, blackPoint, gamma; + + switch (name) { + case 'DeviceGrayCS': + return this.singletons.gray; + case 'DeviceRgbCS': + return this.singletons.rgb; + case 'DeviceCmykCS': + return this.singletons.cmyk; + case 'CalGrayCS': + whitePoint = IR[1].WhitePoint; + blackPoint = IR[1].BlackPoint; + gamma = IR[1].Gamma; + return new CalGrayCS(whitePoint, blackPoint, gamma); + case 'CalRGBCS': + whitePoint = IR[1].WhitePoint; + blackPoint = IR[1].BlackPoint; + gamma = IR[1].Gamma; + var matrix = IR[1].Matrix; + return new CalRGBCS(whitePoint, blackPoint, gamma, matrix); + case 'PatternCS': + var basePatternCS = IR[1]; + if (basePatternCS) { + basePatternCS = ColorSpace.fromIR(basePatternCS); + } + return new PatternCS(basePatternCS); + case 'IndexedCS': + var baseIndexedCS = IR[1]; + var hiVal = IR[2]; + var lookup = IR[3]; + return new IndexedCS(ColorSpace.fromIR(baseIndexedCS), hiVal, lookup); + case 'AlternateCS': + var numComps = IR[1]; + var alt = IR[2]; + var tintFnIR = IR[3]; + + return new AlternateCS(numComps, ColorSpace.fromIR(alt), + PDFFunction.fromIR(tintFnIR)); + case 'LabCS': + whitePoint = IR[1].WhitePoint; + blackPoint = IR[1].BlackPoint; + var range = IR[1].Range; + return new LabCS(whitePoint, blackPoint, range); + default: + error('Unknown name ' + name); + } + return null; + }; + + ColorSpace.parseToIR = function ColorSpace_parseToIR(cs, xref, res) { + if (isName(cs)) { + var colorSpaces = res.get('ColorSpace'); + if (isDict(colorSpaces)) { + var refcs = colorSpaces.get(cs.name); + if (refcs) { + cs = refcs; + } + } + } + + cs = xref.fetchIfRef(cs); + var mode; + + if (isName(cs)) { + mode = cs.name; + this.mode = mode; + + switch (mode) { + case 'DeviceGray': + case 'G': + return 'DeviceGrayCS'; + case 'DeviceRGB': + case 'RGB': + return 'DeviceRgbCS'; + case 'DeviceCMYK': + case 'CMYK': + return 'DeviceCmykCS'; + case 'Pattern': + return ['PatternCS', null]; + default: + error('unrecognized colorspace ' + mode); + } + } else if (isArray(cs)) { + mode = cs[0].name; + this.mode = mode; + var numComps, params; + + switch (mode) { + case 'DeviceGray': + case 'G': + return 'DeviceGrayCS'; + case 'DeviceRGB': + case 'RGB': + return 'DeviceRgbCS'; + case 'DeviceCMYK': + case 'CMYK': + return 'DeviceCmykCS'; + case 'CalGray': + params = xref.fetchIfRef(cs[1]).getAll(); + return ['CalGrayCS', params]; + case 'CalRGB': + params = xref.fetchIfRef(cs[1]).getAll(); + return ['CalRGBCS', params]; + case 'ICCBased': + var stream = xref.fetchIfRef(cs[1]); + var dict = stream.dict; + numComps = dict.get('N'); + if (numComps === 1) { + return 'DeviceGrayCS'; + } else if (numComps === 3) { + return 'DeviceRgbCS'; + } else if (numComps === 4) { + return 'DeviceCmykCS'; + } + break; + case 'Pattern': + var basePatternCS = cs[1]; + if (basePatternCS) { + basePatternCS = ColorSpace.parseToIR(basePatternCS, xref, res); + } + return ['PatternCS', basePatternCS]; + case 'Indexed': + case 'I': + var baseIndexedCS = ColorSpace.parseToIR(cs[1], xref, res); + var hiVal = cs[2] + 1; + var lookup = xref.fetchIfRef(cs[3]); + if (isStream(lookup)) { + lookup = lookup.getBytes(); + } + return ['IndexedCS', baseIndexedCS, hiVal, lookup]; + case 'Separation': + case 'DeviceN': + var name = cs[1]; + numComps = 1; + if (isName(name)) { + numComps = 1; + } else if (isArray(name)) { + numComps = name.length; + } + var alt = ColorSpace.parseToIR(cs[2], xref, res); + var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3])); + return ['AlternateCS', numComps, alt, tintFnIR]; + case 'Lab': + params = cs[1].getAll(); + return ['LabCS', params]; + default: + error('unimplemented color space object "' + mode + '"'); + } + } else { + error('unrecognized color space object: "' + cs + '"'); + } + return null; + }; + /** + * Checks if a decode map matches the default decode map for a color space. + * This handles the general decode maps where there are two values per + * component. e.g. [0, 1, 0, 1, 0, 1] for a RGB color. + * This does not handle Lab, Indexed, or Pattern decode maps since they are + * slightly different. + * @param {Array} decode Decode map (usually from an image). + * @param {Number} n Number of components the color space has. + */ + ColorSpace.isDefaultDecode = function ColorSpace_isDefaultDecode(decode, n) { + if (!decode) { + return true; + } + + if (n * 2 !== decode.length) { + warn('The decode map is not the correct length'); + return true; + } + for (var i = 0, ii = decode.length; i < ii; i += 2) { + if (decode[i] !== 0 || decode[i + 1] !== 1) { + return false; + } + } + return true; + }; + + ColorSpace.singletons = { + get gray() { + return shadow(this, 'gray', new DeviceGrayCS()); + }, + get rgb() { + return shadow(this, 'rgb', new DeviceRgbCS()); + }, + get cmyk() { + return shadow(this, 'cmyk', new DeviceCmykCS()); + } + }; + + return ColorSpace; +})(); + +/** + * Alternate color space handles both Separation and DeviceN color spaces. A + * Separation color space is actually just a DeviceN with one color component. + * Both color spaces use a tinting function to convert colors to a base color + * space. + */ +var AlternateCS = (function AlternateCSClosure() { + function AlternateCS(numComps, base, tintFn) { + this.name = 'Alternate'; + this.numComps = numComps; + this.defaultColor = new Float32Array(numComps); + for (var i = 0; i < numComps; ++i) { + this.defaultColor[i] = 1; + } + this.base = base; + this.tintFn = tintFn; + this.tmpBuf = new Float32Array(base.numComps); + } + + AlternateCS.prototype = { + getRgb: ColorSpace.prototype.getRgb, + getRgbItem: function AlternateCS_getRgbItem(src, srcOffset, + dest, destOffset) { + var tmpBuf = this.tmpBuf; + this.tintFn(src, srcOffset, tmpBuf, 0); + this.base.getRgbItem(tmpBuf, 0, dest, destOffset); + }, + getRgbBuffer: function AlternateCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + var tintFn = this.tintFn; + var base = this.base; + var scale = 1 / ((1 << bits) - 1); + var baseNumComps = base.numComps; + var usesZeroToOneRange = base.usesZeroToOneRange; + var isPassthrough = (base.isPassthrough(8) || !usesZeroToOneRange) && + alpha01 === 0; + var pos = isPassthrough ? destOffset : 0; + var baseBuf = isPassthrough ? dest : new Uint8Array(baseNumComps * count); + var numComps = this.numComps; + + var scaled = new Float32Array(numComps); + var tinted = new Float32Array(baseNumComps); + var i, j; + if (usesZeroToOneRange) { + for (i = 0; i < count; i++) { + for (j = 0; j < numComps; j++) { + scaled[j] = src[srcOffset++] * scale; + } + tintFn(scaled, 0, tinted, 0); + for (j = 0; j < baseNumComps; j++) { + baseBuf[pos++] = tinted[j] * 255; + } + } + } else { + for (i = 0; i < count; i++) { + for (j = 0; j < numComps; j++) { + scaled[j] = src[srcOffset++] * scale; + } + tintFn(scaled, 0, tinted, 0); + base.getRgbItem(tinted, 0, baseBuf, pos); + pos += baseNumComps; + } + } + if (!isPassthrough) { + base.getRgbBuffer(baseBuf, 0, count, dest, destOffset, 8, alpha01); + } + }, + getOutputLength: function AlternateCS_getOutputLength(inputLength, + alpha01) { + return this.base.getOutputLength(inputLength * + this.base.numComps / this.numComps, + alpha01); + }, + isPassthrough: ColorSpace.prototype.isPassthrough, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function AlternateCS_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); + }, + usesZeroToOneRange: true + }; + + return AlternateCS; +})(); + +var PatternCS = (function PatternCSClosure() { + function PatternCS(baseCS) { + this.name = 'Pattern'; + this.base = baseCS; + } + PatternCS.prototype = {}; + + return PatternCS; +})(); + +var IndexedCS = (function IndexedCSClosure() { + function IndexedCS(base, highVal, lookup) { + this.name = 'Indexed'; + this.numComps = 1; + this.defaultColor = new Uint8Array([0]); + this.base = base; + this.highVal = highVal; + + var baseNumComps = base.numComps; + var length = baseNumComps * highVal; + var lookupArray; + + if (isStream(lookup)) { + lookupArray = new Uint8Array(length); + var bytes = lookup.getBytes(length); + lookupArray.set(bytes); + } else if (isString(lookup)) { + lookupArray = new Uint8Array(length); + for (var i = 0; i < length; ++i) { + lookupArray[i] = lookup.charCodeAt(i); + } + } else if (lookup instanceof Uint8Array || lookup instanceof Array) { + lookupArray = lookup; + } else { + error('Unrecognized lookup table: ' + lookup); + } + this.lookup = lookupArray; + } + + IndexedCS.prototype = { + getRgb: ColorSpace.prototype.getRgb, + getRgbItem: function IndexedCS_getRgbItem(src, srcOffset, + dest, destOffset) { + var numComps = this.base.numComps; + var start = src[srcOffset] * numComps; + this.base.getRgbItem(this.lookup, start, dest, destOffset); + }, + getRgbBuffer: function IndexedCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + var base = this.base; + var numComps = base.numComps; + var outputDelta = base.getOutputLength(numComps, alpha01); + var lookup = this.lookup; + + for (var i = 0; i < count; ++i) { + var lookupPos = src[srcOffset++] * numComps; + base.getRgbBuffer(lookup, lookupPos, 1, dest, destOffset, 8, alpha01); + destOffset += outputDelta; + } + }, + getOutputLength: function IndexedCS_getOutputLength(inputLength, alpha01) { + return this.base.getOutputLength(inputLength * this.base.numComps, + alpha01); + }, + isPassthrough: ColorSpace.prototype.isPassthrough, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function IndexedCS_isDefaultDecode(decodeMap) { + // indexed color maps shouldn't be changed + return true; + }, + usesZeroToOneRange: true + }; + return IndexedCS; +})(); + +var DeviceGrayCS = (function DeviceGrayCSClosure() { + function DeviceGrayCS() { + this.name = 'DeviceGray'; + this.numComps = 1; + this.defaultColor = new Float32Array([0]); + } + + DeviceGrayCS.prototype = { + getRgb: ColorSpace.prototype.getRgb, + getRgbItem: function DeviceGrayCS_getRgbItem(src, srcOffset, + dest, destOffset) { + var c = (src[srcOffset] * 255) | 0; + c = c < 0 ? 0 : c > 255 ? 255 : c; + dest[destOffset] = dest[destOffset + 1] = dest[destOffset + 2] = c; + }, + getRgbBuffer: function DeviceGrayCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + var scale = 255 / ((1 << bits) - 1); + var j = srcOffset, q = destOffset; + for (var i = 0; i < count; ++i) { + var c = (scale * src[j++]) | 0; + dest[q++] = c; + dest[q++] = c; + dest[q++] = c; + q += alpha01; + } + }, + getOutputLength: function DeviceGrayCS_getOutputLength(inputLength, + alpha01) { + return inputLength * (3 + alpha01); + }, + isPassthrough: ColorSpace.prototype.isPassthrough, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function DeviceGrayCS_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); + }, + usesZeroToOneRange: true + }; + return DeviceGrayCS; +})(); + +var DeviceRgbCS = (function DeviceRgbCSClosure() { + function DeviceRgbCS() { + this.name = 'DeviceRGB'; + this.numComps = 3; + this.defaultColor = new Float32Array([0, 0, 0]); + } + DeviceRgbCS.prototype = { + getRgb: ColorSpace.prototype.getRgb, + getRgbItem: function DeviceRgbCS_getRgbItem(src, srcOffset, + dest, destOffset) { + var r = (src[srcOffset] * 255) | 0; + var g = (src[srcOffset + 1] * 255) | 0; + var b = (src[srcOffset + 2] * 255) | 0; + dest[destOffset] = r < 0 ? 0 : r > 255 ? 255 : r; + dest[destOffset + 1] = g < 0 ? 0 : g > 255 ? 255 : g; + dest[destOffset + 2] = b < 0 ? 0 : b > 255 ? 255 : b; + }, + getRgbBuffer: function DeviceRgbCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + if (bits === 8 && alpha01 === 0) { + dest.set(src.subarray(srcOffset, srcOffset + count * 3), destOffset); + return; + } + var scale = 255 / ((1 << bits) - 1); + var j = srcOffset, q = destOffset; + for (var i = 0; i < count; ++i) { + dest[q++] = (scale * src[j++]) | 0; + dest[q++] = (scale * src[j++]) | 0; + dest[q++] = (scale * src[j++]) | 0; + q += alpha01; + } + }, + getOutputLength: function DeviceRgbCS_getOutputLength(inputLength, + alpha01) { + return (inputLength * (3 + alpha01) / 3) | 0; + }, + isPassthrough: function DeviceRgbCS_isPassthrough(bits) { + return bits === 8; + }, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function DeviceRgbCS_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); + }, + usesZeroToOneRange: true + }; + return DeviceRgbCS; +})(); + +var DeviceCmykCS = (function DeviceCmykCSClosure() { + // The coefficients below was found using numerical analysis: the method of + // steepest descent for the sum((f_i - color_value_i)^2) for r/g/b colors, + // where color_value is the tabular value from the table of sampled RGB colors + // from CMYK US Web Coated (SWOP) colorspace, and f_i is the corresponding + // CMYK color conversion using the estimation below: + // f(A, B,.. N) = Acc+Bcm+Ccy+Dck+c+Fmm+Gmy+Hmk+Im+Jyy+Kyk+Ly+Mkk+Nk+255 + function convertToRgb(src, srcOffset, srcScale, dest, destOffset) { + var c = src[srcOffset + 0] * srcScale; + var m = src[srcOffset + 1] * srcScale; + var y = src[srcOffset + 2] * srcScale; + var k = src[srcOffset + 3] * srcScale; + + var r = + (c * (-4.387332384609988 * c + 54.48615194189176 * m + + 18.82290502165302 * y + 212.25662451639585 * k + + -285.2331026137004) + + m * (1.7149763477362134 * m - 5.6096736904047315 * y + + -17.873870861415444 * k - 5.497006427196366) + + y * (-2.5217340131683033 * y - 21.248923337353073 * k + + 17.5119270841813) + + k * (-21.86122147463605 * k - 189.48180835922747) + 255) | 0; + var g = + (c * (8.841041422036149 * c + 60.118027045597366 * m + + 6.871425592049007 * y + 31.159100130055922 * k + + -79.2970844816548) + + m * (-15.310361306967817 * m + 17.575251261109482 * y + + 131.35250912493976 * k - 190.9453302588951) + + y * (4.444339102852739 * y + 9.8632861493405 * k - 24.86741582555878) + + k * (-20.737325471181034 * k - 187.80453709719578) + 255) | 0; + var b = + (c * (0.8842522430003296 * c + 8.078677503112928 * m + + 30.89978309703729 * y - 0.23883238689178934 * k + + -14.183576799673286) + + m * (10.49593273432072 * m + 63.02378494754052 * y + + 50.606957656360734 * k - 112.23884253719248) + + y * (0.03296041114873217 * y + 115.60384449646641 * k + + -193.58209356861505) + + k * (-22.33816807309886 * k - 180.12613974708367) + 255) | 0; + + dest[destOffset] = r > 255 ? 255 : r < 0 ? 0 : r; + dest[destOffset + 1] = g > 255 ? 255 : g < 0 ? 0 : g; + dest[destOffset + 2] = b > 255 ? 255 : b < 0 ? 0 : b; + } + + function DeviceCmykCS() { + this.name = 'DeviceCMYK'; + this.numComps = 4; + this.defaultColor = new Float32Array([0, 0, 0, 1]); + } + DeviceCmykCS.prototype = { + getRgb: ColorSpace.prototype.getRgb, + getRgbItem: function DeviceCmykCS_getRgbItem(src, srcOffset, + dest, destOffset) { + convertToRgb(src, srcOffset, 1, dest, destOffset); + }, + getRgbBuffer: function DeviceCmykCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + var scale = 1 / ((1 << bits) - 1); + for (var i = 0; i < count; i++) { + convertToRgb(src, srcOffset, scale, dest, destOffset); + srcOffset += 4; + destOffset += 3 + alpha01; + } + }, + getOutputLength: function DeviceCmykCS_getOutputLength(inputLength, + alpha01) { + return (inputLength / 4 * (3 + alpha01)) | 0; + }, + isPassthrough: ColorSpace.prototype.isPassthrough, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function DeviceCmykCS_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); + }, + usesZeroToOneRange: true + }; + + return DeviceCmykCS; +})(); + +// +// CalGrayCS: Based on "PDF Reference, Sixth Ed", p.245 +// +var CalGrayCS = (function CalGrayCSClosure() { + function CalGrayCS(whitePoint, blackPoint, gamma) { + this.name = 'CalGray'; + this.numComps = 1; + this.defaultColor = new Float32Array([0]); + + if (!whitePoint) { + error('WhitePoint missing - required for color space CalGray'); + } + blackPoint = blackPoint || [0, 0, 0]; + gamma = gamma || 1; + + // Translate arguments to spec variables. + this.XW = whitePoint[0]; + this.YW = whitePoint[1]; + this.ZW = whitePoint[2]; + + this.XB = blackPoint[0]; + this.YB = blackPoint[1]; + this.ZB = blackPoint[2]; + + this.G = gamma; + + // Validate variables as per spec. + if (this.XW < 0 || this.ZW < 0 || this.YW !== 1) { + error('Invalid WhitePoint components for ' + this.name + + ', no fallback available'); + } + + if (this.XB < 0 || this.YB < 0 || this.ZB < 0) { + info('Invalid BlackPoint for ' + this.name + ', falling back to default'); + this.XB = this.YB = this.ZB = 0; + } + + if (this.XB !== 0 || this.YB !== 0 || this.ZB !== 0) { + warn(this.name + ', BlackPoint: XB: ' + this.XB + ', YB: ' + this.YB + + ', ZB: ' + this.ZB + ', only default values are supported.'); + } + + if (this.G < 1) { + info('Invalid Gamma: ' + this.G + ' for ' + this.name + + ', falling back to default'); + this.G = 1; + } + } + + function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) { + // A represents a gray component of a calibrated gray space. + // A <---> AG in the spec + var A = src[srcOffset] * scale; + var AG = Math.pow(A, cs.G); + + // Computes L as per spec. ( = cs.YW * AG ) + // Except if other than default BlackPoint values are used. + var L = cs.YW * AG; + // http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html, Ch 4. + // Convert values to rgb range [0, 255]. + var val = Math.max(295.8 * Math.pow(L, 0.333333333333333333) - 40.8, 0) | 0; + dest[destOffset] = val; + dest[destOffset + 1] = val; + dest[destOffset + 2] = val; + } + + CalGrayCS.prototype = { + getRgb: ColorSpace.prototype.getRgb, + getRgbItem: function CalGrayCS_getRgbItem(src, srcOffset, + dest, destOffset) { + convertToRgb(this, src, srcOffset, dest, destOffset, 1); + }, + getRgbBuffer: function CalGrayCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + var scale = 1 / ((1 << bits) - 1); + + for (var i = 0; i < count; ++i) { + convertToRgb(this, src, srcOffset, dest, destOffset, scale); + srcOffset += 1; + destOffset += 3 + alpha01; + } + }, + getOutputLength: function CalGrayCS_getOutputLength(inputLength, alpha01) { + return inputLength * (3 + alpha01); + }, + isPassthrough: ColorSpace.prototype.isPassthrough, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function CalGrayCS_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); + }, + usesZeroToOneRange: true + }; + return CalGrayCS; +})(); + +// +// CalRGBCS: Based on "PDF Reference, Sixth Ed", p.247 +// +var CalRGBCS = (function CalRGBCSClosure() { + + // See http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html for these + // matrices. + var BRADFORD_SCALE_MATRIX = new Float32Array([ + 0.8951, 0.2664, -0.1614, + -0.7502, 1.7135, 0.0367, + 0.0389, -0.0685, 1.0296]); + + var BRADFORD_SCALE_INVERSE_MATRIX = new Float32Array([ + 0.9869929, -0.1470543, 0.1599627, + 0.4323053, 0.5183603, 0.0492912, + -0.0085287, 0.0400428, 0.9684867]); + + // See http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html. + var SRGB_D65_XYZ_TO_RGB_MATRIX = new Float32Array([ + 3.2404542, -1.5371385, -0.4985314, + -0.9692660, 1.8760108, 0.0415560, + 0.0556434, -0.2040259, 1.0572252]); + + var FLAT_WHITEPOINT_MATRIX = new Float32Array([1, 1, 1]); + + var tempNormalizeMatrix = new Float32Array(3); + var tempConvertMatrix1 = new Float32Array(3); + var tempConvertMatrix2 = new Float32Array(3); + + var DECODE_L_CONSTANT = Math.pow(((8 + 16) / 116), 3) / 8.0; + + function CalRGBCS(whitePoint, blackPoint, gamma, matrix) { + this.name = 'CalRGB'; + this.numComps = 3; + this.defaultColor = new Float32Array(3); + + if (!whitePoint) { + error('WhitePoint missing - required for color space CalRGB'); + } + blackPoint = blackPoint || new Float32Array(3); + gamma = gamma || new Float32Array([1, 1, 1]); + matrix = matrix || new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); + + // Translate arguments to spec variables. + var XW = whitePoint[0]; + var YW = whitePoint[1]; + var ZW = whitePoint[2]; + this.whitePoint = whitePoint; + + var XB = blackPoint[0]; + var YB = blackPoint[1]; + var ZB = blackPoint[2]; + this.blackPoint = blackPoint; + + this.GR = gamma[0]; + this.GG = gamma[1]; + this.GB = gamma[2]; + + this.MXA = matrix[0]; + this.MYA = matrix[1]; + this.MZA = matrix[2]; + this.MXB = matrix[3]; + this.MYB = matrix[4]; + this.MZB = matrix[5]; + this.MXC = matrix[6]; + this.MYC = matrix[7]; + this.MZC = matrix[8]; + + // Validate variables as per spec. + if (XW < 0 || ZW < 0 || YW !== 1) { + error('Invalid WhitePoint components for ' + this.name + + ', no fallback available'); + } + + if (XB < 0 || YB < 0 || ZB < 0) { + info('Invalid BlackPoint for ' + this.name + ' [' + XB + ', ' + YB + + ', ' + ZB + '], falling back to default'); + this.blackPoint = new Float32Array(3); + } + + if (this.GR < 0 || this.GG < 0 || this.GB < 0) { + info('Invalid Gamma [' + this.GR + ', ' + this.GG + ', ' + this.GB + + '] for ' + this.name + ', falling back to default'); + this.GR = this.GG = this.GB = 1; + } + + if (this.MXA < 0 || this.MYA < 0 || this.MZA < 0 || + this.MXB < 0 || this.MYB < 0 || this.MZB < 0 || + this.MXC < 0 || this.MYC < 0 || this.MZC < 0) { + info('Invalid Matrix for ' + this.name + ' [' + + this.MXA + ', ' + this.MYA + ', ' + this.MZA + + this.MXB + ', ' + this.MYB + ', ' + this.MZB + + this.MXC + ', ' + this.MYC + ', ' + this.MZC + + '], falling back to default'); + this.MXA = this.MYB = this.MZC = 1; + this.MXB = this.MYA = this.MZA = this.MXC = this.MYC = this.MZB = 0; + } + } + + function matrixProduct(a, b, result) { + result[0] = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + result[1] = a[3] * b[0] + a[4] * b[1] + a[5] * b[2]; + result[2] = a[6] * b[0] + a[7] * b[1] + a[8] * b[2]; + } + + function convertToFlat(sourceWhitePoint, LMS, result) { + result[0] = LMS[0] * 1 / sourceWhitePoint[0]; + result[1] = LMS[1] * 1 / sourceWhitePoint[1]; + result[2] = LMS[2] * 1 / sourceWhitePoint[2]; + } + + function convertToD65(sourceWhitePoint, LMS, result) { + var D65X = 0.95047; + var D65Y = 1; + var D65Z = 1.08883; + + result[0] = LMS[0] * D65X / sourceWhitePoint[0]; + result[1] = LMS[1] * D65Y / sourceWhitePoint[1]; + result[2] = LMS[2] * D65Z / sourceWhitePoint[2]; + } + + function sRGBTransferFunction(color) { + // See http://en.wikipedia.org/wiki/SRGB. + if (color <= 0.0031308){ + return adjustToRange(0, 1, 12.92 * color); + } + + return adjustToRange(0, 1, (1 + 0.055) * Math.pow(color, 1 / 2.4) - 0.055); + } + + function adjustToRange(min, max, value) { + return Math.max(min, Math.min(max, value)); + } + + function decodeL(L) { + if (L < 0) { + return -decodeL(-L); + } + + if (L > 8.0) { + return Math.pow(((L + 16) / 116), 3); + } + + return L * DECODE_L_CONSTANT; + } + + function compensateBlackPoint(sourceBlackPoint, XYZ_Flat, result) { + + // In case the blackPoint is already the default blackPoint then there is + // no need to do compensation. + if (sourceBlackPoint[0] === 0 && + sourceBlackPoint[1] === 0 && + sourceBlackPoint[2] === 0) { + result[0] = XYZ_Flat[0]; + result[1] = XYZ_Flat[1]; + result[2] = XYZ_Flat[2]; + return; + } + + // For the blackPoint calculation details, please see + // http://www.adobe.com/content/dam/Adobe/en/devnet/photoshop/sdk/ + // AdobeBPC.pdf. + // The destination blackPoint is the default blackPoint [0, 0, 0]. + var zeroDecodeL = decodeL(0); + + var X_DST = zeroDecodeL; + var X_SRC = decodeL(sourceBlackPoint[0]); + + var Y_DST = zeroDecodeL; + var Y_SRC = decodeL(sourceBlackPoint[1]); + + var Z_DST = zeroDecodeL; + var Z_SRC = decodeL(sourceBlackPoint[2]); + + var X_Scale = (1 - X_DST) / (1 - X_SRC); + var X_Offset = 1 - X_Scale; + + var Y_Scale = (1 - Y_DST) / (1 - Y_SRC); + var Y_Offset = 1 - Y_Scale; + + var Z_Scale = (1 - Z_DST) / (1 - Z_SRC); + var Z_Offset = 1 - Z_Scale; + + result[0] = XYZ_Flat[0] * X_Scale + X_Offset; + result[1] = XYZ_Flat[1] * Y_Scale + Y_Offset; + result[2] = XYZ_Flat[2] * Z_Scale + Z_Offset; + } + + function normalizeWhitePointToFlat(sourceWhitePoint, XYZ_In, result) { + + // In case the whitePoint is already flat then there is no need to do + // normalization. + if (sourceWhitePoint[0] === 1 && sourceWhitePoint[2] === 1) { + result[0] = XYZ_In[0]; + result[1] = XYZ_In[1]; + result[2] = XYZ_In[2]; + return; + } + + var LMS = result; + matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS); + + var LMS_Flat = tempNormalizeMatrix; + convertToFlat(sourceWhitePoint, LMS, LMS_Flat); + + matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_Flat, result); + } + + function normalizeWhitePointToD65(sourceWhitePoint, XYZ_In, result) { + + var LMS = result; + matrixProduct(BRADFORD_SCALE_MATRIX, XYZ_In, LMS); + + var LMS_D65 = tempNormalizeMatrix; + convertToD65(sourceWhitePoint, LMS, LMS_D65); + + matrixProduct(BRADFORD_SCALE_INVERSE_MATRIX, LMS_D65, result); + } + + function convertToRgb(cs, src, srcOffset, dest, destOffset, scale) { + // A, B and C represent a red, green and blue components of a calibrated + // rgb space. + var A = adjustToRange(0, 1, src[srcOffset] * scale); + var B = adjustToRange(0, 1, src[srcOffset + 1] * scale); + var C = adjustToRange(0, 1, src[srcOffset + 2] * scale); + + // A <---> AGR in the spec + // B <---> BGG in the spec + // C <---> CGB in the spec + var AGR = Math.pow(A, cs.GR); + var BGG = Math.pow(B, cs.GG); + var CGB = Math.pow(C, cs.GB); + + // Computes intermediate variables L, M, N as per spec. + // To decode X, Y, Z values map L, M, N directly to them. + var X = cs.MXA * AGR + cs.MXB * BGG + cs.MXC * CGB; + var Y = cs.MYA * AGR + cs.MYB * BGG + cs.MYC * CGB; + var Z = cs.MZA * AGR + cs.MZB * BGG + cs.MZC * CGB; + + // The following calculations are based on this document: + // http://www.adobe.com/content/dam/Adobe/en/devnet/photoshop/sdk/ + // AdobeBPC.pdf. + var XYZ = tempConvertMatrix1; + XYZ[0] = X; + XYZ[1] = Y; + XYZ[2] = Z; + var XYZ_Flat = tempConvertMatrix2; + + normalizeWhitePointToFlat(cs.whitePoint, XYZ, XYZ_Flat); + + var XYZ_Black = tempConvertMatrix1; + compensateBlackPoint(cs.blackPoint, XYZ_Flat, XYZ_Black); + + var XYZ_D65 = tempConvertMatrix2; + normalizeWhitePointToD65(FLAT_WHITEPOINT_MATRIX, XYZ_Black, XYZ_D65); + + var SRGB = tempConvertMatrix1; + matrixProduct(SRGB_D65_XYZ_TO_RGB_MATRIX, XYZ_D65, SRGB); + + var sR = sRGBTransferFunction(SRGB[0]); + var sG = sRGBTransferFunction(SRGB[1]); + var sB = sRGBTransferFunction(SRGB[2]); + + // Convert the values to rgb range [0, 255]. + dest[destOffset] = Math.round(sR * 255); + dest[destOffset + 1] = Math.round(sG * 255); + dest[destOffset + 2] = Math.round(sB * 255); + } + + CalRGBCS.prototype = { + getRgb: function CalRGBCS_getRgb(src, srcOffset) { + var rgb = new Uint8Array(3); + this.getRgbItem(src, srcOffset, rgb, 0); + return rgb; + }, + getRgbItem: function CalRGBCS_getRgbItem(src, srcOffset, + dest, destOffset) { + convertToRgb(this, src, srcOffset, dest, destOffset, 1); + }, + getRgbBuffer: function CalRGBCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + var scale = 1 / ((1 << bits) - 1); + + for (var i = 0; i < count; ++i) { + convertToRgb(this, src, srcOffset, dest, destOffset, scale); + srcOffset += 3; + destOffset += 3 + alpha01; + } + }, + getOutputLength: function CalRGBCS_getOutputLength(inputLength, alpha01) { + return (inputLength * (3 + alpha01) / 3) | 0; + }, + isPassthrough: ColorSpace.prototype.isPassthrough, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function CalRGBCS_isDefaultDecode(decodeMap) { + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); + }, + usesZeroToOneRange: true + }; + return CalRGBCS; +})(); + +// +// LabCS: Based on "PDF Reference, Sixth Ed", p.250 +// +var LabCS = (function LabCSClosure() { + function LabCS(whitePoint, blackPoint, range) { + this.name = 'Lab'; + this.numComps = 3; + this.defaultColor = new Float32Array([0, 0, 0]); + + if (!whitePoint) { + error('WhitePoint missing - required for color space Lab'); + } + blackPoint = blackPoint || [0, 0, 0]; + range = range || [-100, 100, -100, 100]; + + // Translate args to spec variables + this.XW = whitePoint[0]; + this.YW = whitePoint[1]; + this.ZW = whitePoint[2]; + this.amin = range[0]; + this.amax = range[1]; + this.bmin = range[2]; + this.bmax = range[3]; + + // These are here just for completeness - the spec doesn't offer any + // formulas that use BlackPoint in Lab + this.XB = blackPoint[0]; + this.YB = blackPoint[1]; + this.ZB = blackPoint[2]; + + // Validate vars as per spec + if (this.XW < 0 || this.ZW < 0 || this.YW !== 1) { + error('Invalid WhitePoint components, no fallback available'); + } + + if (this.XB < 0 || this.YB < 0 || this.ZB < 0) { + info('Invalid BlackPoint, falling back to default'); + this.XB = this.YB = this.ZB = 0; + } + + if (this.amin > this.amax || this.bmin > this.bmax) { + info('Invalid Range, falling back to defaults'); + this.amin = -100; + this.amax = 100; + this.bmin = -100; + this.bmax = 100; + } + } + + // Function g(x) from spec + function fn_g(x) { + if (x >= 6 / 29) { + return x * x * x; + } else { + return (108 / 841) * (x - 4 / 29); + } + } + + function decode(value, high1, low2, high2) { + return low2 + (value) * (high2 - low2) / (high1); + } + + // If decoding is needed maxVal should be 2^bits per component - 1. + function convertToRgb(cs, src, srcOffset, maxVal, dest, destOffset) { + // XXX: Lab input is in the range of [0, 100], [amin, amax], [bmin, bmax] + // not the usual [0, 1]. If a command like setFillColor is used the src + // values will already be within the correct range. However, if we are + // converting an image we have to map the values to the correct range given + // above. + // Ls,as,bs <---> L*,a*,b* in the spec + var Ls = src[srcOffset]; + var as = src[srcOffset + 1]; + var bs = src[srcOffset + 2]; + if (maxVal !== false) { + Ls = decode(Ls, maxVal, 0, 100); + as = decode(as, maxVal, cs.amin, cs.amax); + bs = decode(bs, maxVal, cs.bmin, cs.bmax); + } + + // Adjust limits of 'as' and 'bs' + as = as > cs.amax ? cs.amax : as < cs.amin ? cs.amin : as; + bs = bs > cs.bmax ? cs.bmax : bs < cs.bmin ? cs.bmin : bs; + + // Computes intermediate variables X,Y,Z as per spec + var M = (Ls + 16) / 116; + var L = M + (as / 500); + var N = M - (bs / 200); + + var X = cs.XW * fn_g(L); + var Y = cs.YW * fn_g(M); + var Z = cs.ZW * fn_g(N); + + var r, g, b; + // Using different conversions for D50 and D65 white points, + // per http://www.color.org/srgb.pdf + if (cs.ZW < 1) { + // Assuming D50 (X=0.9642, Y=1.00, Z=0.8249) + r = X * 3.1339 + Y * -1.6170 + Z * -0.4906; + g = X * -0.9785 + Y * 1.9160 + Z * 0.0333; + b = X * 0.0720 + Y * -0.2290 + Z * 1.4057; + } else { + // Assuming D65 (X=0.9505, Y=1.00, Z=1.0888) + r = X * 3.2406 + Y * -1.5372 + Z * -0.4986; + g = X * -0.9689 + Y * 1.8758 + Z * 0.0415; + b = X * 0.0557 + Y * -0.2040 + Z * 1.0570; + } + // clamp color values to [0,1] range then convert to [0,255] range. + dest[destOffset] = r <= 0 ? 0 : r >= 1 ? 255 : Math.sqrt(r) * 255 | 0; + dest[destOffset + 1] = g <= 0 ? 0 : g >= 1 ? 255 : Math.sqrt(g) * 255 | 0; + dest[destOffset + 2] = b <= 0 ? 0 : b >= 1 ? 255 : Math.sqrt(b) * 255 | 0; + } + + LabCS.prototype = { + getRgb: ColorSpace.prototype.getRgb, + getRgbItem: function LabCS_getRgbItem(src, srcOffset, dest, destOffset) { + convertToRgb(this, src, srcOffset, false, dest, destOffset); + }, + getRgbBuffer: function LabCS_getRgbBuffer(src, srcOffset, count, + dest, destOffset, bits, + alpha01) { + var maxVal = (1 << bits) - 1; + for (var i = 0; i < count; i++) { + convertToRgb(this, src, srcOffset, maxVal, dest, destOffset); + srcOffset += 3; + destOffset += 3 + alpha01; + } + }, + getOutputLength: function LabCS_getOutputLength(inputLength, alpha01) { + return (inputLength * (3 + alpha01) / 3) | 0; + }, + isPassthrough: ColorSpace.prototype.isPassthrough, + fillRgb: ColorSpace.prototype.fillRgb, + isDefaultDecode: function LabCS_isDefaultDecode(decodeMap) { + // XXX: Decoding is handled with the lab conversion because of the strange + // ranges that are used. + return true; + }, + usesZeroToOneRange: false + }; + return LabCS; +})(); + + +var ARCFourCipher = (function ARCFourCipherClosure() { + function ARCFourCipher(key) { + this.a = 0; + this.b = 0; + var s = new Uint8Array(256); + var i, j = 0, tmp, keyLength = key.length; + for (i = 0; i < 256; ++i) { + s[i] = i; + } + for (i = 0; i < 256; ++i) { + tmp = s[i]; + j = (j + tmp + key[i % keyLength]) & 0xFF; + s[i] = s[j]; + s[j] = tmp; + } + this.s = s; + } + + ARCFourCipher.prototype = { + encryptBlock: function ARCFourCipher_encryptBlock(data) { + var i, n = data.length, tmp, tmp2; + var a = this.a, b = this.b, s = this.s; + var output = new Uint8Array(n); + for (i = 0; i < n; ++i) { + a = (a + 1) & 0xFF; + tmp = s[a]; + b = (b + tmp) & 0xFF; + tmp2 = s[b]; + s[a] = tmp2; + s[b] = tmp; + output[i] = data[i] ^ s[(tmp + tmp2) & 0xFF]; + } + this.a = a; + this.b = b; + return output; + } + }; + ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock; + + return ARCFourCipher; +})(); + +var calculateMD5 = (function calculateMD5Closure() { + var r = new Uint8Array([ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]); + + var k = new Int32Array([ + -680876936, -389564586, 606105819, -1044525330, -176418897, 1200080426, + -1473231341, -45705983, 1770035416, -1958414417, -42063, -1990404162, + 1804603682, -40341101, -1502002290, 1236535329, -165796510, -1069501632, + 643717713, -373897302, -701558691, 38016083, -660478335, -405537848, + 568446438, -1019803690, -187363961, 1163531501, -1444681467, -51403784, + 1735328473, -1926607734, -378558, -2022574463, 1839030562, -35309556, + -1530992060, 1272893353, -155497632, -1094730640, 681279174, -358537222, + -722521979, 76029189, -640364487, -421815835, 530742520, -995338651, + -198630844, 1126891415, -1416354905, -57434055, 1700485571, -1894986606, + -1051523, -2054922799, 1873313359, -30611744, -1560198380, 1309151649, + -145523070, -1120210379, 718787259, -343485551]); + + function hash(data, offset, length) { + var h0 = 1732584193, h1 = -271733879, h2 = -1732584194, h3 = 271733878; + // pre-processing + var paddedLength = (length + 72) & ~63; // data + 9 extra bytes + var padded = new Uint8Array(paddedLength); + var i, j, n; + for (i = 0; i < length; ++i) { + padded[i] = data[offset++]; + } + padded[i++] = 0x80; + n = paddedLength - 8; + while (i < n) { + padded[i++] = 0; + } + padded[i++] = (length << 3) & 0xFF; + padded[i++] = (length >> 5) & 0xFF; + padded[i++] = (length >> 13) & 0xFF; + padded[i++] = (length >> 21) & 0xFF; + padded[i++] = (length >>> 29) & 0xFF; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + var w = new Int32Array(16); + for (i = 0; i < paddedLength;) { + for (j = 0; j < 16; ++j, i += 4) { + w[j] = (padded[i] | (padded[i + 1] << 8) | + (padded[i + 2] << 16) | (padded[i + 3] << 24)); + } + var a = h0, b = h1, c = h2, d = h3, f, g; + for (j = 0; j < 64; ++j) { + if (j < 16) { + f = (b & c) | ((~b) & d); + g = j; + } else if (j < 32) { + f = (d & b) | ((~d) & c); + g = (5 * j + 1) & 15; + } else if (j < 48) { + f = b ^ c ^ d; + g = (3 * j + 5) & 15; + } else { + f = c ^ (b | (~d)); + g = (7 * j) & 15; + } + var tmp = d, rotateArg = (a + f + k[j] + w[g]) | 0, rotate = r[j]; + d = c; + c = b; + b = (b + ((rotateArg << rotate) | (rotateArg >>> (32 - rotate)))) | 0; + a = tmp; + } + h0 = (h0 + a) | 0; + h1 = (h1 + b) | 0; + h2 = (h2 + c) | 0; + h3 = (h3 + d) | 0; + } + return new Uint8Array([ + h0 & 0xFF, (h0 >> 8) & 0xFF, (h0 >> 16) & 0xFF, (h0 >>> 24) & 0xFF, + h1 & 0xFF, (h1 >> 8) & 0xFF, (h1 >> 16) & 0xFF, (h1 >>> 24) & 0xFF, + h2 & 0xFF, (h2 >> 8) & 0xFF, (h2 >> 16) & 0xFF, (h2 >>> 24) & 0xFF, + h3 & 0xFF, (h3 >> 8) & 0xFF, (h3 >> 16) & 0xFF, (h3 >>> 24) & 0xFF + ]); + } + + return hash; +})(); +var Word64 = (function Word64Closure() { + function Word64(highInteger, lowInteger) { + this.high = highInteger | 0; + this.low = lowInteger | 0; + } + Word64.prototype = { + and: function Word64_and(word) { + this.high &= word.high; + this.low &= word.low; + }, + xor: function Word64_xor(word) { + this.high ^= word.high; + this.low ^= word.low; + }, + + or: function Word64_or(word) { + this.high |= word.high; + this.low |= word.low; + }, + + shiftRight: function Word64_shiftRight(places) { + if (places >= 32) { + this.low = (this.high >>> (places - 32)) | 0; + this.high = 0; + } else { + this.low = (this.low >>> places) | (this.high << (32 - places)); + this.high = (this.high >>> places) | 0; + } + }, + + shiftLeft: function Word64_shiftLeft(places) { + if (places >= 32) { + this.high = this.low << (places - 32); + this.low = 0; + } else { + this.high = (this.high << places) | (this.low >>> (32 - places)); + this.low = this.low << places; + } + }, + + rotateRight: function Word64_rotateRight(places) { + var low, high; + if (places & 32) { + high = this.low; + low = this.high; + } else { + low = this.low; + high = this.high; + } + places &= 31; + this.low = (low >>> places) | (high << (32 - places)); + this.high = (high >>> places) | (low << (32 - places)); + }, + + not: function Word64_not() { + this.high = ~this.high; + this.low = ~this.low; + }, + + add: function Word64_add(word) { + var lowAdd = (this.low >>> 0) + (word.low >>> 0); + var highAdd = (this.high >>> 0) + (word.high >>> 0); + if (lowAdd > 0xFFFFFFFF) { + highAdd += 1; + } + this.low = lowAdd | 0; + this.high = highAdd | 0; + }, + + copyTo: function Word64_copyTo(bytes, offset) { + bytes[offset] = (this.high >>> 24) & 0xFF; + bytes[offset + 1] = (this.high >> 16) & 0xFF; + bytes[offset + 2] = (this.high >> 8) & 0xFF; + bytes[offset + 3] = this.high & 0xFF; + bytes[offset + 4] = (this.low >>> 24) & 0xFF; + bytes[offset + 5] = (this.low >> 16) & 0xFF; + bytes[offset + 6] = (this.low >> 8) & 0xFF; + bytes[offset + 7] = this.low & 0xFF; + }, + + assign: function Word64_assign(word) { + this.high = word.high; + this.low = word.low; + } + }; + return Word64; +})(); + +var calculateSHA256 = (function calculateSHA256Closure() { + function rotr(x, n) { + return (x >>> n) | (x << 32 - n); + } + + function ch(x, y, z) { + return (x & y) ^ (~x & z); + } + + function maj(x, y, z) { + return (x & y) ^ (x & z) ^ (y & z); + } + + function sigma(x) { + return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22); + } + + function sigmaPrime(x) { + return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25); + } + + function littleSigma(x) { + return rotr(x, 7) ^ rotr(x, 18) ^ x >>> 3; + } + + function littleSigmaPrime(x) { + return rotr(x, 17) ^ rotr(x, 19) ^ x >>> 10; + } + + var k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]; + + function hash(data, offset, length) { + // initial hash values + var h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, + h3 = 0xa54ff53a, h4 = 0x510e527f, h5 = 0x9b05688c, + h6 = 0x1f83d9ab, h7 = 0x5be0cd19; + // pre-processing + var paddedLength = Math.ceil((length + 9) / 64) * 64; + var padded = new Uint8Array(paddedLength); + var i, j, n; + for (i = 0; i < length; ++i) { + padded[i] = data[offset++]; + } + padded[i++] = 0x80; + n = paddedLength - 8; + while (i < n) { + padded[i++] = 0; + } + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = (length >>> 29) & 0xFF; + padded[i++] = (length >> 21) & 0xFF; + padded[i++] = (length >> 13) & 0xFF; + padded[i++] = (length >> 5) & 0xFF; + padded[i++] = (length << 3) & 0xFF; + var w = new Uint32Array(64); + // for each 512 bit block + for (i = 0; i < paddedLength;) { + for (j = 0; j < 16; ++j) { + w[j] = (padded[i] << 24 | (padded[i + 1] << 16) | + (padded[i + 2] << 8) | (padded[i + 3])); + i += 4; + } + + for (j = 16; j < 64; ++j) { + w[j] = littleSigmaPrime(w[j - 2]) + w[j - 7] + + littleSigma(w[j - 15]) + w[j - 16] | 0; + } + var a = h0, b = h1, c = h2, d = h3, e = h4, + f = h5, g = h6, h = h7, t1, t2; + for (j = 0; j < 64; ++j) { + t1 = h + sigmaPrime(e) + ch(e, f, g) + k[j] + w[j]; + t2 = sigma(a) + maj(a, b, c); + h = g; + g = f; + f = e; + e = (d + t1) | 0; + d = c; + c = b; + b = a; + a = (t1 + t2) | 0; + } + h0 = (h0 + a) | 0; + h1 = (h1 + b) | 0; + h2 = (h2 + c) | 0; + h3 = (h3 + d) | 0; + h4 = (h4 + e) | 0; + h5 = (h5 + f) | 0; + h6 = (h6 + g) | 0; + h7 = (h7 + h) | 0; + } + return new Uint8Array([ + (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, (h0) & 0xFF, + (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, (h1) & 0xFF, + (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, (h2) & 0xFF, + (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, (h3) & 0xFF, + (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, (h4) & 0xFF, + (h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, (h5) & 0xFF, + (h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, (h6) & 0xFF, + (h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, (h7) & 0xFF + ]); + } + + return hash; +})(); + +var calculateSHA512 = (function calculateSHA512Closure() { + function ch(result, x, y, z, tmp) { + result.assign(x); + result.and(y); + tmp.assign(x); + tmp.not(); + tmp.and(z); + result.xor(tmp); + } + + function maj(result, x, y, z, tmp) { + result.assign(x); + result.and(y); + tmp.assign(x); + tmp.and(z); + result.xor(tmp); + tmp.assign(y); + tmp.and(z); + result.xor(tmp); + } + + function sigma(result, x, tmp) { + result.assign(x); + result.rotateRight(28); + tmp.assign(x); + tmp.rotateRight(34); + result.xor(tmp); + tmp.assign(x); + tmp.rotateRight(39); + result.xor(tmp); + } + + function sigmaPrime(result, x, tmp) { + result.assign(x); + result.rotateRight(14); + tmp.assign(x); + tmp.rotateRight(18); + result.xor(tmp); + tmp.assign(x); + tmp.rotateRight(41); + result.xor(tmp); + } + + function littleSigma(result, x, tmp) { + result.assign(x); + result.rotateRight(1); + tmp.assign(x); + tmp.rotateRight(8); + result.xor(tmp); + tmp.assign(x); + tmp.shiftRight(7); + result.xor(tmp); + } + + function littleSigmaPrime(result, x, tmp) { + result.assign(x); + result.rotateRight(19); + tmp.assign(x); + tmp.rotateRight(61); + result.xor(tmp); + tmp.assign(x); + tmp.shiftRight(6); + result.xor(tmp); + } + + var k = [ + new Word64(0x428a2f98, 0xd728ae22), new Word64(0x71374491, 0x23ef65cd), + new Word64(0xb5c0fbcf, 0xec4d3b2f), new Word64(0xe9b5dba5, 0x8189dbbc), + new Word64(0x3956c25b, 0xf348b538), new Word64(0x59f111f1, 0xb605d019), + new Word64(0x923f82a4, 0xaf194f9b), new Word64(0xab1c5ed5, 0xda6d8118), + new Word64(0xd807aa98, 0xa3030242), new Word64(0x12835b01, 0x45706fbe), + new Word64(0x243185be, 0x4ee4b28c), new Word64(0x550c7dc3, 0xd5ffb4e2), + new Word64(0x72be5d74, 0xf27b896f), new Word64(0x80deb1fe, 0x3b1696b1), + new Word64(0x9bdc06a7, 0x25c71235), new Word64(0xc19bf174, 0xcf692694), + new Word64(0xe49b69c1, 0x9ef14ad2), new Word64(0xefbe4786, 0x384f25e3), + new Word64(0x0fc19dc6, 0x8b8cd5b5), new Word64(0x240ca1cc, 0x77ac9c65), + new Word64(0x2de92c6f, 0x592b0275), new Word64(0x4a7484aa, 0x6ea6e483), + new Word64(0x5cb0a9dc, 0xbd41fbd4), new Word64(0x76f988da, 0x831153b5), + new Word64(0x983e5152, 0xee66dfab), new Word64(0xa831c66d, 0x2db43210), + new Word64(0xb00327c8, 0x98fb213f), new Word64(0xbf597fc7, 0xbeef0ee4), + new Word64(0xc6e00bf3, 0x3da88fc2), new Word64(0xd5a79147, 0x930aa725), + new Word64(0x06ca6351, 0xe003826f), new Word64(0x14292967, 0x0a0e6e70), + new Word64(0x27b70a85, 0x46d22ffc), new Word64(0x2e1b2138, 0x5c26c926), + new Word64(0x4d2c6dfc, 0x5ac42aed), new Word64(0x53380d13, 0x9d95b3df), + new Word64(0x650a7354, 0x8baf63de), new Word64(0x766a0abb, 0x3c77b2a8), + new Word64(0x81c2c92e, 0x47edaee6), new Word64(0x92722c85, 0x1482353b), + new Word64(0xa2bfe8a1, 0x4cf10364), new Word64(0xa81a664b, 0xbc423001), + new Word64(0xc24b8b70, 0xd0f89791), new Word64(0xc76c51a3, 0x0654be30), + new Word64(0xd192e819, 0xd6ef5218), new Word64(0xd6990624, 0x5565a910), + new Word64(0xf40e3585, 0x5771202a), new Word64(0x106aa070, 0x32bbd1b8), + new Word64(0x19a4c116, 0xb8d2d0c8), new Word64(0x1e376c08, 0x5141ab53), + new Word64(0x2748774c, 0xdf8eeb99), new Word64(0x34b0bcb5, 0xe19b48a8), + new Word64(0x391c0cb3, 0xc5c95a63), new Word64(0x4ed8aa4a, 0xe3418acb), + new Word64(0x5b9cca4f, 0x7763e373), new Word64(0x682e6ff3, 0xd6b2b8a3), + new Word64(0x748f82ee, 0x5defb2fc), new Word64(0x78a5636f, 0x43172f60), + new Word64(0x84c87814, 0xa1f0ab72), new Word64(0x8cc70208, 0x1a6439ec), + new Word64(0x90befffa, 0x23631e28), new Word64(0xa4506ceb, 0xde82bde9), + new Word64(0xbef9a3f7, 0xb2c67915), new Word64(0xc67178f2, 0xe372532b), + new Word64(0xca273ece, 0xea26619c), new Word64(0xd186b8c7, 0x21c0c207), + new Word64(0xeada7dd6, 0xcde0eb1e), new Word64(0xf57d4f7f, 0xee6ed178), + new Word64(0x06f067aa, 0x72176fba), new Word64(0x0a637dc5, 0xa2c898a6), + new Word64(0x113f9804, 0xbef90dae), new Word64(0x1b710b35, 0x131c471b), + new Word64(0x28db77f5, 0x23047d84), new Word64(0x32caab7b, 0x40c72493), + new Word64(0x3c9ebe0a, 0x15c9bebc), new Word64(0x431d67c4, 0x9c100d4c), + new Word64(0x4cc5d4be, 0xcb3e42b6), new Word64(0x597f299c, 0xfc657e2a), + new Word64(0x5fcb6fab, 0x3ad6faec), new Word64(0x6c44198c, 0x4a475817)]; + + function hash(data, offset, length, mode384) { + mode384 = !!mode384; + // initial hash values + var h0, h1, h2, h3, h4, h5, h6, h7; + if (!mode384) { + h0 = new Word64(0x6a09e667, 0xf3bcc908); + h1 = new Word64(0xbb67ae85, 0x84caa73b); + h2 = new Word64(0x3c6ef372, 0xfe94f82b); + h3 = new Word64(0xa54ff53a, 0x5f1d36f1); + h4 = new Word64(0x510e527f, 0xade682d1); + h5 = new Word64(0x9b05688c, 0x2b3e6c1f); + h6 = new Word64(0x1f83d9ab, 0xfb41bd6b); + h7 = new Word64(0x5be0cd19, 0x137e2179); + } + else { + // SHA384 is exactly the same + // except with different starting values and a trimmed result + h0 = new Word64(0xcbbb9d5d, 0xc1059ed8); + h1 = new Word64(0x629a292a, 0x367cd507); + h2 = new Word64(0x9159015a, 0x3070dd17); + h3 = new Word64(0x152fecd8, 0xf70e5939); + h4 = new Word64(0x67332667, 0xffc00b31); + h5 = new Word64(0x8eb44a87, 0x68581511); + h6 = new Word64(0xdb0c2e0d, 0x64f98fa7); + h7 = new Word64(0x47b5481d, 0xbefa4fa4); + } + + // pre-processing + var paddedLength = Math.ceil((length + 17) / 128) * 128; + var padded = new Uint8Array(paddedLength); + var i, j, n; + for (i = 0; i < length; ++i) { + padded[i] = data[offset++]; + } + padded[i++] = 0x80; + n = paddedLength - 16; + while (i < n) { + padded[i++] = 0; + } + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = 0; + padded[i++] = (length >>> 29) & 0xFF; + padded[i++] = (length >> 21) & 0xFF; + padded[i++] = (length >> 13) & 0xFF; + padded[i++] = (length >> 5) & 0xFF; + padded[i++] = (length << 3) & 0xFF; + + var w = new Array(80); + for (i = 0; i < 80; i++) { + w[i] = new Word64(0, 0); + } + var a = new Word64(0, 0), b = new Word64(0, 0), c = new Word64(0, 0); + var d = new Word64(0, 0), e = new Word64(0, 0), f = new Word64(0, 0); + var g = new Word64(0, 0), h = new Word64(0, 0); + var t1 = new Word64(0, 0), t2 = new Word64(0, 0); + var tmp1 = new Word64(0, 0), tmp2 = new Word64(0, 0), tmp3; + + // for each 1024 bit block + for (i = 0; i < paddedLength;) { + for (j = 0; j < 16; ++j) { + w[j].high = (padded[i] << 24) | (padded[i + 1] << 16) | + (padded[i + 2] << 8) | (padded[i + 3]); + w[j].low = (padded[i + 4]) << 24 | (padded[i + 5]) << 16 | + (padded[i + 6]) << 8 | (padded[i + 7]); + i += 8; + } + for (j = 16; j < 80; ++j) { + tmp3 = w[j]; + littleSigmaPrime(tmp3, w[j - 2], tmp2); + tmp3.add(w[j - 7]); + littleSigma(tmp1, w[j - 15], tmp2); + tmp3.add(tmp1); + tmp3.add(w[j - 16]); + } + + a.assign(h0); b.assign(h1); c.assign(h2); d.assign(h3); + e.assign(h4); f.assign(h5); g.assign(h6); h.assign(h7); + for (j = 0; j < 80; ++j) { + t1.assign(h); + sigmaPrime(tmp1, e, tmp2); + t1.add(tmp1); + ch(tmp1, e, f, g, tmp2); + t1.add(tmp1); + t1.add(k[j]); + t1.add(w[j]); + + sigma(t2, a, tmp2); + maj(tmp1, a, b, c, tmp2); + t2.add(tmp1); + + tmp3 = h; + h = g; + g = f; + f = e; + d.add(t1); + e = d; + d = c; + c = b; + b = a; + tmp3.assign(t1); + tmp3.add(t2); + a = tmp3; + } + h0.add(a); + h1.add(b); + h2.add(c); + h3.add(d); + h4.add(e); + h5.add(f); + h6.add(g); + h7.add(h); + } + + var result; + if (!mode384) { + result = new Uint8Array(64); + h0.copyTo(result,0); + h1.copyTo(result,8); + h2.copyTo(result,16); + h3.copyTo(result,24); + h4.copyTo(result,32); + h5.copyTo(result,40); + h6.copyTo(result,48); + h7.copyTo(result,56); + } + else { + result = new Uint8Array(48); + h0.copyTo(result,0); + h1.copyTo(result,8); + h2.copyTo(result,16); + h3.copyTo(result,24); + h4.copyTo(result,32); + h5.copyTo(result,40); + } + return result; + } + + return hash; +})(); +var calculateSHA384 = (function calculateSHA384Closure() { + function hash(data, offset, length) { + return calculateSHA512(data, offset, length, true); + } + + return hash; +})(); +var NullCipher = (function NullCipherClosure() { + function NullCipher() { + } + + NullCipher.prototype = { + decryptBlock: function NullCipher_decryptBlock(data) { + return data; + } + }; + + return NullCipher; +})(); + +var AES128Cipher = (function AES128CipherClosure() { + var rcon = new Uint8Array([ + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, + 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, + 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, + 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, + 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, + 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, + 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, + 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, + 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, + 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, + 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, + 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, + 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, + 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, + 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, + 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, + 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, + 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, + 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, + 0x74, 0xe8, 0xcb, 0x8d]); + + var s = new Uint8Array([ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, + 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, + 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, + 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, + 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, + 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, + 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, + 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, + 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, + 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, + 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, + 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, + 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, + 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, + 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, + 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, + 0xb0, 0x54, 0xbb, 0x16]); + + var inv_s = new Uint8Array([ + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, + 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, + 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, + 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, + 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, + 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, + 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, + 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, + 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, + 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, + 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, + 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, + 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, + 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, + 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, + 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, + 0x55, 0x21, 0x0c, 0x7d]); + var mixCol = new Uint8Array(256); + for (var i = 0; i < 256; i++) { + if (i < 128) { + mixCol[i] = i << 1; + } else { + mixCol[i] = (i << 1) ^ 0x1b; + } + } + var mix = new Uint32Array([ + 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, + 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, + 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, + 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, + 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, + 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, + 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, + 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, + 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, + 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, + 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, + 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, + 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, + 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, + 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, + 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, + 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, + 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, + 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, + 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, + 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, + 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, + 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, + 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, + 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, + 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, + 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, + 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, + 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, + 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, + 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, + 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, + 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, + 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, + 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, + 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, + 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, + 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, + 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, + 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, + 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, + 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, + 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3]); + + function expandKey128(cipherKey) { + var b = 176, result = new Uint8Array(b); + result.set(cipherKey); + for (var j = 16, i = 1; j < b; ++i) { + // RotWord + var t1 = result[j - 3], t2 = result[j - 2], + t3 = result[j - 1], t4 = result[j - 4]; + // SubWord + t1 = s[t1]; + t2 = s[t2]; + t3 = s[t3]; + t4 = s[t4]; + // Rcon + t1 = t1 ^ rcon[i]; + for (var n = 0; n < 4; ++n) { + result[j] = (t1 ^= result[j - 16]); + j++; + result[j] = (t2 ^= result[j - 16]); + j++; + result[j] = (t3 ^= result[j - 16]); + j++; + result[j] = (t4 ^= result[j - 16]); + j++; + } + } + return result; + } + + function decrypt128(input, key) { + var state = new Uint8Array(16); + state.set(input); + var i, j, k; + var t, u, v; + // AddRoundKey + for (j = 0, k = 160; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + for (i = 9; i >= 1; --i) { + // InvShiftRows + t = state[13]; + state[13] = state[9]; + state[9] = state[5]; + state[5] = state[1]; + state[1] = t; + t = state[14]; + u = state[10]; + state[14] = state[6]; + state[10] = state[2]; + state[6] = t; + state[2] = u; + t = state[15]; + u = state[11]; + v = state[7]; + state[15] = state[3]; + state[11] = t; + state[7] = u; + state[3] = v; + // InvSubBytes + for (j = 0; j < 16; ++j) { + state[j] = inv_s[state[j]]; + } + // AddRoundKey + for (j = 0, k = i * 16; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + // InvMixColumns + for (j = 0; j < 16; j += 4) { + var s0 = mix[state[j]], s1 = mix[state[j + 1]], + s2 = mix[state[j + 2]], s3 = mix[state[j + 3]]; + t = (s0 ^ (s1 >>> 8) ^ (s1 << 24) ^ (s2 >>> 16) ^ (s2 << 16) ^ + (s3 >>> 24) ^ (s3 << 8)); + state[j] = (t >>> 24) & 0xFF; + state[j + 1] = (t >> 16) & 0xFF; + state[j + 2] = (t >> 8) & 0xFF; + state[j + 3] = t & 0xFF; + } + } + // InvShiftRows + t = state[13]; + state[13] = state[9]; + state[9] = state[5]; + state[5] = state[1]; + state[1] = t; + t = state[14]; + u = state[10]; + state[14] = state[6]; + state[10] = state[2]; + state[6] = t; + state[2] = u; + t = state[15]; + u = state[11]; + v = state[7]; + state[15] = state[3]; + state[11] = t; + state[7] = u; + state[3] = v; + for (j = 0; j < 16; ++j) { + // InvSubBytes + state[j] = inv_s[state[j]]; + // AddRoundKey + state[j] ^= key[j]; + } + return state; + } + + function encrypt128(input, key) { + var t, u, v, k; + var state = new Uint8Array(16); + state.set(input); + for (j = 0; j < 16; ++j) { + // AddRoundKey + state[j] ^= key[j]; + } + + for (i = 1; i < 10; i++) { + //SubBytes + for (j = 0; j < 16; ++j) { + state[j] = s[state[j]]; + } + //ShiftRows + v = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = v; + v = state[2]; + u = state[6]; + state[2] = state[10]; + state[6] = state[14]; + state[10] = v; + state[14] = u; + v = state[3]; + u = state[7]; + t = state[11]; + state[3] = state[15]; + state[7] = v; + state[11] = u; + state[15] = t; + //MixColumns + for (var j = 0; j < 16; j += 4) { + var s0 = state[j + 0], s1 = state[j + 1]; + var s2 = state[j + 2], s3 = state[j + 3]; + t = s0 ^ s1 ^ s2 ^ s3; + state[j + 0] ^= t ^ mixCol[s0 ^ s1]; + state[j + 1] ^= t ^ mixCol[s1 ^ s2]; + state[j + 2] ^= t ^ mixCol[s2 ^ s3]; + state[j + 3] ^= t ^ mixCol[s3 ^ s0]; + } + //AddRoundKey + for (j = 0, k = i * 16; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + } + + //SubBytes + for (j = 0; j < 16; ++j) { + state[j] = s[state[j]]; + } + //ShiftRows + v = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = v; + v = state[2]; + u = state[6]; + state[2] = state[10]; + state[6] = state[14]; + state[10] = v; + state[14] = u; + v = state[3]; + u = state[7]; + t = state[11]; + state[3] = state[15]; + state[7] = v; + state[11] = u; + state[15] = t; + //AddRoundKey + for (j = 0, k = 160; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + return state; + } + + function AES128Cipher(key) { + this.key = expandKey128(key); + this.buffer = new Uint8Array(16); + this.bufferPosition = 0; + } + + function decryptBlock2(data, finalize) { + var i, j, ii, sourceLength = data.length, + buffer = this.buffer, bufferLength = this.bufferPosition, + result = [], iv = this.iv; + for (i = 0; i < sourceLength; ++i) { + buffer[bufferLength] = data[i]; + ++bufferLength; + if (bufferLength < 16) { + continue; + } + // buffer is full, decrypting + var plain = decrypt128(buffer, this.key); + // xor-ing the IV vector to get plain text + for (j = 0; j < 16; ++j) { + plain[j] ^= iv[j]; + } + iv = buffer; + result.push(plain); + buffer = new Uint8Array(16); + bufferLength = 0; + } + // saving incomplete buffer + this.buffer = buffer; + this.bufferLength = bufferLength; + this.iv = iv; + if (result.length === 0) { + return new Uint8Array([]); + } + // combining plain text blocks into one + var outputLength = 16 * result.length; + if (finalize) { + // undo a padding that is described in RFC 2898 + var lastBlock = result[result.length - 1]; + var psLen = lastBlock[15]; + if (psLen <= 16) { + for (i = 15, ii = 16 - psLen; i >= ii; --i) { + if (lastBlock[i] !== psLen) { + // Invalid padding, assume that the block has no padding. + psLen = 0; + break; + } + } + outputLength -= psLen; + result[result.length - 1] = lastBlock.subarray(0, 16 - psLen); + } + } + var output = new Uint8Array(outputLength); + for (i = 0, j = 0, ii = result.length; i < ii; ++i, j += 16) { + output.set(result[i], j); + } + return output; + } + + AES128Cipher.prototype = { + decryptBlock: function AES128Cipher_decryptBlock(data, finalize) { + var i, sourceLength = data.length; + var buffer = this.buffer, bufferLength = this.bufferPosition; + // waiting for IV values -- they are at the start of the stream + for (i = 0; bufferLength < 16 && i < sourceLength; ++i, ++bufferLength) { + buffer[bufferLength] = data[i]; + } + if (bufferLength < 16) { + // need more data + this.bufferLength = bufferLength; + return new Uint8Array([]); + } + this.iv = buffer; + this.buffer = new Uint8Array(16); + this.bufferLength = 0; + // starting decryption + this.decryptBlock = decryptBlock2; + return this.decryptBlock(data.subarray(16), finalize); + }, + encrypt: function AES128Cipher_encrypt(data, iv) { + var i, j, ii, sourceLength = data.length, + buffer = this.buffer, bufferLength = this.bufferPosition, + result = []; + if (!iv) { + iv = new Uint8Array(16); + } + for (i = 0; i < sourceLength; ++i) { + buffer[bufferLength] = data[i]; + ++bufferLength; + if (bufferLength < 16) { + continue; + } + for (j = 0; j < 16; ++j) { + buffer[j] ^= iv[j]; + } + + // buffer is full, encrypting + var cipher = encrypt128(buffer, this.key); + iv = cipher; + result.push(cipher); + buffer = new Uint8Array(16); + bufferLength = 0; + } + // saving incomplete buffer + this.buffer = buffer; + this.bufferLength = bufferLength; + this.iv = iv; + if (result.length === 0) { + return new Uint8Array([]); + } + // combining plain text blocks into one + var outputLength = 16 * result.length; + var output = new Uint8Array(outputLength); + for (i = 0, j = 0, ii = result.length; i < ii; ++i, j += 16) { + output.set(result[i], j); + } + return output; + } + }; + + return AES128Cipher; +})(); + +var AES256Cipher = (function AES256CipherClosure() { + var rcon = new Uint8Array([ + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, + 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, + 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, + 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, + 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, + 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, + 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, + 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, + 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, + 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, + 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, + 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, + 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, + 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, + 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, + 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, + 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, + 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, + 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, + 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, + 0x74, 0xe8, 0xcb, 0x8d]); + + var s = new Uint8Array([ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, + 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, + 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, + 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, + 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, + 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, + 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, + 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, + 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, + 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, + 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, + 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, + 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, + 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, + 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, + 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, + 0xb0, 0x54, 0xbb, 0x16]); + + var inv_s = new Uint8Array([ + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, + 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, + 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, + 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, + 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, + 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, + 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, + 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, + 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, + 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, + 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, + 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, + 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, + 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, + 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, + 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, + 0x55, 0x21, 0x0c, 0x7d]); + + var mixCol = new Uint8Array(256); + for (var i = 0; i < 256; i++) { + if (i < 128) { + mixCol[i] = i << 1; + } else { + mixCol[i] = (i << 1) ^ 0x1b; + } + } + var mix = new Uint32Array([ + 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, + 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, + 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, + 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, + 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, + 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, + 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, + 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, + 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, + 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, + 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, + 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, + 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, + 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, + 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, + 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, + 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, + 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, + 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, + 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, + 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, + 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, + 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, + 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, + 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, + 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, + 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, + 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, + 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, + 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, + 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, + 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, + 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, + 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, + 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, + 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, + 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, + 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, + 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, + 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, + 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, + 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, + 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3]); + + function expandKey256(cipherKey) { + var b = 240, result = new Uint8Array(b); + var r = 1; + + result.set(cipherKey); + for (var j = 32, i = 1; j < b; ++i) { + if (j % 32 === 16) { + t1 = s[t1]; + t2 = s[t2]; + t3 = s[t3]; + t4 = s[t4]; + } else if (j % 32 === 0) { + // RotWord + var t1 = result[j - 3], t2 = result[j - 2], + t3 = result[j - 1], t4 = result[j - 4]; + // SubWord + t1 = s[t1]; + t2 = s[t2]; + t3 = s[t3]; + t4 = s[t4]; + // Rcon + t1 = t1 ^ r; + if ((r <<= 1) >= 256) { + r = (r ^ 0x1b) & 0xFF; + } + } + + for (var n = 0; n < 4; ++n) { + result[j] = (t1 ^= result[j - 32]); + j++; + result[j] = (t2 ^= result[j - 32]); + j++; + result[j] = (t3 ^= result[j - 32]); + j++; + result[j] = (t4 ^= result[j - 32]); + j++; + } + } + return result; + } + + function decrypt256(input, key) { + var state = new Uint8Array(16); + state.set(input); + var i, j, k; + var t, u, v; + // AddRoundKey + for (j = 0, k = 224; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + for (i = 13; i >= 1; --i) { + // InvShiftRows + t = state[13]; + state[13] = state[9]; + state[9] = state[5]; + state[5] = state[1]; + state[1] = t; + t = state[14]; + u = state[10]; + state[14] = state[6]; + state[10] = state[2]; + state[6] = t; + state[2] = u; + t = state[15]; + u = state[11]; + v = state[7]; + state[15] = state[3]; + state[11] = t; + state[7] = u; + state[3] = v; + // InvSubBytes + for (j = 0; j < 16; ++j) { + state[j] = inv_s[state[j]]; + } + // AddRoundKey + for (j = 0, k = i * 16; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + // InvMixColumns + for (j = 0; j < 16; j += 4) { + var s0 = mix[state[j]], s1 = mix[state[j + 1]], + s2 = mix[state[j + 2]], s3 = mix[state[j + 3]]; + t = (s0 ^ (s1 >>> 8) ^ (s1 << 24) ^ (s2 >>> 16) ^ (s2 << 16) ^ + (s3 >>> 24) ^ (s3 << 8)); + state[j] = (t >>> 24) & 0xFF; + state[j + 1] = (t >> 16) & 0xFF; + state[j + 2] = (t >> 8) & 0xFF; + state[j + 3] = t & 0xFF; + } + } + // InvShiftRows + t = state[13]; + state[13] = state[9]; + state[9] = state[5]; + state[5] = state[1]; + state[1] = t; + t = state[14]; + u = state[10]; + state[14] = state[6]; + state[10] = state[2]; + state[6] = t; + state[2] = u; + t = state[15]; + u = state[11]; + v = state[7]; + state[15] = state[3]; + state[11] = t; + state[7] = u; + state[3] = v; + for (j = 0; j < 16; ++j) { + // InvSubBytes + state[j] = inv_s[state[j]]; + // AddRoundKey + state[j] ^= key[j]; + } + return state; + } + + function encrypt256(input, key) { + var t, u, v, k; + var state = new Uint8Array(16); + state.set(input); + for (j = 0; j < 16; ++j) { + // AddRoundKey + state[j] ^= key[j]; + } + + for (i = 1; i < 14; i++) { + //SubBytes + for (j = 0; j < 16; ++j) { + state[j] = s[state[j]]; + } + //ShiftRows + v = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = v; + v = state[2]; + u = state[6]; + state[2] = state[10]; + state[6] = state[14]; + state[10] = v; + state[14] = u; + v = state[3]; + u = state[7]; + t = state[11]; + state[3] = state[15]; + state[7] = v; + state[11] = u; + state[15] = t; + //MixColumns + for (var j = 0; j < 16; j += 4) { + var s0 = state[j + 0], s1 = state[j + 1]; + var s2 = state[j + 2], s3 = state[j + 3]; + t = s0 ^ s1 ^ s2 ^ s3; + state[j + 0] ^= t ^ mixCol[s0 ^ s1]; + state[j + 1] ^= t ^ mixCol[s1 ^ s2]; + state[j + 2] ^= t ^ mixCol[s2 ^ s3]; + state[j + 3] ^= t ^ mixCol[s3 ^ s0]; + } + //AddRoundKey + for (j = 0, k = i * 16; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + } + + //SubBytes + for (j = 0; j < 16; ++j) { + state[j] = s[state[j]]; + } + //ShiftRows + v = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = v; + v = state[2]; + u = state[6]; + state[2] = state[10]; + state[6] = state[14]; + state[10] = v; + state[14] = u; + v = state[3]; + u = state[7]; + t = state[11]; + state[3] = state[15]; + state[7] = v; + state[11] = u; + state[15] = t; + //AddRoundKey + for (j = 0, k = 224; j < 16; ++j, ++k) { + state[j] ^= key[k]; + } + + return state; + + } + + function AES256Cipher(key) { + this.key = expandKey256(key); + this.buffer = new Uint8Array(16); + this.bufferPosition = 0; + } + + function decryptBlock2(data, finalize) { + var i, j, ii, sourceLength = data.length, + buffer = this.buffer, bufferLength = this.bufferPosition, + result = [], iv = this.iv; + + for (i = 0; i < sourceLength; ++i) { + buffer[bufferLength] = data[i]; + ++bufferLength; + if (bufferLength < 16) { + continue; + } + // buffer is full, decrypting + var plain = decrypt256(buffer, this.key); + // xor-ing the IV vector to get plain text + for (j = 0; j < 16; ++j) { + plain[j] ^= iv[j]; + } + iv = buffer; + result.push(plain); + buffer = new Uint8Array(16); + bufferLength = 0; + } + // saving incomplete buffer + this.buffer = buffer; + this.bufferLength = bufferLength; + this.iv = iv; + if (result.length === 0) { + return new Uint8Array([]); + } + // combining plain text blocks into one + var outputLength = 16 * result.length; + if (finalize) { + // undo a padding that is described in RFC 2898 + var lastBlock = result[result.length - 1]; + var psLen = lastBlock[15]; + if (psLen <= 16) { + for (i = 15, ii = 16 - psLen; i >= ii; --i) { + if (lastBlock[i] !== psLen) { + // Invalid padding, assume that the block has no padding. + psLen = 0; + break; + } + } + outputLength -= psLen; + result[result.length - 1] = lastBlock.subarray(0, 16 - psLen); + } + } + var output = new Uint8Array(outputLength); + for (i = 0, j = 0, ii = result.length; i < ii; ++i, j += 16) { + output.set(result[i], j); + } + return output; + + } + + AES256Cipher.prototype = { + decryptBlock: function AES256Cipher_decryptBlock(data, finalize, iv) { + var i, sourceLength = data.length; + var buffer = this.buffer, bufferLength = this.bufferPosition; + // if not supplied an IV wait for IV values + // they are at the start of the stream + if (iv) { + this.iv = iv; + } else { + for (i = 0; bufferLength < 16 && + i < sourceLength; ++i, ++bufferLength) { + buffer[bufferLength] = data[i]; + } + if (bufferLength < 16) { + //need more data + this.bufferLength = bufferLength; + return new Uint8Array([]); + } + this.iv = buffer; + data = data.subarray(16); + } + this.buffer = new Uint8Array(16); + this.bufferLength = 0; + // starting decryption + this.decryptBlock = decryptBlock2; + return this.decryptBlock(data, finalize); + }, + encrypt: function AES256Cipher_encrypt(data, iv) { + var i, j, ii, sourceLength = data.length, + buffer = this.buffer, bufferLength = this.bufferPosition, + result = []; + if (!iv) { + iv = new Uint8Array(16); + } + for (i = 0; i < sourceLength; ++i) { + buffer[bufferLength] = data[i]; + ++bufferLength; + if (bufferLength < 16) { + continue; + } + for (j = 0; j < 16; ++j) { + buffer[j] ^= iv[j]; + } + + // buffer is full, encrypting + var cipher = encrypt256(buffer, this.key); + this.iv = cipher; + result.push(cipher); + buffer = new Uint8Array(16); + bufferLength = 0; + } + // saving incomplete buffer + this.buffer = buffer; + this.bufferLength = bufferLength; + this.iv = iv; + if (result.length === 0) { + return new Uint8Array([]); + } + // combining plain text blocks into one + var outputLength = 16 * result.length; + var output = new Uint8Array(outputLength); + for (i = 0, j = 0, ii = result.length; i < ii; ++i, j += 16) { + output.set(result[i], j); + } + return output; + } + }; + + return AES256Cipher; +})(); + +var PDF17 = (function PDF17Closure() { + + function compareByteArrays(array1, array2) { + if (array1.length !== array2.length) { + return false; + } + for (var i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; + } + + function PDF17() { + } + + PDF17.prototype = { + checkOwnerPassword: function PDF17_checkOwnerPassword(password, + ownerValidationSalt, + userBytes, + ownerPassword) { + var hashData = new Uint8Array(password.length + 56); + hashData.set(password, 0); + hashData.set(ownerValidationSalt, password.length); + hashData.set(userBytes, password.length + ownerValidationSalt.length); + var result = calculateSHA256(hashData, 0, hashData.length); + return compareByteArrays(result, ownerPassword); + }, + checkUserPassword: function PDF17_checkUserPassword(password, + userValidationSalt, + userPassword) { + var hashData = new Uint8Array(password.length + 8); + hashData.set(password, 0); + hashData.set(userValidationSalt, password.length); + var result = calculateSHA256(hashData, 0, hashData.length); + return compareByteArrays(result, userPassword); + }, + getOwnerKey: function PDF17_getOwnerKey(password, ownerKeySalt, userBytes, + ownerEncryption) { + var hashData = new Uint8Array(password.length + 56); + hashData.set(password, 0); + hashData.set(ownerKeySalt, password.length); + hashData.set(userBytes, password.length + ownerKeySalt.length); + var key = calculateSHA256(hashData, 0, hashData.length); + var cipher = new AES256Cipher(key); + return cipher.decryptBlock(ownerEncryption, + false, + new Uint8Array(16)); + + }, + getUserKey: function PDF17_getUserKey(password, userKeySalt, + userEncryption) { + var hashData = new Uint8Array(password.length + 8); + hashData.set(password, 0); + hashData.set(userKeySalt, password.length); + //key is the decryption key for the UE string + var key = calculateSHA256(hashData, 0, hashData.length); + var cipher = new AES256Cipher(key); + return cipher.decryptBlock(userEncryption, + false, + new Uint8Array(16)); + } + }; + return PDF17; +})(); + +var PDF20 = (function PDF20Closure() { + + function concatArrays(array1, array2) { + var t = new Uint8Array(array1.length + array2.length); + t.set(array1, 0); + t.set(array2, array1.length); + return t; + } + + function calculatePDF20Hash(password, input, userBytes) { + //This refers to Algorithm 2.B as defined in ISO 32000-2 + var k = calculateSHA256(input, 0, input.length).subarray(0, 32); + var e = [0]; + var i = 0; + while (i < 64 || e[e.length - 1] > i - 32) { + var arrayLength = password.length + k.length + userBytes.length; + + var k1 = new Uint8Array(arrayLength * 64); + var array = concatArrays(password, k); + array = concatArrays(array, userBytes); + for (var j = 0, pos = 0; j < 64; j++, pos += arrayLength) { + k1.set(array, pos); + } + //AES128 CBC NO PADDING with + //first 16 bytes of k as the key and the second 16 as the iv. + var cipher = new AES128Cipher(k.subarray(0, 16)); + e = cipher.encrypt(k1, k.subarray(16, 32)); + //Now we have to take the first 16 bytes of an unsigned + //big endian integer... and compute the remainder + //modulo 3.... That is a fairly large number and + //JavaScript isn't going to handle that well... + //So we're using a trick that allows us to perform + //modulo math byte by byte + var remainder = 0; + for (var z = 0; z < 16; z++) { + remainder *= (256 % 3); + remainder %= 3; + remainder += ((e[z] >>> 0) % 3); + remainder %= 3; + } + if (remainder === 0) { + k = calculateSHA256(e, 0, e.length); + } + else if (remainder === 1) { + k = calculateSHA384(e, 0, e.length); + } + else if (remainder === 2) { + k = calculateSHA512(e, 0, e.length); + } + i++; + } + return k.subarray(0, 32); + } + + function PDF20() { + } + + function compareByteArrays(array1, array2) { + if (array1.length !== array2.length) { + return false; + } + for (var i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; + } + + PDF20.prototype = { + hash: function PDF20_hash(password, concatBytes, userBytes) { + return calculatePDF20Hash(password, concatBytes, userBytes); + }, + checkOwnerPassword: function PDF20_checkOwnerPassword(password, + ownerValidationSalt, + userBytes, + ownerPassword) { + var hashData = new Uint8Array(password.length + 56); + hashData.set(password, 0); + hashData.set(ownerValidationSalt, password.length); + hashData.set(userBytes, password.length + ownerValidationSalt.length); + var result = calculatePDF20Hash(password, hashData, userBytes); + return compareByteArrays(result, ownerPassword); + }, + checkUserPassword: function PDF20_checkUserPassword(password, + userValidationSalt, + userPassword) { + var hashData = new Uint8Array(password.length + 8); + hashData.set(password, 0); + hashData.set(userValidationSalt, password.length); + var result = calculatePDF20Hash(password, hashData, []); + return compareByteArrays(result, userPassword); + }, + getOwnerKey: function PDF20_getOwnerKey(password, ownerKeySalt, userBytes, + ownerEncryption) { + var hashData = new Uint8Array(password.length + 56); + hashData.set(password, 0); + hashData.set(ownerKeySalt, password.length); + hashData.set(userBytes, password.length + ownerKeySalt.length); + var key = calculatePDF20Hash(password, hashData, userBytes); + var cipher = new AES256Cipher(key); + return cipher.decryptBlock(ownerEncryption, + false, + new Uint8Array(16)); + + }, + getUserKey: function PDF20_getUserKey(password, userKeySalt, + userEncryption) { + var hashData = new Uint8Array(password.length + 8); + hashData.set(password, 0); + hashData.set(userKeySalt, password.length); + //key is the decryption key for the UE string + var key = calculatePDF20Hash(password, hashData, []); + var cipher = new AES256Cipher(key); + return cipher.decryptBlock(userEncryption, + false, + new Uint8Array(16)); + } + }; + return PDF20; +})(); + +var CipherTransform = (function CipherTransformClosure() { + function CipherTransform(stringCipherConstructor, streamCipherConstructor) { + this.stringCipherConstructor = stringCipherConstructor; + this.streamCipherConstructor = streamCipherConstructor; + } + + CipherTransform.prototype = { + createStream: function CipherTransform_createStream(stream, length) { + var cipher = new this.streamCipherConstructor(); + return new DecryptStream(stream, length, + function cipherTransformDecryptStream(data, finalize) { + return cipher.decryptBlock(data, finalize); + } + ); + }, + decryptString: function CipherTransform_decryptString(s) { + var cipher = new this.stringCipherConstructor(); + var data = stringToBytes(s); + data = cipher.decryptBlock(data, true); + return bytesToString(data); + } + }; + return CipherTransform; +})(); + +var CipherTransformFactory = (function CipherTransformFactoryClosure() { + var defaultPasswordBytes = new Uint8Array([ + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, + 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, + 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]); + + function createEncryptionKey20(revision, password, ownerPassword, + ownerValidationSalt, ownerKeySalt, uBytes, + userPassword, userValidationSalt, userKeySalt, + ownerEncryption, userEncryption, perms) { + if (password) { + var passwordLength = Math.min(127, password.length); + password = password.subarray(0, passwordLength); + } else { + password = []; + } + var pdfAlgorithm; + if (revision === 6) { + pdfAlgorithm = new PDF20(); + } else { + pdfAlgorithm = new PDF17(); + } + + if (pdfAlgorithm) { + if (pdfAlgorithm.checkUserPassword(password, userValidationSalt, + userPassword)) { + return pdfAlgorithm.getUserKey(password, userKeySalt, userEncryption); + } else if (pdfAlgorithm.checkOwnerPassword(password, ownerValidationSalt, + uBytes, + ownerPassword)) { + return pdfAlgorithm.getOwnerKey(password, ownerKeySalt, uBytes, + ownerEncryption); + } + } + + return null; + } + + function prepareKeyData(fileId, password, ownerPassword, userPassword, + flags, revision, keyLength, encryptMetadata) { + var hashDataSize = 40 + ownerPassword.length + fileId.length; + var hashData = new Uint8Array(hashDataSize), i = 0, j, n; + if (password) { + n = Math.min(32, password.length); + for (; i < n; ++i) { + hashData[i] = password[i]; + } + } + j = 0; + while (i < 32) { + hashData[i++] = defaultPasswordBytes[j++]; + } + // as now the padded password in the hashData[0..i] + for (j = 0, n = ownerPassword.length; j < n; ++j) { + hashData[i++] = ownerPassword[j]; + } + hashData[i++] = flags & 0xFF; + hashData[i++] = (flags >> 8) & 0xFF; + hashData[i++] = (flags >> 16) & 0xFF; + hashData[i++] = (flags >>> 24) & 0xFF; + for (j = 0, n = fileId.length; j < n; ++j) { + hashData[i++] = fileId[j]; + } + if (revision >= 4 && !encryptMetadata) { + hashData[i++] = 0xFF; + hashData[i++] = 0xFF; + hashData[i++] = 0xFF; + hashData[i++] = 0xFF; + } + var hash = calculateMD5(hashData, 0, i); + var keyLengthInBytes = keyLength >> 3; + if (revision >= 3) { + for (j = 0; j < 50; ++j) { + hash = calculateMD5(hash, 0, keyLengthInBytes); + } + } + var encryptionKey = hash.subarray(0, keyLengthInBytes); + var cipher, checkData; + + if (revision >= 3) { + for (i = 0; i < 32; ++i) { + hashData[i] = defaultPasswordBytes[i]; + } + for (j = 0, n = fileId.length; j < n; ++j) { + hashData[i++] = fileId[j]; + } + cipher = new ARCFourCipher(encryptionKey); + checkData = cipher.encryptBlock(calculateMD5(hashData, 0, i)); + n = encryptionKey.length; + var derivedKey = new Uint8Array(n), k; + for (j = 1; j <= 19; ++j) { + for (k = 0; k < n; ++k) { + derivedKey[k] = encryptionKey[k] ^ j; + } + cipher = new ARCFourCipher(derivedKey); + checkData = cipher.encryptBlock(checkData); + } + for (j = 0, n = checkData.length; j < n; ++j) { + if (userPassword[j] !== checkData[j]) { + return null; + } + } + } else { + cipher = new ARCFourCipher(encryptionKey); + checkData = cipher.encryptBlock(defaultPasswordBytes); + for (j = 0, n = checkData.length; j < n; ++j) { + if (userPassword[j] !== checkData[j]) { + return null; + } + } + } + return encryptionKey; + } + + function decodeUserPassword(password, ownerPassword, revision, keyLength) { + var hashData = new Uint8Array(32), i = 0, j, n; + n = Math.min(32, password.length); + for (; i < n; ++i) { + hashData[i] = password[i]; + } + j = 0; + while (i < 32) { + hashData[i++] = defaultPasswordBytes[j++]; + } + var hash = calculateMD5(hashData, 0, i); + var keyLengthInBytes = keyLength >> 3; + if (revision >= 3) { + for (j = 0; j < 50; ++j) { + hash = calculateMD5(hash, 0, hash.length); + } + } + + var cipher, userPassword; + if (revision >= 3) { + userPassword = ownerPassword; + var derivedKey = new Uint8Array(keyLengthInBytes), k; + for (j = 19; j >= 0; j--) { + for (k = 0; k < keyLengthInBytes; ++k) { + derivedKey[k] = hash[k] ^ j; + } + cipher = new ARCFourCipher(derivedKey); + userPassword = cipher.encryptBlock(userPassword); + } + } else { + cipher = new ARCFourCipher(hash.subarray(0, keyLengthInBytes)); + userPassword = cipher.encryptBlock(ownerPassword); + } + return userPassword; + } + + var identityName = Name.get('Identity'); + + function CipherTransformFactory(dict, fileId, password) { + var filter = dict.get('Filter'); + if (!isName(filter) || filter.name !== 'Standard') { + error('unknown encryption method'); + } + this.dict = dict; + var algorithm = dict.get('V'); + if (!isInt(algorithm) || + (algorithm !== 1 && algorithm !== 2 && algorithm !== 4 && + algorithm !== 5)) { + error('unsupported encryption algorithm'); + } + this.algorithm = algorithm; + var keyLength = dict.get('Length') || 40; + if (!isInt(keyLength) || + keyLength < 40 || (keyLength % 8) !== 0) { + error('invalid key length'); + } + + // prepare keys + var ownerPassword = stringToBytes(dict.get('O')).subarray(0, 32); + var userPassword = stringToBytes(dict.get('U')).subarray(0, 32); + var flags = dict.get('P'); + var revision = dict.get('R'); + // meaningful when V is 4 or 5 + var encryptMetadata = ((algorithm === 4 || algorithm === 5) && + dict.get('EncryptMetadata') !== false); + this.encryptMetadata = encryptMetadata; + + var fileIdBytes = stringToBytes(fileId); + var passwordBytes; + if (password) { + if (revision === 6) { + try { + password = utf8StringToString(password); + } catch (ex) { + warn('CipherTransformFactory: ' + + 'Unable to convert UTF8 encoded password.'); + } + } + passwordBytes = stringToBytes(password); + } + + var encryptionKey; + if (algorithm !== 5) { + encryptionKey = prepareKeyData(fileIdBytes, passwordBytes, + ownerPassword, userPassword, flags, + revision, keyLength, encryptMetadata); + } + else { + var ownerValidationSalt = stringToBytes(dict.get('O')).subarray(32, 40); + var ownerKeySalt = stringToBytes(dict.get('O')).subarray(40, 48); + var uBytes = stringToBytes(dict.get('U')).subarray(0, 48); + var userValidationSalt = stringToBytes(dict.get('U')).subarray(32, 40); + var userKeySalt = stringToBytes(dict.get('U')).subarray(40, 48); + var ownerEncryption = stringToBytes(dict.get('OE')); + var userEncryption = stringToBytes(dict.get('UE')); + var perms = stringToBytes(dict.get('Perms')); + encryptionKey = + createEncryptionKey20(revision, passwordBytes, + ownerPassword, ownerValidationSalt, + ownerKeySalt, uBytes, + userPassword, userValidationSalt, + userKeySalt, ownerEncryption, + userEncryption, perms); + } + if (!encryptionKey && !password) { + throw new PasswordException('No password given', + PasswordResponses.NEED_PASSWORD); + } else if (!encryptionKey && password) { + // Attempting use the password as an owner password + var decodedPassword = decodeUserPassword(passwordBytes, ownerPassword, + revision, keyLength); + encryptionKey = prepareKeyData(fileIdBytes, decodedPassword, + ownerPassword, userPassword, flags, + revision, keyLength, encryptMetadata); + } + + if (!encryptionKey) { + throw new PasswordException('Incorrect Password', + PasswordResponses.INCORRECT_PASSWORD); + } + + this.encryptionKey = encryptionKey; + + if (algorithm >= 4) { + this.cf = dict.get('CF'); + this.stmf = dict.get('StmF') || identityName; + this.strf = dict.get('StrF') || identityName; + this.eff = dict.get('EFF') || this.stmf; + } + } + + function buildObjectKey(num, gen, encryptionKey, isAes) { + var key = new Uint8Array(encryptionKey.length + 9), i, n; + for (i = 0, n = encryptionKey.length; i < n; ++i) { + key[i] = encryptionKey[i]; + } + key[i++] = num & 0xFF; + key[i++] = (num >> 8) & 0xFF; + key[i++] = (num >> 16) & 0xFF; + key[i++] = gen & 0xFF; + key[i++] = (gen >> 8) & 0xFF; + if (isAes) { + key[i++] = 0x73; + key[i++] = 0x41; + key[i++] = 0x6C; + key[i++] = 0x54; + } + var hash = calculateMD5(key, 0, i); + return hash.subarray(0, Math.min(encryptionKey.length + 5, 16)); + } + + function buildCipherConstructor(cf, name, num, gen, key) { + var cryptFilter = cf.get(name.name); + var cfm; + if (cryptFilter !== null && cryptFilter !== undefined) { + cfm = cryptFilter.get('CFM'); + } + if (!cfm || cfm.name === 'None') { + return function cipherTransformFactoryBuildCipherConstructorNone() { + return new NullCipher(); + }; + } + if ('V2' === cfm.name) { + return function cipherTransformFactoryBuildCipherConstructorV2() { + return new ARCFourCipher(buildObjectKey(num, gen, key, false)); + }; + } + if ('AESV2' === cfm.name) { + return function cipherTransformFactoryBuildCipherConstructorAESV2() { + return new AES128Cipher(buildObjectKey(num, gen, key, true)); + }; + } + if ('AESV3' === cfm.name) { + return function cipherTransformFactoryBuildCipherConstructorAESV3() { + return new AES256Cipher(key); + }; + } + error('Unknown crypto method'); + } + + CipherTransformFactory.prototype = { + createCipherTransform: + function CipherTransformFactory_createCipherTransform(num, gen) { + if (this.algorithm === 4 || this.algorithm === 5) { + return new CipherTransform( + buildCipherConstructor(this.cf, this.stmf, + num, gen, this.encryptionKey), + buildCipherConstructor(this.cf, this.strf, + num, gen, this.encryptionKey)); + } + // algorithms 1 and 2 + var key = buildObjectKey(num, gen, this.encryptionKey, false); + var cipherConstructor = function buildCipherCipherConstructor() { + return new ARCFourCipher(key); + }; + return new CipherTransform(cipherConstructor, cipherConstructor); + } + }; + + return CipherTransformFactory; +})(); + + +var PatternType = { + FUNCTION_BASED: 1, + AXIAL: 2, + RADIAL: 3, + FREE_FORM_MESH: 4, + LATTICE_FORM_MESH: 5, + COONS_PATCH_MESH: 6, + TENSOR_PATCH_MESH: 7 +}; + +var Pattern = (function PatternClosure() { + // Constructor should define this.getPattern + function Pattern() { + error('should not call Pattern constructor'); + } + + Pattern.prototype = { + // Input: current Canvas context + // Output: the appropriate fillStyle or strokeStyle + getPattern: function Pattern_getPattern(ctx) { + error('Should not call Pattern.getStyle: ' + ctx); + } + }; + + Pattern.parseShading = function Pattern_parseShading(shading, matrix, xref, + res) { + + var dict = isStream(shading) ? shading.dict : shading; + var type = dict.get('ShadingType'); + + try { + switch (type) { + case PatternType.AXIAL: + case PatternType.RADIAL: + // Both radial and axial shadings are handled by RadialAxial shading. + return new Shadings.RadialAxial(dict, matrix, xref, res); + case PatternType.FREE_FORM_MESH: + case PatternType.LATTICE_FORM_MESH: + case PatternType.COONS_PATCH_MESH: + case PatternType.TENSOR_PATCH_MESH: + return new Shadings.Mesh(shading, matrix, xref, res); + default: + throw new Error('Unknown PatternType: ' + type); + } + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern); + warn(ex); + return new Shadings.Dummy(); + } + }; + return Pattern; +})(); + +var Shadings = {}; + +// A small number to offset the first/last color stops so we can insert ones to +// support extend. Number.MIN_VALUE appears to be too small and breaks the +// extend. 1e-7 works in FF but chrome seems to use an even smaller sized number +// internally so we have to go bigger. +Shadings.SMALL_NUMBER = 1e-2; + +// Radial and axial shading have very similar implementations +// If needed, the implementations can be broken into two classes +Shadings.RadialAxial = (function RadialAxialClosure() { + function RadialAxial(dict, matrix, xref, res) { + this.matrix = matrix; + this.coordsArr = dict.get('Coords'); + this.shadingType = dict.get('ShadingType'); + this.type = 'Pattern'; + var cs = dict.get('ColorSpace', 'CS'); + cs = ColorSpace.parse(cs, xref, res); + this.cs = cs; + + var t0 = 0.0, t1 = 1.0; + if (dict.has('Domain')) { + var domainArr = dict.get('Domain'); + t0 = domainArr[0]; + t1 = domainArr[1]; + } + + var extendStart = false, extendEnd = false; + if (dict.has('Extend')) { + var extendArr = dict.get('Extend'); + extendStart = extendArr[0]; + extendEnd = extendArr[1]; + } + + if (this.shadingType === PatternType.RADIAL && + (!extendStart || !extendEnd)) { + // Radial gradient only currently works if either circle is fully within + // the other circle. + var x1 = this.coordsArr[0]; + var y1 = this.coordsArr[1]; + var r1 = this.coordsArr[2]; + var x2 = this.coordsArr[3]; + var y2 = this.coordsArr[4]; + var r2 = this.coordsArr[5]; + var distance = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + if (r1 <= r2 + distance && + r2 <= r1 + distance) { + warn('Unsupported radial gradient.'); + } + } + + this.extendStart = extendStart; + this.extendEnd = extendEnd; + + var fnObj = dict.get('Function'); + var fn = PDFFunction.parseArray(xref, fnObj); + + // 10 samples seems good enough for now, but probably won't work + // if there are sharp color changes. Ideally, we would implement + // the spec faithfully and add lossless optimizations. + var diff = t1 - t0; + var step = diff / 10; + + var colorStops = this.colorStops = []; + + // Protect against bad domains so we don't end up in an infinte loop below. + if (t0 >= t1 || step <= 0) { + // Acrobat doesn't seem to handle these cases so we'll ignore for + // now. + info('Bad shading domain.'); + return; + } + + var color = new Float32Array(cs.numComps), ratio = new Float32Array(1); + var rgbColor; + for (var i = t0; i <= t1; i += step) { + ratio[0] = i; + fn(ratio, 0, color, 0); + rgbColor = cs.getRgb(color, 0); + var cssColor = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); + colorStops.push([(i - t0) / diff, cssColor]); + } + + var background = 'transparent'; + if (dict.has('Background')) { + rgbColor = cs.getRgb(dict.get('Background'), 0); + background = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); + } + + if (!extendStart) { + // Insert a color stop at the front and offset the first real color stop + // so it doesn't conflict with the one we insert. + colorStops.unshift([0, background]); + colorStops[1][0] += Shadings.SMALL_NUMBER; + } + if (!extendEnd) { + // Same idea as above in extendStart but for the end. + colorStops[colorStops.length - 1][0] -= Shadings.SMALL_NUMBER; + colorStops.push([1, background]); + } + + this.colorStops = colorStops; + } + + RadialAxial.prototype = { + getIR: function RadialAxial_getIR() { + var coordsArr = this.coordsArr; + var shadingType = this.shadingType; + var type, p0, p1, r0, r1; + if (shadingType === PatternType.AXIAL) { + p0 = [coordsArr[0], coordsArr[1]]; + p1 = [coordsArr[2], coordsArr[3]]; + r0 = null; + r1 = null; + type = 'axial'; + } else if (shadingType === PatternType.RADIAL) { + p0 = [coordsArr[0], coordsArr[1]]; + p1 = [coordsArr[3], coordsArr[4]]; + r0 = coordsArr[2]; + r1 = coordsArr[5]; + type = 'radial'; + } else { + error('getPattern type unknown: ' + shadingType); + } + + var matrix = this.matrix; + if (matrix) { + p0 = Util.applyTransform(p0, matrix); + p1 = Util.applyTransform(p1, matrix); + } + + return ['RadialAxial', type, this.colorStops, p0, p1, r0, r1]; + } + }; + + return RadialAxial; +})(); + +// All mesh shading. For now, they will be presented as set of the triangles +// to be drawn on the canvas and rgb color for each vertex. +Shadings.Mesh = (function MeshClosure() { + function MeshStreamReader(stream, context) { + this.stream = stream; + this.context = context; + this.buffer = 0; + this.bufferLength = 0; + + var numComps = context.numComps; + this.tmpCompsBuf = new Float32Array(numComps); + var csNumComps = context.colorSpace; + this.tmpCsCompsBuf = context.colorFn ? new Float32Array(csNumComps) : + this.tmpCompsBuf; + } + MeshStreamReader.prototype = { + get hasData() { + if (this.stream.end) { + return this.stream.pos < this.stream.end; + } + if (this.bufferLength > 0) { + return true; + } + var nextByte = this.stream.getByte(); + if (nextByte < 0) { + return false; + } + this.buffer = nextByte; + this.bufferLength = 8; + return true; + }, + readBits: function MeshStreamReader_readBits(n) { + var buffer = this.buffer; + var bufferLength = this.bufferLength; + if (n === 32) { + if (bufferLength === 0) { + return ((this.stream.getByte() << 24) | + (this.stream.getByte() << 16) | (this.stream.getByte() << 8) | + this.stream.getByte()) >>> 0; + } + buffer = (buffer << 24) | (this.stream.getByte() << 16) | + (this.stream.getByte() << 8) | this.stream.getByte(); + var nextByte = this.stream.getByte(); + this.buffer = nextByte & ((1 << bufferLength) - 1); + return ((buffer << (8 - bufferLength)) | + ((nextByte & 0xFF) >> bufferLength)) >>> 0; + } + if (n === 8 && bufferLength === 0) { + return this.stream.getByte(); + } + while (bufferLength < n) { + buffer = (buffer << 8) | this.stream.getByte(); + bufferLength += 8; + } + bufferLength -= n; + this.bufferLength = bufferLength; + this.buffer = buffer & ((1 << bufferLength) - 1); + return buffer >> bufferLength; + }, + align: function MeshStreamReader_align() { + this.buffer = 0; + this.bufferLength = 0; + }, + readFlag: function MeshStreamReader_readFlag() { + return this.readBits(this.context.bitsPerFlag); + }, + readCoordinate: function MeshStreamReader_readCoordinate() { + var bitsPerCoordinate = this.context.bitsPerCoordinate; + var xi = this.readBits(bitsPerCoordinate); + var yi = this.readBits(bitsPerCoordinate); + var decode = this.context.decode; + var scale = bitsPerCoordinate < 32 ? 1 / ((1 << bitsPerCoordinate) - 1) : + 2.3283064365386963e-10; // 2 ^ -32 + return [ + xi * scale * (decode[1] - decode[0]) + decode[0], + yi * scale * (decode[3] - decode[2]) + decode[2] + ]; + }, + readComponents: function MeshStreamReader_readComponents() { + var numComps = this.context.numComps; + var bitsPerComponent = this.context.bitsPerComponent; + var scale = bitsPerComponent < 32 ? 1 / ((1 << bitsPerComponent) - 1) : + 2.3283064365386963e-10; // 2 ^ -32 + var decode = this.context.decode; + var components = this.tmpCompsBuf; + for (var i = 0, j = 4; i < numComps; i++, j += 2) { + var ci = this.readBits(bitsPerComponent); + components[i] = ci * scale * (decode[j + 1] - decode[j]) + decode[j]; + } + var color = this.tmpCsCompsBuf; + if (this.context.colorFn) { + this.context.colorFn(components, 0, color, 0); + } + return this.context.colorSpace.getRgb(color, 0); + } + }; + + function decodeType4Shading(mesh, reader) { + var coords = mesh.coords; + var colors = mesh.colors; + var operators = []; + var ps = []; // not maintaining cs since that will match ps + var verticesLeft = 0; // assuming we have all data to start a new triangle + while (reader.hasData) { + var f = reader.readFlag(); + var coord = reader.readCoordinate(); + var color = reader.readComponents(); + if (verticesLeft === 0) { // ignoring flags if we started a triangle + assert(0 <= f && f <= 2, 'Unknown type4 flag'); + switch (f) { + case 0: + verticesLeft = 3; + break; + case 1: + ps.push(ps[ps.length - 2], ps[ps.length - 1]); + verticesLeft = 1; + break; + case 2: + ps.push(ps[ps.length - 3], ps[ps.length - 1]); + verticesLeft = 1; + break; + } + operators.push(f); + } + ps.push(coords.length); + coords.push(coord); + colors.push(color); + verticesLeft--; + + reader.align(); + } + + var psPacked = new Int32Array(ps); + + mesh.figures.push({ + type: 'triangles', + coords: psPacked, + colors: psPacked + }); + } + + function decodeType5Shading(mesh, reader, verticesPerRow) { + var coords = mesh.coords; + var colors = mesh.colors; + var ps = []; // not maintaining cs since that will match ps + while (reader.hasData) { + var coord = reader.readCoordinate(); + var color = reader.readComponents(); + ps.push(coords.length); + coords.push(coord); + colors.push(color); + } + + var psPacked = new Int32Array(ps); + + mesh.figures.push({ + type: 'lattice', + coords: psPacked, + colors: psPacked, + verticesPerRow: verticesPerRow + }); + } + + var MIN_SPLIT_PATCH_CHUNKS_AMOUNT = 3; + var MAX_SPLIT_PATCH_CHUNKS_AMOUNT = 20; + + var TRIANGLE_DENSITY = 20; // count of triangles per entire mesh bounds + + var getB = (function getBClosure() { + function buildB(count) { + var lut = []; + for (var i = 0; i <= count; i++) { + var t = i / count, t_ = 1 - t; + lut.push(new Float32Array([t_ * t_ * t_, 3 * t * t_ * t_, + 3 * t * t * t_, t * t * t])); + } + return lut; + } + var cache = []; + return function getB(count) { + if (!cache[count]) { + cache[count] = buildB(count); + } + return cache[count]; + }; + })(); + + function buildFigureFromPatch(mesh, index) { + var figure = mesh.figures[index]; + assert(figure.type === 'patch', 'Unexpected patch mesh figure'); + + var coords = mesh.coords, colors = mesh.colors; + var pi = figure.coords; + var ci = figure.colors; + + var figureMinX = Math.min(coords[pi[0]][0], coords[pi[3]][0], + coords[pi[12]][0], coords[pi[15]][0]); + var figureMinY = Math.min(coords[pi[0]][1], coords[pi[3]][1], + coords[pi[12]][1], coords[pi[15]][1]); + var figureMaxX = Math.max(coords[pi[0]][0], coords[pi[3]][0], + coords[pi[12]][0], coords[pi[15]][0]); + var figureMaxY = Math.max(coords[pi[0]][1], coords[pi[3]][1], + coords[pi[12]][1], coords[pi[15]][1]); + var splitXBy = Math.ceil((figureMaxX - figureMinX) * TRIANGLE_DENSITY / + (mesh.bounds[2] - mesh.bounds[0])); + splitXBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT, + Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitXBy)); + var splitYBy = Math.ceil((figureMaxY - figureMinY) * TRIANGLE_DENSITY / + (mesh.bounds[3] - mesh.bounds[1])); + splitYBy = Math.max(MIN_SPLIT_PATCH_CHUNKS_AMOUNT, + Math.min(MAX_SPLIT_PATCH_CHUNKS_AMOUNT, splitYBy)); + + var verticesPerRow = splitXBy + 1; + var figureCoords = new Int32Array((splitYBy + 1) * verticesPerRow); + var figureColors = new Int32Array((splitYBy + 1) * verticesPerRow); + var k = 0; + var cl = new Uint8Array(3), cr = new Uint8Array(3); + var c0 = colors[ci[0]], c1 = colors[ci[1]], + c2 = colors[ci[2]], c3 = colors[ci[3]]; + var bRow = getB(splitYBy), bCol = getB(splitXBy); + for (var row = 0; row <= splitYBy; row++) { + cl[0] = ((c0[0] * (splitYBy - row) + c2[0] * row) / splitYBy) | 0; + cl[1] = ((c0[1] * (splitYBy - row) + c2[1] * row) / splitYBy) | 0; + cl[2] = ((c0[2] * (splitYBy - row) + c2[2] * row) / splitYBy) | 0; + + cr[0] = ((c1[0] * (splitYBy - row) + c3[0] * row) / splitYBy) | 0; + cr[1] = ((c1[1] * (splitYBy - row) + c3[1] * row) / splitYBy) | 0; + cr[2] = ((c1[2] * (splitYBy - row) + c3[2] * row) / splitYBy) | 0; + + for (var col = 0; col <= splitXBy; col++, k++) { + if ((row === 0 || row === splitYBy) && + (col === 0 || col === splitXBy)) { + continue; + } + var x = 0, y = 0; + var q = 0; + for (var i = 0; i <= 3; i++) { + for (var j = 0; j <= 3; j++, q++) { + var m = bRow[row][i] * bCol[col][j]; + x += coords[pi[q]][0] * m; + y += coords[pi[q]][1] * m; + } + } + figureCoords[k] = coords.length; + coords.push([x, y]); + figureColors[k] = colors.length; + var newColor = new Uint8Array(3); + newColor[0] = ((cl[0] * (splitXBy - col) + cr[0] * col) / splitXBy) | 0; + newColor[1] = ((cl[1] * (splitXBy - col) + cr[1] * col) / splitXBy) | 0; + newColor[2] = ((cl[2] * (splitXBy - col) + cr[2] * col) / splitXBy) | 0; + colors.push(newColor); + } + } + figureCoords[0] = pi[0]; + figureColors[0] = ci[0]; + figureCoords[splitXBy] = pi[3]; + figureColors[splitXBy] = ci[1]; + figureCoords[verticesPerRow * splitYBy] = pi[12]; + figureColors[verticesPerRow * splitYBy] = ci[2]; + figureCoords[verticesPerRow * splitYBy + splitXBy] = pi[15]; + figureColors[verticesPerRow * splitYBy + splitXBy] = ci[3]; + + mesh.figures[index] = { + type: 'lattice', + coords: figureCoords, + colors: figureColors, + verticesPerRow: verticesPerRow + }; + } + + function decodeType6Shading(mesh, reader) { + // A special case of Type 7. The p11, p12, p21, p22 automatically filled + var coords = mesh.coords; + var colors = mesh.colors; + var ps = new Int32Array(16); // p00, p10, ..., p30, p01, ..., p33 + var cs = new Int32Array(4); // c00, c30, c03, c33 + while (reader.hasData) { + var f = reader.readFlag(); + assert(0 <= f && f <= 3, 'Unknown type6 flag'); + var i, ii; + var pi = coords.length; + for (i = 0, ii = (f !== 0 ? 8 : 12); i < ii; i++) { + coords.push(reader.readCoordinate()); + } + var ci = colors.length; + for (i = 0, ii = (f !== 0 ? 2 : 4); i < ii; i++) { + colors.push(reader.readComponents()); + } + var tmp1, tmp2, tmp3, tmp4; + switch (f) { + case 0: + ps[12] = pi + 3; ps[13] = pi + 4; ps[14] = pi + 5; ps[15] = pi + 6; + ps[ 8] = pi + 2; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 7; + ps[ 4] = pi + 1; /* calculated below */ ps[ 7] = pi + 8; + ps[ 0] = pi; ps[ 1] = pi + 11; ps[ 2] = pi + 10; ps[ 3] = pi + 9; + cs[2] = ci + 1; cs[3] = ci + 2; + cs[0] = ci; cs[1] = ci + 3; + break; + case 1: + tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15]; + ps[12] = pi + 5; ps[13] = pi + 4; ps[14] = pi + 3; ps[15] = pi + 2; + ps[ 8] = pi + 6; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 1; + ps[ 4] = pi + 7; /* calculated below */ ps[ 7] = pi; + ps[ 0] = tmp1; ps[ 1] = tmp2; ps[ 2] = tmp3; ps[ 3] = tmp4; + tmp1 = cs[2]; tmp2 = cs[3]; + cs[2] = ci + 1; cs[3] = ci; + cs[0] = tmp1; cs[1] = tmp2; + break; + case 2: + ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6; ps[15] = pi + 5; + ps[ 8] = ps[11]; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 4; + ps[ 4] = ps[7]; /* calculated below */ ps[ 7] = pi + 3; + ps[ 0] = ps[3]; ps[ 1] = pi; ps[ 2] = pi + 1; ps[ 3] = pi + 2; + cs[2] = cs[3]; cs[3] = ci + 1; + cs[0] = cs[1]; cs[1] = ci; + break; + case 3: + ps[12] = ps[0]; ps[13] = ps[1]; ps[14] = ps[2]; ps[15] = ps[3]; + ps[ 8] = pi; /* values for 5, 6, 9, 10 are */ ps[11] = pi + 7; + ps[ 4] = pi + 1; /* calculated below */ ps[ 7] = pi + 6; + ps[ 0] = pi + 2; ps[ 1] = pi + 3; ps[ 2] = pi + 4; ps[ 3] = pi + 5; + cs[2] = cs[0]; cs[3] = cs[1]; + cs[0] = ci; cs[1] = ci + 1; + break; + } + // set p11, p12, p21, p22 + ps[5] = coords.length; + coords.push([ + (-4 * coords[ps[0]][0] - coords[ps[15]][0] + + 6 * (coords[ps[4]][0] + coords[ps[1]][0]) - + 2 * (coords[ps[12]][0] + coords[ps[3]][0]) + + 3 * (coords[ps[13]][0] + coords[ps[7]][0])) / 9, + (-4 * coords[ps[0]][1] - coords[ps[15]][1] + + 6 * (coords[ps[4]][1] + coords[ps[1]][1]) - + 2 * (coords[ps[12]][1] + coords[ps[3]][1]) + + 3 * (coords[ps[13]][1] + coords[ps[7]][1])) / 9 + ]); + ps[6] = coords.length; + coords.push([ + (-4 * coords[ps[3]][0] - coords[ps[12]][0] + + 6 * (coords[ps[2]][0] + coords[ps[7]][0]) - + 2 * (coords[ps[0]][0] + coords[ps[15]][0]) + + 3 * (coords[ps[4]][0] + coords[ps[14]][0])) / 9, + (-4 * coords[ps[3]][1] - coords[ps[12]][1] + + 6 * (coords[ps[2]][1] + coords[ps[7]][1]) - + 2 * (coords[ps[0]][1] + coords[ps[15]][1]) + + 3 * (coords[ps[4]][1] + coords[ps[14]][1])) / 9 + ]); + ps[9] = coords.length; + coords.push([ + (-4 * coords[ps[12]][0] - coords[ps[3]][0] + + 6 * (coords[ps[8]][0] + coords[ps[13]][0]) - + 2 * (coords[ps[0]][0] + coords[ps[15]][0]) + + 3 * (coords[ps[11]][0] + coords[ps[1]][0])) / 9, + (-4 * coords[ps[12]][1] - coords[ps[3]][1] + + 6 * (coords[ps[8]][1] + coords[ps[13]][1]) - + 2 * (coords[ps[0]][1] + coords[ps[15]][1]) + + 3 * (coords[ps[11]][1] + coords[ps[1]][1])) / 9 + ]); + ps[10] = coords.length; + coords.push([ + (-4 * coords[ps[15]][0] - coords[ps[0]][0] + + 6 * (coords[ps[11]][0] + coords[ps[14]][0]) - + 2 * (coords[ps[12]][0] + coords[ps[3]][0]) + + 3 * (coords[ps[2]][0] + coords[ps[8]][0])) / 9, + (-4 * coords[ps[15]][1] - coords[ps[0]][1] + + 6 * (coords[ps[11]][1] + coords[ps[14]][1]) - + 2 * (coords[ps[12]][1] + coords[ps[3]][1]) + + 3 * (coords[ps[2]][1] + coords[ps[8]][1])) / 9 + ]); + mesh.figures.push({ + type: 'patch', + coords: new Int32Array(ps), // making copies of ps and cs + colors: new Int32Array(cs) + }); + } + } + + function decodeType7Shading(mesh, reader) { + var coords = mesh.coords; + var colors = mesh.colors; + var ps = new Int32Array(16); // p00, p10, ..., p30, p01, ..., p33 + var cs = new Int32Array(4); // c00, c30, c03, c33 + while (reader.hasData) { + var f = reader.readFlag(); + assert(0 <= f && f <= 3, 'Unknown type7 flag'); + var i, ii; + var pi = coords.length; + for (i = 0, ii = (f !== 0 ? 12 : 16); i < ii; i++) { + coords.push(reader.readCoordinate()); + } + var ci = colors.length; + for (i = 0, ii = (f !== 0 ? 2 : 4); i < ii; i++) { + colors.push(reader.readComponents()); + } + var tmp1, tmp2, tmp3, tmp4; + switch (f) { + case 0: + ps[12] = pi + 3; ps[13] = pi + 4; ps[14] = pi + 5; ps[15] = pi + 6; + ps[ 8] = pi + 2; ps[ 9] = pi + 13; ps[10] = pi + 14; ps[11] = pi + 7; + ps[ 4] = pi + 1; ps[ 5] = pi + 12; ps[ 6] = pi + 15; ps[ 7] = pi + 8; + ps[ 0] = pi; ps[ 1] = pi + 11; ps[ 2] = pi + 10; ps[ 3] = pi + 9; + cs[2] = ci + 1; cs[3] = ci + 2; + cs[0] = ci; cs[1] = ci + 3; + break; + case 1: + tmp1 = ps[12]; tmp2 = ps[13]; tmp3 = ps[14]; tmp4 = ps[15]; + ps[12] = pi + 5; ps[13] = pi + 4; ps[14] = pi + 3; ps[15] = pi + 2; + ps[ 8] = pi + 6; ps[ 9] = pi + 11; ps[10] = pi + 10; ps[11] = pi + 1; + ps[ 4] = pi + 7; ps[ 5] = pi + 8; ps[ 6] = pi + 9; ps[ 7] = pi; + ps[ 0] = tmp1; ps[ 1] = tmp2; ps[ 2] = tmp3; ps[ 3] = tmp4; + tmp1 = cs[2]; tmp2 = cs[3]; + cs[2] = ci + 1; cs[3] = ci; + cs[0] = tmp1; cs[1] = tmp2; + break; + case 2: + ps[12] = ps[15]; ps[13] = pi + 7; ps[14] = pi + 6; ps[15] = pi + 5; + ps[ 8] = ps[11]; ps[ 9] = pi + 8; ps[10] = pi + 11; ps[11] = pi + 4; + ps[ 4] = ps[7]; ps[ 5] = pi + 9; ps[ 6] = pi + 10; ps[ 7] = pi + 3; + ps[ 0] = ps[3]; ps[ 1] = pi; ps[ 2] = pi + 1; ps[ 3] = pi + 2; + cs[2] = cs[3]; cs[3] = ci + 1; + cs[0] = cs[1]; cs[1] = ci; + break; + case 3: + ps[12] = ps[0]; ps[13] = ps[1]; ps[14] = ps[2]; ps[15] = ps[3]; + ps[ 8] = pi; ps[ 9] = pi + 9; ps[10] = pi + 8; ps[11] = pi + 7; + ps[ 4] = pi + 1; ps[ 5] = pi + 10; ps[ 6] = pi + 11; ps[ 7] = pi + 6; + ps[ 0] = pi + 2; ps[ 1] = pi + 3; ps[ 2] = pi + 4; ps[ 3] = pi + 5; + cs[2] = cs[0]; cs[3] = cs[1]; + cs[0] = ci; cs[1] = ci + 1; + break; + } + mesh.figures.push({ + type: 'patch', + coords: new Int32Array(ps), // making copies of ps and cs + colors: new Int32Array(cs) + }); + } + } + + function updateBounds(mesh) { + var minX = mesh.coords[0][0], minY = mesh.coords[0][1], + maxX = minX, maxY = minY; + for (var i = 1, ii = mesh.coords.length; i < ii; i++) { + var x = mesh.coords[i][0], y = mesh.coords[i][1]; + minX = minX > x ? x : minX; + minY = minY > y ? y : minY; + maxX = maxX < x ? x : maxX; + maxY = maxY < y ? y : maxY; + } + mesh.bounds = [minX, minY, maxX, maxY]; + } + + function packData(mesh) { + var i, ii, j, jj; + + var coords = mesh.coords; + var coordsPacked = new Float32Array(coords.length * 2); + for (i = 0, j = 0, ii = coords.length; i < ii; i++) { + var xy = coords[i]; + coordsPacked[j++] = xy[0]; + coordsPacked[j++] = xy[1]; + } + mesh.coords = coordsPacked; + + var colors = mesh.colors; + var colorsPacked = new Uint8Array(colors.length * 3); + for (i = 0, j = 0, ii = colors.length; i < ii; i++) { + var c = colors[i]; + colorsPacked[j++] = c[0]; + colorsPacked[j++] = c[1]; + colorsPacked[j++] = c[2]; + } + mesh.colors = colorsPacked; + + var figures = mesh.figures; + for (i = 0, ii = figures.length; i < ii; i++) { + var figure = figures[i], ps = figure.coords, cs = figure.colors; + for (j = 0, jj = ps.length; j < jj; j++) { + ps[j] *= 2; + cs[j] *= 3; + } + } + } + + function Mesh(stream, matrix, xref, res) { + assert(isStream(stream), 'Mesh data is not a stream'); + var dict = stream.dict; + this.matrix = matrix; + this.shadingType = dict.get('ShadingType'); + this.type = 'Pattern'; + this.bbox = dict.get('BBox'); + var cs = dict.get('ColorSpace', 'CS'); + cs = ColorSpace.parse(cs, xref, res); + this.cs = cs; + this.background = dict.has('Background') ? + cs.getRgb(dict.get('Background'), 0) : null; + + var fnObj = dict.get('Function'); + var fn = fnObj ? PDFFunction.parseArray(xref, fnObj) : null; + + this.coords = []; + this.colors = []; + this.figures = []; + + var decodeContext = { + bitsPerCoordinate: dict.get('BitsPerCoordinate'), + bitsPerComponent: dict.get('BitsPerComponent'), + bitsPerFlag: dict.get('BitsPerFlag'), + decode: dict.get('Decode'), + colorFn: fn, + colorSpace: cs, + numComps: fn ? 1 : cs.numComps + }; + var reader = new MeshStreamReader(stream, decodeContext); + + var patchMesh = false; + switch (this.shadingType) { + case PatternType.FREE_FORM_MESH: + decodeType4Shading(this, reader); + break; + case PatternType.LATTICE_FORM_MESH: + var verticesPerRow = dict.get('VerticesPerRow') | 0; + assert(verticesPerRow >= 2, 'Invalid VerticesPerRow'); + decodeType5Shading(this, reader, verticesPerRow); + break; + case PatternType.COONS_PATCH_MESH: + decodeType6Shading(this, reader); + patchMesh = true; + break; + case PatternType.TENSOR_PATCH_MESH: + decodeType7Shading(this, reader); + patchMesh = true; + break; + default: + error('Unsupported mesh type.'); + break; + } + + if (patchMesh) { + // dirty bounds calculation for determining, how dense shall be triangles + updateBounds(this); + for (var i = 0, ii = this.figures.length; i < ii; i++) { + buildFigureFromPatch(this, i); + } + } + // calculate bounds + updateBounds(this); + + packData(this); + } + + Mesh.prototype = { + getIR: function Mesh_getIR() { + return ['Mesh', this.shadingType, this.coords, this.colors, this.figures, + this.bounds, this.matrix, this.bbox, this.background]; + } + }; + + return Mesh; +})(); + +Shadings.Dummy = (function DummyClosure() { + function Dummy() { + this.type = 'Pattern'; + } + + Dummy.prototype = { + getIR: function Dummy_getIR() { + return ['Dummy']; + } + }; + return Dummy; +})(); + +function getTilingPatternIR(operatorList, dict, args) { + var matrix = dict.get('Matrix'); + var bbox = dict.get('BBox'); + var xstep = dict.get('XStep'); + var ystep = dict.get('YStep'); + var paintType = dict.get('PaintType'); + var tilingType = dict.get('TilingType'); + + return [ + 'TilingPattern', args, operatorList, matrix, bbox, xstep, ystep, + paintType, tilingType + ]; +} + + +var PartialEvaluator = (function PartialEvaluatorClosure() { + function PartialEvaluator(pdfManager, xref, handler, pageIndex, + uniquePrefix, idCounters, fontCache) { + this.pdfManager = pdfManager; + this.xref = xref; + this.handler = handler; + this.pageIndex = pageIndex; + this.uniquePrefix = uniquePrefix; + this.idCounters = idCounters; + this.fontCache = fontCache; + } + + // Trying to minimize Date.now() usage and check every 100 time + var TIME_SLOT_DURATION_MS = 20; + var CHECK_TIME_EVERY = 100; + function TimeSlotManager() { + this.reset(); + } + TimeSlotManager.prototype = { + check: function TimeSlotManager_check() { + if (++this.checked < CHECK_TIME_EVERY) { + return false; + } + this.checked = 0; + return this.endTime <= Date.now(); + }, + reset: function TimeSlotManager_reset() { + this.endTime = Date.now() + TIME_SLOT_DURATION_MS; + this.checked = 0; + } + }; + + var deferred = Promise.resolve(); + + var TILING_PATTERN = 1, SHADING_PATTERN = 2; + + PartialEvaluator.prototype = { + hasBlendModes: function PartialEvaluator_hasBlendModes(resources) { + if (!isDict(resources)) { + return false; + } + + var processed = Object.create(null); + if (resources.objId) { + processed[resources.objId] = true; + } + + var nodes = [resources]; + while (nodes.length) { + var key; + var node = nodes.shift(); + // First check the current resources for blend modes. + var graphicStates = node.get('ExtGState'); + if (isDict(graphicStates)) { + graphicStates = graphicStates.getAll(); + for (key in graphicStates) { + var graphicState = graphicStates[key]; + var bm = graphicState['BM']; + if (isName(bm) && bm.name !== 'Normal') { + return true; + } + } + } + // Descend into the XObjects to look for more resources and blend modes. + var xObjects = node.get('XObject'); + if (!isDict(xObjects)) { + continue; + } + xObjects = xObjects.getAll(); + for (key in xObjects) { + var xObject = xObjects[key]; + if (!isStream(xObject)) { + continue; + } + if (xObject.dict.objId) { + if (processed[xObject.dict.objId]) { + // stream has objId and is processed already + continue; + } + processed[xObject.dict.objId] = true; + } + var xResources = xObject.dict.get('Resources'); + // Checking objId to detect an infinite loop. + if (isDict(xResources) && + (!xResources.objId || !processed[xResources.objId])) { + nodes.push(xResources); + if (xResources.objId) { + processed[xResources.objId] = true; + } + } + } + } + return false; + }, + + buildFormXObject: function PartialEvaluator_buildFormXObject(resources, + xobj, smask, + operatorList, + initialState) { + var matrix = xobj.dict.get('Matrix'); + var bbox = xobj.dict.get('BBox'); + var group = xobj.dict.get('Group'); + if (group) { + var groupOptions = { + matrix: matrix, + bbox: bbox, + smask: smask, + isolated: false, + knockout: false + }; + + var groupSubtype = group.get('S'); + var colorSpace; + if (isName(groupSubtype) && groupSubtype.name === 'Transparency') { + groupOptions.isolated = (group.get('I') || false); + groupOptions.knockout = (group.get('K') || false); + colorSpace = (group.has('CS') ? + ColorSpace.parse(group.get('CS'), this.xref, resources) : null); + } + + if (smask && smask.backdrop) { + colorSpace = colorSpace || ColorSpace.singletons.rgb; + smask.backdrop = colorSpace.getRgb(smask.backdrop, 0); + } + + operatorList.addOp(OPS.beginGroup, [groupOptions]); + } + + operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]); + + return this.getOperatorList(xobj, + (xobj.dict.get('Resources') || resources), operatorList, initialState). + then(function () { + operatorList.addOp(OPS.paintFormXObjectEnd, []); + + if (group) { + operatorList.addOp(OPS.endGroup, [groupOptions]); + } + }); + }, + + buildPaintImageXObject: + function PartialEvaluator_buildPaintImageXObject(resources, image, + inline, operatorList, + cacheKey, imageCache) { + var self = this; + var dict = image.dict; + var w = dict.get('Width', 'W'); + var h = dict.get('Height', 'H'); + + if (!(w && isNum(w)) || !(h && isNum(h))) { + warn('Image dimensions are missing, or not numbers.'); + return; + } + if (PDFJS.maxImageSize !== -1 && w * h > PDFJS.maxImageSize) { + warn('Image exceeded maximum allowed size and was removed.'); + return; + } + + var imageMask = (dict.get('ImageMask', 'IM') || false); + var imgData, args; + if (imageMask) { + // This depends on a tmpCanvas being filled with the + // current fillStyle, such that processing the pixel + // data can't be done here. Instead of creating a + // complete PDFImage, only read the information needed + // for later. + + var width = dict.get('Width', 'W'); + var height = dict.get('Height', 'H'); + var bitStrideLength = (width + 7) >> 3; + var imgArray = image.getBytes(bitStrideLength * height); + var decode = dict.get('Decode', 'D'); + var inverseDecode = (!!decode && decode[0] > 0); + + imgData = PDFImage.createMask(imgArray, width, height, + image instanceof DecodeStream, + inverseDecode); + imgData.cached = true; + args = [imgData]; + operatorList.addOp(OPS.paintImageMaskXObject, args); + if (cacheKey) { + imageCache[cacheKey] = { + fn: OPS.paintImageMaskXObject, + args: args + }; + } + return; + } + + var softMask = (dict.get('SMask', 'SM') || false); + var mask = (dict.get('Mask') || false); + + var SMALL_IMAGE_DIMENSIONS = 200; + // Inlining small images into the queue as RGB data + if (inline && !softMask && !mask && !(image instanceof JpegStream) && + (w + h) < SMALL_IMAGE_DIMENSIONS) { + var imageObj = new PDFImage(this.xref, resources, image, + inline, null, null); + // We force the use of RGBA_32BPP images here, because we can't handle + // any other kind. + imgData = imageObj.createImageData(/* forceRGBA = */ true); + operatorList.addOp(OPS.paintInlineImageXObject, [imgData]); + return; + } + + // If there is no imageMask, create the PDFImage and a lot + // of image processing can be done here. + var uniquePrefix = (this.uniquePrefix || ''); + var objId = 'img_' + uniquePrefix + (++this.idCounters.obj); + operatorList.addDependency(objId); + args = [objId, w, h]; + + if (!softMask && !mask && image instanceof JpegStream && + image.isNativelySupported(this.xref, resources)) { + // These JPEGs don't need any more processing so we can just send it. + operatorList.addOp(OPS.paintJpegXObject, args); + this.handler.send('obj', + [objId, this.pageIndex, 'JpegStream', image.getIR()]); + return; + } + + PDFImage.buildImage(self.handler, self.xref, resources, image, inline). + then(function(imageObj) { + var imgData = imageObj.createImageData(/* forceRGBA = */ false); + self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], + [imgData.data.buffer]); + }).then(undefined, function (reason) { + warn('Unable to decode image: ' + reason); + self.handler.send('obj', [objId, self.pageIndex, 'Image', null]); + }); + + operatorList.addOp(OPS.paintImageXObject, args); + if (cacheKey) { + imageCache[cacheKey] = { + fn: OPS.paintImageXObject, + args: args + }; + } + }, + + handleSMask: function PartialEvaluator_handleSmask(smask, resources, + operatorList, + stateManager) { + var smaskContent = smask.get('G'); + var smaskOptions = { + subtype: smask.get('S').name, + backdrop: smask.get('BC') + }; + return this.buildFormXObject(resources, smaskContent, smaskOptions, + operatorList, stateManager.state.clone()); + }, + + handleTilingType: + function PartialEvaluator_handleTilingType(fn, args, resources, + pattern, patternDict, + operatorList) { + // Create an IR of the pattern code. + var tilingOpList = new OperatorList(); + return this.getOperatorList(pattern, + (patternDict.get('Resources') || resources), tilingOpList). + then(function () { + // Add the dependencies to the parent operator list so they are + // resolved before sub operator list is executed synchronously. + operatorList.addDependencies(tilingOpList.dependencies); + operatorList.addOp(fn, getTilingPatternIR({ + fnArray: tilingOpList.fnArray, + argsArray: tilingOpList.argsArray + }, patternDict, args)); + }); + }, + + handleSetFont: + function PartialEvaluator_handleSetFont(resources, fontArgs, fontRef, + operatorList, state) { + // TODO(mack): Not needed? + var fontName; + if (fontArgs) { + fontArgs = fontArgs.slice(); + fontName = fontArgs[0].name; + } + + var self = this; + return this.loadFont(fontName, fontRef, this.xref, resources).then( + function (translated) { + if (!translated.font.isType3Font) { + return translated; + } + return translated.loadType3Data(self, resources, operatorList).then( + function () { + return translated; + }); + }).then(function (translated) { + state.font = translated.font; + translated.send(self.handler); + return translated.loadedName; + }); + }, + + handleText: function PartialEvaluator_handleText(chars, state) { + var font = state.font; + var glyphs = font.charsToGlyphs(chars); + var isAddToPathSet = !!(state.textRenderingMode & + TextRenderingMode.ADD_TO_PATH_FLAG); + if (font.data && (isAddToPathSet || PDFJS.disableFontFace)) { + var buildPath = function (fontChar) { + if (!font.renderer.hasBuiltPath(fontChar)) { + var path = font.renderer.getPathJs(fontChar); + this.handler.send('commonobj', [ + font.loadedName + '_path_' + fontChar, + 'FontPath', + path + ]); + } + }.bind(this); + + for (var i = 0, ii = glyphs.length; i < ii; i++) { + var glyph = glyphs[i]; + if (glyph === null) { + continue; + } + buildPath(glyph.fontChar); + + // If the glyph has an accent we need to build a path for its + // fontChar too, otherwise CanvasGraphics_paintChar will fail. + var accent = glyph.accent; + if (accent && accent.fontChar) { + buildPath(accent.fontChar); + } + } + } + + return glyphs; + }, + + setGState: function PartialEvaluator_setGState(resources, gState, + operatorList, xref, + stateManager) { + // This array holds the converted/processed state data. + var gStateObj = []; + var gStateMap = gState.map; + var self = this; + var promise = Promise.resolve(); + for (var key in gStateMap) { + var value = gStateMap[key]; + switch (key) { + case 'Type': + break; + case 'LW': + case 'LC': + case 'LJ': + case 'ML': + case 'D': + case 'RI': + case 'FL': + case 'CA': + case 'ca': + gStateObj.push([key, value]); + break; + case 'Font': + promise = promise.then(function () { + return self.handleSetFont(resources, null, value[0], + operatorList, stateManager.state). + then(function (loadedName) { + operatorList.addDependency(loadedName); + gStateObj.push([key, [loadedName, value[1]]]); + }); + }); + break; + case 'BM': + gStateObj.push([key, value]); + break; + case 'SMask': + if (isName(value) && value.name === 'None') { + gStateObj.push([key, false]); + break; + } + var dict = xref.fetchIfRef(value); + if (isDict(dict)) { + promise = promise.then(function () { + return self.handleSMask(dict, resources, operatorList, + stateManager); + }); + gStateObj.push([key, true]); + } else { + warn('Unsupported SMask type'); + } + + break; + // Only generate info log messages for the following since + // they are unlikely to have a big impact on the rendering. + case 'OP': + case 'op': + case 'OPM': + case 'BG': + case 'BG2': + case 'UCR': + case 'UCR2': + case 'TR': + case 'TR2': + case 'HT': + case 'SM': + case 'SA': + case 'AIS': + case 'TK': + // TODO implement these operators. + info('graphic state operator ' + key); + break; + default: + info('Unknown graphic state operator ' + key); + break; + } + } + return promise.then(function () { + if (gStateObj.length >= 0) { + operatorList.addOp(OPS.setGState, [gStateObj]); + } + }); + }, + + loadFont: function PartialEvaluator_loadFont(fontName, font, xref, + resources) { + + function errorFont() { + return Promise.resolve(new TranslatedFont('g_font_error', + new ErrorFont('Font ' + fontName + ' is not available'), font)); + } + var fontRef; + if (font) { // Loading by ref. + assert(isRef(font)); + fontRef = font; + } else { // Loading by name. + var fontRes = resources.get('Font'); + if (fontRes) { + fontRef = fontRes.getRaw(fontName); + } else { + warn('fontRes not available'); + return errorFont(); + } + } + if (!fontRef) { + warn('fontRef not available'); + return errorFont(); + } + + if (this.fontCache.has(fontRef)) { + return this.fontCache.get(fontRef); + } + + font = xref.fetchIfRef(fontRef); + if (!isDict(font)) { + return errorFont(); + } + + // We are holding font.translated references just for fontRef that are not + // dictionaries (Dict). See explanation below. + if (font.translated) { + return font.translated; + } + + var fontCapability = createPromiseCapability(); + + var preEvaluatedFont = this.preEvaluateFont(font, xref); + var descriptor = preEvaluatedFont.descriptor; + var fontID = fontRef.num + '_' + fontRef.gen; + if (isDict(descriptor)) { + if (!descriptor.fontAliases) { + descriptor.fontAliases = Object.create(null); + } + + var fontAliases = descriptor.fontAliases; + var hash = preEvaluatedFont.hash; + if (fontAliases[hash]) { + var aliasFontRef = fontAliases[hash].aliasRef; + if (aliasFontRef && this.fontCache.has(aliasFontRef)) { + this.fontCache.putAlias(fontRef, aliasFontRef); + return this.fontCache.get(fontRef); + } + } + + if (!fontAliases[hash]) { + fontAliases[hash] = { + fontID: Font.getFontID() + }; + } + + fontAliases[hash].aliasRef = fontRef; + fontID = fontAliases[hash].fontID; + } + + // Workaround for bad PDF generators that don't reference fonts + // properly, i.e. by not using an object identifier. + // Check if the fontRef is a Dict (as opposed to a standard object), + // in which case we don't cache the font and instead reference it by + // fontName in font.loadedName below. + var fontRefIsDict = isDict(fontRef); + if (!fontRefIsDict) { + this.fontCache.put(fontRef, fontCapability.promise); + } + + // Keep track of each font we translated so the caller can + // load them asynchronously before calling display on a page. + font.loadedName = 'g_font_' + (fontRefIsDict ? + fontName.replace(/\W/g, '') : fontID); + + font.translated = fontCapability.promise; + + // TODO move promises into translate font + var translatedPromise; + try { + translatedPromise = Promise.resolve( + this.translateFont(preEvaluatedFont, xref)); + } catch (e) { + translatedPromise = Promise.reject(e); + } + + translatedPromise.then(function (translatedFont) { + if (translatedFont.fontType !== undefined) { + var xrefFontStats = xref.stats.fontTypes; + xrefFontStats[translatedFont.fontType] = true; + } + + fontCapability.resolve(new TranslatedFont(font.loadedName, + translatedFont, font)); + }, function (reason) { + // TODO fontCapability.reject? + UnsupportedManager.notify(UNSUPPORTED_FEATURES.font); + + try { + // error, but it's still nice to have font type reported + var descriptor = preEvaluatedFont.descriptor; + var fontFile3 = descriptor && descriptor.get('FontFile3'); + var subtype = fontFile3 && fontFile3.get('Subtype'); + var fontType = getFontType(preEvaluatedFont.type, + subtype && subtype.name); + var xrefFontStats = xref.stats.fontTypes; + xrefFontStats[fontType] = true; + } catch (ex) { } + + fontCapability.resolve(new TranslatedFont(font.loadedName, + new ErrorFont(reason instanceof Error ? reason.message : reason), + font)); + }); + return fontCapability.promise; + }, + + buildPath: function PartialEvaluator_buildPath(operatorList, fn, args) { + var lastIndex = operatorList.length - 1; + if (!args) { + args = []; + } + if (lastIndex < 0 || + operatorList.fnArray[lastIndex] !== OPS.constructPath) { + operatorList.addOp(OPS.constructPath, [[fn], args]); + } else { + var opArgs = operatorList.argsArray[lastIndex]; + opArgs[0].push(fn); + Array.prototype.push.apply(opArgs[1], args); + } + }, + + handleColorN: function PartialEvaluator_handleColorN(operatorList, fn, args, + cs, patterns, resources, xref) { + // compile tiling patterns + var patternName = args[args.length - 1]; + // SCN/scn applies patterns along with normal colors + var pattern; + if (isName(patternName) && + (pattern = patterns.get(patternName.name))) { + var dict = (isStream(pattern) ? pattern.dict : pattern); + var typeNum = dict.get('PatternType'); + + if (typeNum === TILING_PATTERN) { + var color = cs.base ? cs.base.getRgb(args, 0) : null; + return this.handleTilingType(fn, color, resources, pattern, + dict, operatorList); + } else if (typeNum === SHADING_PATTERN) { + var shading = dict.get('Shading'); + var matrix = dict.get('Matrix'); + pattern = Pattern.parseShading(shading, matrix, xref, resources); + operatorList.addOp(fn, pattern.getIR()); + return Promise.resolve(); + } else { + return Promise.reject('Unknown PatternType: ' + typeNum); + } + } + // TODO shall we fail here? + operatorList.addOp(fn, args); + return Promise.resolve(); + }, + + getOperatorList: function PartialEvaluator_getOperatorList(stream, + resources, + operatorList, + initialState) { + + var self = this; + var xref = this.xref; + var imageCache = {}; + + assert(operatorList); + + resources = (resources || Dict.empty); + var xobjs = (resources.get('XObject') || Dict.empty); + var patterns = (resources.get('Pattern') || Dict.empty); + var stateManager = new StateManager(initialState || new EvalState()); + var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager); + var timeSlotManager = new TimeSlotManager(); + + return new Promise(function next(resolve, reject) { + timeSlotManager.reset(); + var stop, operation = {}, i, ii, cs; + while (!(stop = timeSlotManager.check())) { + // The arguments parsed by read() are used beyond this loop, so we + // cannot reuse the same array on each iteration. Therefore we pass + // in |null| as the initial value (see the comment on + // EvaluatorPreprocessor_read() for why). + operation.args = null; + if (!(preprocessor.read(operation))) { + break; + } + var args = operation.args; + var fn = operation.fn; + + switch (fn | 0) { + case OPS.paintXObject: + if (args[0].code) { + break; + } + // eagerly compile XForm objects + var name = args[0].name; + if (imageCache[name] !== undefined) { + operatorList.addOp(imageCache[name].fn, imageCache[name].args); + args = null; + continue; + } + + var xobj = xobjs.get(name); + if (xobj) { + assert(isStream(xobj), 'XObject should be a stream'); + + var type = xobj.dict.get('Subtype'); + assert(isName(type), + 'XObject should have a Name subtype'); + + if (type.name === 'Form') { + stateManager.save(); + return self.buildFormXObject(resources, xobj, null, + operatorList, + stateManager.state.clone()). + then(function () { + stateManager.restore(); + next(resolve, reject); + }, reject); + } else if (type.name === 'Image') { + self.buildPaintImageXObject(resources, xobj, false, + operatorList, name, imageCache); + args = null; + continue; + } else if (type.name === 'PS') { + // PostScript XObjects are unused when viewing documents. + // See section 4.7.1 of Adobe's PDF reference. + info('Ignored XObject subtype PS'); + continue; + } else { + error('Unhandled XObject subtype ' + type.name); + } + } + break; + case OPS.setFont: + var fontSize = args[1]; + // eagerly collect all fonts + return self.handleSetFont(resources, args, null, + operatorList, stateManager.state). + then(function (loadedName) { + operatorList.addDependency(loadedName); + operatorList.addOp(OPS.setFont, [loadedName, fontSize]); + next(resolve, reject); + }, reject); + case OPS.endInlineImage: + var cacheKey = args[0].cacheKey; + if (cacheKey) { + var cacheEntry = imageCache[cacheKey]; + if (cacheEntry !== undefined) { + operatorList.addOp(cacheEntry.fn, cacheEntry.args); + args = null; + continue; + } + } + self.buildPaintImageXObject(resources, args[0], true, + operatorList, cacheKey, imageCache); + args = null; + continue; + case OPS.showText: + args[0] = self.handleText(args[0], stateManager.state); + break; + case OPS.showSpacedText: + var arr = args[0]; + var combinedGlyphs = []; + var arrLength = arr.length; + for (i = 0; i < arrLength; ++i) { + var arrItem = arr[i]; + if (isString(arrItem)) { + Array.prototype.push.apply(combinedGlyphs, + self.handleText(arrItem, stateManager.state)); + } else if (isNum(arrItem)) { + combinedGlyphs.push(arrItem); + } + } + args[0] = combinedGlyphs; + fn = OPS.showText; + break; + case OPS.nextLineShowText: + operatorList.addOp(OPS.nextLine); + args[0] = self.handleText(args[0], stateManager.state); + fn = OPS.showText; + break; + case OPS.nextLineSetSpacingShowText: + operatorList.addOp(OPS.nextLine); + operatorList.addOp(OPS.setWordSpacing, [args.shift()]); + operatorList.addOp(OPS.setCharSpacing, [args.shift()]); + args[0] = self.handleText(args[0], stateManager.state); + fn = OPS.showText; + break; + case OPS.setTextRenderingMode: + stateManager.state.textRenderingMode = args[0]; + break; + + case OPS.setFillColorSpace: + stateManager.state.fillColorSpace = + ColorSpace.parse(args[0], xref, resources); + continue; + case OPS.setStrokeColorSpace: + stateManager.state.strokeColorSpace = + ColorSpace.parse(args[0], xref, resources); + continue; + case OPS.setFillColor: + cs = stateManager.state.fillColorSpace; + args = cs.getRgb(args, 0); + fn = OPS.setFillRGBColor; + break; + case OPS.setStrokeColor: + cs = stateManager.state.strokeColorSpace; + args = cs.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + case OPS.setFillGray: + stateManager.state.fillColorSpace = ColorSpace.singletons.gray; + args = ColorSpace.singletons.gray.getRgb(args, 0); + fn = OPS.setFillRGBColor; + break; + case OPS.setStrokeGray: + stateManager.state.strokeColorSpace = ColorSpace.singletons.gray; + args = ColorSpace.singletons.gray.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + case OPS.setFillCMYKColor: + stateManager.state.fillColorSpace = ColorSpace.singletons.cmyk; + args = ColorSpace.singletons.cmyk.getRgb(args, 0); + fn = OPS.setFillRGBColor; + break; + case OPS.setStrokeCMYKColor: + stateManager.state.strokeColorSpace = ColorSpace.singletons.cmyk; + args = ColorSpace.singletons.cmyk.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + case OPS.setFillRGBColor: + stateManager.state.fillColorSpace = ColorSpace.singletons.rgb; + args = ColorSpace.singletons.rgb.getRgb(args, 0); + break; + case OPS.setStrokeRGBColor: + stateManager.state.strokeColorSpace = ColorSpace.singletons.rgb; + args = ColorSpace.singletons.rgb.getRgb(args, 0); + break; + case OPS.setFillColorN: + cs = stateManager.state.fillColorSpace; + if (cs.name === 'Pattern') { + return self.handleColorN(operatorList, OPS.setFillColorN, + args, cs, patterns, resources, xref).then(function() { + next(resolve, reject); + }, reject); + } + args = cs.getRgb(args, 0); + fn = OPS.setFillRGBColor; + break; + case OPS.setStrokeColorN: + cs = stateManager.state.strokeColorSpace; + if (cs.name === 'Pattern') { + return self.handleColorN(operatorList, OPS.setStrokeColorN, + args, cs, patterns, resources, xref).then(function() { + next(resolve, reject); + }, reject); + } + args = cs.getRgb(args, 0); + fn = OPS.setStrokeRGBColor; + break; + + case OPS.shadingFill: + var shadingRes = resources.get('Shading'); + if (!shadingRes) { + error('No shading resource found'); + } + + var shading = shadingRes.get(args[0].name); + if (!shading) { + error('No shading object found'); + } + + var shadingFill = Pattern.parseShading(shading, null, xref, + resources); + var patternIR = shadingFill.getIR(); + args = [patternIR]; + fn = OPS.shadingFill; + break; + case OPS.setGState: + var dictName = args[0]; + var extGState = resources.get('ExtGState'); + + if (!isDict(extGState) || !extGState.has(dictName.name)) { + break; + } + + var gState = extGState.get(dictName.name); + return self.setGState(resources, gState, operatorList, xref, + stateManager).then(function() { + next(resolve, reject); + }, reject); + case OPS.moveTo: + case OPS.lineTo: + case OPS.curveTo: + case OPS.curveTo2: + case OPS.curveTo3: + case OPS.closePath: + self.buildPath(operatorList, fn, args); + continue; + case OPS.rectangle: + self.buildPath(operatorList, fn, args); + continue; + } + operatorList.addOp(fn, args); + } + if (stop) { + deferred.then(function () { + next(resolve, reject); + }); + return; + } + // Some PDFs don't close all restores inside object/form. + // Closing those for them. + for (i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) { + operatorList.addOp(OPS.restore, []); + } + resolve(); + }); + }, + + getTextContent: function PartialEvaluator_getTextContent(stream, resources, + stateManager) { + + stateManager = (stateManager || new StateManager(new TextState())); + + var textContent = { + items: [], + styles: Object.create(null) + }; + var bidiTexts = textContent.items; + var SPACE_FACTOR = 0.3; + var MULTI_SPACE_FACTOR = 1.5; + + var self = this; + var xref = this.xref; + + resources = (xref.fetchIfRef(resources) || Dict.empty); + + // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd. + var xobjs = null; + var xobjsCache = {}; + + var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager); + + var textState; + + function newTextChunk() { + var font = textState.font; + if (!(font.loadedName in textContent.styles)) { + textContent.styles[font.loadedName] = { + fontFamily: font.fallbackName, + ascent: font.ascent, + descent: font.descent, + vertical: font.vertical + }; + } + return { + // |str| is initially an array which we push individual chars to, and + // then runBidi() overwrites it with the final string. + str: [], + dir: null, + width: 0, + height: 0, + transform: null, + fontName: font.loadedName + }; + } + + function runBidi(textChunk) { + var str = textChunk.str.join(''); + var bidiResult = PDFJS.bidi(str, -1, textState.font.vertical); + textChunk.str = bidiResult.str; + textChunk.dir = bidiResult.dir; + return textChunk; + } + + function handleSetFont(fontName, fontRef) { + return self.loadFont(fontName, fontRef, xref, resources). + then(function (translated) { + textState.font = translated.font; + textState.fontMatrix = translated.font.fontMatrix || + FONT_IDENTITY_MATRIX; + }); + } + + function buildTextGeometry(chars, textChunk) { + var font = textState.font; + textChunk = textChunk || newTextChunk(); + if (!textChunk.transform) { + // 9.4.4 Text Space Details + var tsm = [textState.fontSize * textState.textHScale, 0, + 0, textState.fontSize, + 0, textState.textRise]; + + if (font.isType3Font && + textState.fontMatrix !== FONT_IDENTITY_MATRIX && + textState.fontSize === 1) { + var glyphHeight = font.bbox[3] - font.bbox[1]; + if (glyphHeight > 0) { + glyphHeight = glyphHeight * textState.fontMatrix[3]; + tsm[3] *= glyphHeight; + } + } + + var trm = textChunk.transform = Util.transform(textState.ctm, + Util.transform(textState.textMatrix, tsm)); + if (!font.vertical) { + textChunk.height = Math.sqrt(trm[2] * trm[2] + trm[3] * trm[3]); + } else { + textChunk.width = Math.sqrt(trm[0] * trm[0] + trm[1] * trm[1]); + } + } + var width = 0; + var height = 0; + var glyphs = font.charsToGlyphs(chars); + var defaultVMetrics = font.defaultVMetrics; + for (var i = 0; i < glyphs.length; i++) { + var glyph = glyphs[i]; + if (!glyph) { // Previous glyph was a space. + width += textState.wordSpacing * textState.textHScale; + continue; + } + var vMetricX = null; + var vMetricY = null; + var glyphWidth = null; + if (font.vertical) { + if (glyph.vmetric) { + glyphWidth = glyph.vmetric[0]; + vMetricX = glyph.vmetric[1]; + vMetricY = glyph.vmetric[2]; + } else { + glyphWidth = glyph.width; + vMetricX = glyph.width * 0.5; + vMetricY = defaultVMetrics[2]; + } + } else { + glyphWidth = glyph.width; + } + + var glyphUnicode = glyph.unicode; + if (NormalizedUnicodes[glyphUnicode] !== undefined) { + glyphUnicode = NormalizedUnicodes[glyphUnicode]; + } + glyphUnicode = reverseIfRtl(glyphUnicode); + + // The following will calculate the x and y of the individual glyphs. + // if (font.vertical) { + // tsm[4] -= vMetricX * Math.abs(textState.fontSize) * + // textState.fontMatrix[0]; + // tsm[5] -= vMetricY * textState.fontSize * + // textState.fontMatrix[0]; + // } + // var trm = Util.transform(textState.textMatrix, tsm); + // var pt = Util.applyTransform([trm[4], trm[5]], textState.ctm); + // var x = pt[0]; + // var y = pt[1]; + + var charSpacing = 0; + if (textChunk.str.length > 0) { + // Apply char spacing only when there are chars. + // As a result there is only spacing between glyphs. + charSpacing = textState.charSpacing; + } + + var tx = 0; + var ty = 0; + if (!font.vertical) { + var w0 = glyphWidth * textState.fontMatrix[0]; + tx = (w0 * textState.fontSize + charSpacing) * + textState.textHScale; + width += tx; + } else { + var w1 = glyphWidth * textState.fontMatrix[0]; + ty = w1 * textState.fontSize + charSpacing; + height += ty; + } + textState.translateTextMatrix(tx, ty); + + textChunk.str.push(glyphUnicode); + } + + var a = textState.textLineMatrix[0]; + var b = textState.textLineMatrix[1]; + var scaleLineX = Math.sqrt(a * a + b * b); + a = textState.ctm[0]; + b = textState.ctm[1]; + var scaleCtmX = Math.sqrt(a * a + b * b); + if (!font.vertical) { + textChunk.width += width * scaleCtmX * scaleLineX; + } else { + textChunk.height += Math.abs(height * scaleCtmX * scaleLineX); + } + return textChunk; + } + + var timeSlotManager = new TimeSlotManager(); + + return new Promise(function next(resolve, reject) { + timeSlotManager.reset(); + var stop, operation = {}, args = []; + while (!(stop = timeSlotManager.check())) { + // The arguments parsed by read() are not used beyond this loop, so + // we can reuse the same array on every iteration, thus avoiding + // unnecessary allocations. + args.length = 0; + operation.args = args; + if (!(preprocessor.read(operation))) { + break; + } + textState = stateManager.state; + var fn = operation.fn; + args = operation.args; + + switch (fn | 0) { + case OPS.setFont: + textState.fontSize = args[1]; + return handleSetFont(args[0].name).then(function() { + next(resolve, reject); + }, reject); + case OPS.setTextRise: + textState.textRise = args[0]; + break; + case OPS.setHScale: + textState.textHScale = args[0] / 100; + break; + case OPS.setLeading: + textState.leading = args[0]; + break; + case OPS.moveText: + textState.translateTextLineMatrix(args[0], args[1]); + textState.textMatrix = textState.textLineMatrix.slice(); + break; + case OPS.setLeadingMoveText: + textState.leading = -args[1]; + textState.translateTextLineMatrix(args[0], args[1]); + textState.textMatrix = textState.textLineMatrix.slice(); + break; + case OPS.nextLine: + textState.carriageReturn(); + break; + case OPS.setTextMatrix: + textState.setTextMatrix(args[0], args[1], args[2], args[3], + args[4], args[5]); + textState.setTextLineMatrix(args[0], args[1], args[2], args[3], + args[4], args[5]); + break; + case OPS.setCharSpacing: + textState.charSpacing = args[0]; + break; + case OPS.setWordSpacing: + textState.wordSpacing = args[0]; + break; + case OPS.beginText: + textState.textMatrix = IDENTITY_MATRIX.slice(); + textState.textLineMatrix = IDENTITY_MATRIX.slice(); + break; + case OPS.showSpacedText: + var items = args[0]; + var textChunk = newTextChunk(); + var offset; + for (var j = 0, jj = items.length; j < jj; j++) { + if (typeof items[j] === 'string') { + buildTextGeometry(items[j], textChunk); + } else { + var val = items[j] / 1000; + if (!textState.font.vertical) { + offset = -val * textState.fontSize * textState.textHScale * + textState.textMatrix[0]; + textState.translateTextMatrix(offset, 0); + textChunk.width += offset; + } else { + offset = -val * textState.fontSize * + textState.textMatrix[3]; + textState.translateTextMatrix(0, offset); + textChunk.height += offset; + } + if (items[j] < 0 && textState.font.spaceWidth > 0) { + var fakeSpaces = -items[j] / textState.font.spaceWidth; + if (fakeSpaces > MULTI_SPACE_FACTOR) { + fakeSpaces = Math.round(fakeSpaces); + while (fakeSpaces--) { + textChunk.str.push(' '); + } + } else if (fakeSpaces > SPACE_FACTOR) { + textChunk.str.push(' '); + } + } + } + } + bidiTexts.push(runBidi(textChunk)); + break; + case OPS.showText: + bidiTexts.push(runBidi(buildTextGeometry(args[0]))); + break; + case OPS.nextLineShowText: + textState.carriageReturn(); + bidiTexts.push(runBidi(buildTextGeometry(args[0]))); + break; + case OPS.nextLineSetSpacingShowText: + textState.wordSpacing = args[0]; + textState.charSpacing = args[1]; + textState.carriageReturn(); + bidiTexts.push(runBidi(buildTextGeometry(args[2]))); + break; + case OPS.paintXObject: + if (args[0].code) { + break; + } + + if (!xobjs) { + xobjs = (resources.get('XObject') || Dict.empty); + } + + var name = args[0].name; + if (xobjsCache.key === name) { + if (xobjsCache.texts) { + Util.appendToArray(bidiTexts, xobjsCache.texts.items); + Util.extendObj(textContent.styles, xobjsCache.texts.styles); + } + break; + } + + var xobj = xobjs.get(name); + if (!xobj) { + break; + } + assert(isStream(xobj), 'XObject should be a stream'); + + var type = xobj.dict.get('Subtype'); + assert(isName(type), + 'XObject should have a Name subtype'); + + if ('Form' !== type.name) { + xobjsCache.key = name; + xobjsCache.texts = null; + break; + } + + stateManager.save(); + var matrix = xobj.dict.get('Matrix'); + if (isArray(matrix) && matrix.length === 6) { + stateManager.transform(matrix); + } + + return self.getTextContent(xobj, + xobj.dict.get('Resources') || resources, stateManager). + then(function (formTextContent) { + Util.appendToArray(bidiTexts, formTextContent.items); + Util.extendObj(textContent.styles, formTextContent.styles); + stateManager.restore(); + + xobjsCache.key = name; + xobjsCache.texts = formTextContent; + + next(resolve, reject); + }, reject); + case OPS.setGState: + var dictName = args[0]; + var extGState = resources.get('ExtGState'); + + if (!isDict(extGState) || !extGState.has(dictName.name)) { + break; + } + + var gsStateMap = extGState.get(dictName.name); + var gsStateFont = null; + for (var key in gsStateMap) { + if (key === 'Font') { + assert(!gsStateFont); + gsStateFont = gsStateMap[key]; + } + } + if (gsStateFont) { + textState.fontSize = gsStateFont[1]; + return handleSetFont(gsStateFont[0]).then(function() { + next(resolve, reject); + }, reject); + } + break; + } // switch + } // while + if (stop) { + deferred.then(function () { + next(resolve, reject); + }); + return; + } + resolve(textContent); + }); + }, + + extractDataStructures: function + partialEvaluatorExtractDataStructures(dict, baseDict, + xref, properties) { + // 9.10.2 + var toUnicode = (dict.get('ToUnicode') || baseDict.get('ToUnicode')); + if (toUnicode) { + properties.toUnicode = this.readToUnicode(toUnicode); + } + if (properties.composite) { + // CIDSystemInfo helps to match CID to glyphs + var cidSystemInfo = dict.get('CIDSystemInfo'); + if (isDict(cidSystemInfo)) { + properties.cidSystemInfo = { + registry: cidSystemInfo.get('Registry'), + ordering: cidSystemInfo.get('Ordering'), + supplement: cidSystemInfo.get('Supplement') + }; + } + + var cidToGidMap = dict.get('CIDToGIDMap'); + if (isStream(cidToGidMap)) { + properties.cidToGidMap = this.readCidToGidMap(cidToGidMap); + } + } + + // Based on 9.6.6 of the spec the encoding can come from multiple places + // and depends on the font type. The base encoding and differences are + // read here, but the encoding that is actually used is chosen during + // glyph mapping in the font. + // TODO: Loading the built in encoding in the font would allow the + // differences to be merged in here not require us to hold on to it. + var differences = []; + var baseEncodingName = null; + var encoding; + if (dict.has('Encoding')) { + encoding = dict.get('Encoding'); + if (isDict(encoding)) { + baseEncodingName = encoding.get('BaseEncoding'); + baseEncodingName = (isName(baseEncodingName) ? + baseEncodingName.name : null); + // Load the differences between the base and original + if (encoding.has('Differences')) { + var diffEncoding = encoding.get('Differences'); + var index = 0; + for (var j = 0, jj = diffEncoding.length; j < jj; j++) { + var data = diffEncoding[j]; + if (isNum(data)) { + index = data; + } else if (isName(data)) { + differences[index++] = data.name; + } else if (isRef(data)) { + diffEncoding[j--] = xref.fetch(data); + continue; + } else { + error('Invalid entry in \'Differences\' array: ' + data); + } + } + } + } else if (isName(encoding)) { + baseEncodingName = encoding.name; + } else { + error('Encoding is not a Name nor a Dict'); + } + // According to table 114 if the encoding is a named encoding it must be + // one of these predefined encodings. + if ((baseEncodingName !== 'MacRomanEncoding' && + baseEncodingName !== 'MacExpertEncoding' && + baseEncodingName !== 'WinAnsiEncoding')) { + baseEncodingName = null; + } + } + + if (baseEncodingName) { + properties.defaultEncoding = Encodings[baseEncodingName].slice(); + } else { + encoding = (properties.type === 'TrueType' ? + Encodings.WinAnsiEncoding : Encodings.StandardEncoding); + // The Symbolic attribute can be misused for regular fonts + // Heuristic: we have to check if the font is a standard one also + if (!!(properties.flags & FontFlags.Symbolic)) { + encoding = Encodings.MacRomanEncoding; + if (!properties.file) { + if (/Symbol/i.test(properties.name)) { + encoding = Encodings.SymbolSetEncoding; + } else if (/Dingbats/i.test(properties.name)) { + encoding = Encodings.ZapfDingbatsEncoding; + } + } + } + properties.defaultEncoding = encoding; + } + + properties.differences = differences; + properties.baseEncodingName = baseEncodingName; + properties.dict = dict; + }, + + readToUnicode: function PartialEvaluator_readToUnicode(toUnicode) { + var cmap, cmapObj = toUnicode; + if (isName(cmapObj)) { + cmap = CMapFactory.create(cmapObj, + { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null); + if (cmap instanceof IdentityCMap) { + return new IdentityToUnicodeMap(0, 0xFFFF); + } + return new ToUnicodeMap(cmap.getMap()); + } else if (isStream(cmapObj)) { + cmap = CMapFactory.create(cmapObj, + { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null); + if (cmap instanceof IdentityCMap) { + return new IdentityToUnicodeMap(0, 0xFFFF); + } + cmap = cmap.getMap(); + // Convert UTF-16BE + // NOTE: cmap can be a sparse array, so use forEach instead of for(;;) + // to iterate over all keys. + cmap.forEach(function(token, i) { + var str = []; + for (var k = 0; k < token.length; k += 2) { + var w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1); + if ((w1 & 0xF800) !== 0xD800) { // w1 < 0xD800 || w1 > 0xDFFF + str.push(w1); + continue; + } + k += 2; + var w2 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1); + str.push(((w1 & 0x3ff) << 10) + (w2 & 0x3ff) + 0x10000); + } + cmap[i] = String.fromCharCode.apply(String, str); + }); + return new ToUnicodeMap(cmap); + } + return null; + }, + + readCidToGidMap: function PartialEvaluator_readCidToGidMap(cidToGidStream) { + // Extract the encoding from the CIDToGIDMap + var glyphsData = cidToGidStream.getBytes(); + + // Set encoding 0 to later verify the font has an encoding + var result = []; + for (var j = 0, jj = glyphsData.length; j < jj; j++) { + var glyphID = (glyphsData[j++] << 8) | glyphsData[j]; + if (glyphID === 0) { + continue; + } + var code = j >> 1; + result[code] = glyphID; + } + return result; + }, + + extractWidths: function PartialEvaluator_extractWidths(dict, xref, + descriptor, + properties) { + var glyphsWidths = []; + var defaultWidth = 0; + var glyphsVMetrics = []; + var defaultVMetrics; + var i, ii, j, jj, start, code, widths; + if (properties.composite) { + defaultWidth = dict.get('DW') || 1000; + + widths = dict.get('W'); + if (widths) { + for (i = 0, ii = widths.length; i < ii; i++) { + start = widths[i++]; + code = xref.fetchIfRef(widths[i]); + if (isArray(code)) { + for (j = 0, jj = code.length; j < jj; j++) { + glyphsWidths[start++] = code[j]; + } + } else { + var width = widths[++i]; + for (j = start; j <= code; j++) { + glyphsWidths[j] = width; + } + } + } + } + + if (properties.vertical) { + var vmetrics = (dict.get('DW2') || [880, -1000]); + defaultVMetrics = [vmetrics[1], defaultWidth * 0.5, vmetrics[0]]; + vmetrics = dict.get('W2'); + if (vmetrics) { + for (i = 0, ii = vmetrics.length; i < ii; i++) { + start = vmetrics[i++]; + code = xref.fetchIfRef(vmetrics[i]); + if (isArray(code)) { + for (j = 0, jj = code.length; j < jj; j++) { + glyphsVMetrics[start++] = [code[j++], code[j++], code[j]]; + } + } else { + var vmetric = [vmetrics[++i], vmetrics[++i], vmetrics[++i]]; + for (j = start; j <= code; j++) { + glyphsVMetrics[j] = vmetric; + } + } + } + } + } + } else { + var firstChar = properties.firstChar; + widths = dict.get('Widths'); + if (widths) { + j = firstChar; + for (i = 0, ii = widths.length; i < ii; i++) { + glyphsWidths[j++] = widths[i]; + } + defaultWidth = (parseFloat(descriptor.get('MissingWidth')) || 0); + } else { + // Trying get the BaseFont metrics (see comment above). + var baseFontName = dict.get('BaseFont'); + if (isName(baseFontName)) { + var metrics = this.getBaseFontMetrics(baseFontName.name); + + glyphsWidths = this.buildCharCodeToWidth(metrics.widths, + properties); + defaultWidth = metrics.defaultWidth; + } + } + } + + // Heuristic: detection of monospace font by checking all non-zero widths + var isMonospace = true; + var firstWidth = defaultWidth; + for (var glyph in glyphsWidths) { + var glyphWidth = glyphsWidths[glyph]; + if (!glyphWidth) { + continue; + } + if (!firstWidth) { + firstWidth = glyphWidth; + continue; + } + if (firstWidth !== glyphWidth) { + isMonospace = false; + break; + } + } + if (isMonospace) { + properties.flags |= FontFlags.FixedPitch; + } + + properties.defaultWidth = defaultWidth; + properties.widths = glyphsWidths; + properties.defaultVMetrics = defaultVMetrics; + properties.vmetrics = glyphsVMetrics; + }, + + isSerifFont: function PartialEvaluator_isSerifFont(baseFontName) { + // Simulating descriptor flags attribute + var fontNameWoStyle = baseFontName.split('-')[0]; + return (fontNameWoStyle in serifFonts) || + (fontNameWoStyle.search(/serif/gi) !== -1); + }, + + getBaseFontMetrics: function PartialEvaluator_getBaseFontMetrics(name) { + var defaultWidth = 0; + var widths = []; + var monospace = false; + var lookupName = (stdFontMap[name] || name); + + if (!(lookupName in Metrics)) { + // Use default fonts for looking up font metrics if the passed + // font is not a base font + if (this.isSerifFont(name)) { + lookupName = 'Times-Roman'; + } else { + lookupName = 'Helvetica'; + } + } + var glyphWidths = Metrics[lookupName]; + + if (isNum(glyphWidths)) { + defaultWidth = glyphWidths; + monospace = true; + } else { + widths = glyphWidths; + } + + return { + defaultWidth: defaultWidth, + monospace: monospace, + widths: widths + }; + }, + + buildCharCodeToWidth: + function PartialEvaluator_bulildCharCodeToWidth(widthsByGlyphName, + properties) { + var widths = Object.create(null); + var differences = properties.differences; + var encoding = properties.defaultEncoding; + for (var charCode = 0; charCode < 256; charCode++) { + if (charCode in differences && + widthsByGlyphName[differences[charCode]]) { + widths[charCode] = widthsByGlyphName[differences[charCode]]; + continue; + } + if (charCode in encoding && widthsByGlyphName[encoding[charCode]]) { + widths[charCode] = widthsByGlyphName[encoding[charCode]]; + continue; + } + } + return widths; + }, + + preEvaluateFont: function PartialEvaluator_preEvaluateFont(dict, xref) { + var baseDict = dict; + var type = dict.get('Subtype'); + assert(isName(type), 'invalid font Subtype'); + + var composite = false; + var uint8array; + if (type.name === 'Type0') { + // If font is a composite + // - get the descendant font + // - set the type according to the descendant font + // - get the FontDescriptor from the descendant font + var df = dict.get('DescendantFonts'); + if (!df) { + error('Descendant fonts are not specified'); + } + dict = (isArray(df) ? xref.fetchIfRef(df[0]) : df); + + type = dict.get('Subtype'); + assert(isName(type), 'invalid font Subtype'); + composite = true; + } + + var descriptor = dict.get('FontDescriptor'); + if (descriptor) { + var hash = new MurmurHash3_64(); + var encoding = baseDict.getRaw('Encoding'); + if (isName(encoding)) { + hash.update(encoding.name); + } else if (isRef(encoding)) { + hash.update(encoding.num + '_' + encoding.gen); + } else if (isDict(encoding)) { + var keys = encoding.getKeys(); + for (var i = 0, ii = keys.length; i < ii; i++) { + var entry = encoding.getRaw(keys[i]); + if (isName(entry)) { + hash.update(entry.name); + } else if (isRef(entry)) { + hash.update(entry.num + '_' + entry.gen); + } else if (isArray(entry)) { // 'Differences' entry. + // Ideally we should check the contents of the array, but to avoid + // parsing it here and then again in |extractDataStructures|, + // we only use the array length for now (fixes bug1157493.pdf). + hash.update(entry.length.toString()); + } + } + } + + var toUnicode = dict.get('ToUnicode') || baseDict.get('ToUnicode'); + if (isStream(toUnicode)) { + var stream = toUnicode.str || toUnicode; + uint8array = stream.buffer ? + new Uint8Array(stream.buffer.buffer, 0, stream.bufferLength) : + new Uint8Array(stream.bytes.buffer, + stream.start, stream.end - stream.start); + hash.update(uint8array); + + } else if (isName(toUnicode)) { + hash.update(toUnicode.name); + } + + var widths = dict.get('Widths') || baseDict.get('Widths'); + if (widths) { + uint8array = new Uint8Array(new Uint32Array(widths).buffer); + hash.update(uint8array); + } + } + + return { + descriptor: descriptor, + dict: dict, + baseDict: baseDict, + composite: composite, + type: type.name, + hash: hash ? hash.hexdigest() : '' + }; + }, + + translateFont: function PartialEvaluator_translateFont(preEvaluatedFont, + xref) { + var baseDict = preEvaluatedFont.baseDict; + var dict = preEvaluatedFont.dict; + var composite = preEvaluatedFont.composite; + var descriptor = preEvaluatedFont.descriptor; + var type = preEvaluatedFont.type; + var maxCharIndex = (composite ? 0xFFFF : 0xFF); + var properties; + + if (!descriptor) { + if (type === 'Type3') { + // FontDescriptor is only required for Type3 fonts when the document + // is a tagged pdf. Create a barbebones one to get by. + descriptor = new Dict(null); + descriptor.set('FontName', Name.get(type)); + descriptor.set('FontBBox', dict.get('FontBBox')); + } else { + // Before PDF 1.5 if the font was one of the base 14 fonts, having a + // FontDescriptor was not required. + // This case is here for compatibility. + var baseFontName = dict.get('BaseFont'); + if (!isName(baseFontName)) { + error('Base font is not specified'); + } + + // Using base font name as a font name. + baseFontName = baseFontName.name.replace(/[,_]/g, '-'); + var metrics = this.getBaseFontMetrics(baseFontName); + + // Simulating descriptor flags attribute + var fontNameWoStyle = baseFontName.split('-')[0]; + var flags = + (this.isSerifFont(fontNameWoStyle) ? FontFlags.Serif : 0) | + (metrics.monospace ? FontFlags.FixedPitch : 0) | + (symbolsFonts[fontNameWoStyle] ? FontFlags.Symbolic : + FontFlags.Nonsymbolic); + + properties = { + type: type, + name: baseFontName, + widths: metrics.widths, + defaultWidth: metrics.defaultWidth, + flags: flags, + firstChar: 0, + lastChar: maxCharIndex + }; + this.extractDataStructures(dict, dict, xref, properties); + properties.widths = this.buildCharCodeToWidth(metrics.widths, + properties); + return new Font(baseFontName, null, properties); + } + } + + // According to the spec if 'FontDescriptor' is declared, 'FirstChar', + // 'LastChar' and 'Widths' should exist too, but some PDF encoders seem + // to ignore this rule when a variant of a standart font is used. + // TODO Fill the width array depending on which of the base font this is + // a variant. + var firstChar = (dict.get('FirstChar') || 0); + var lastChar = (dict.get('LastChar') || maxCharIndex); + + var fontName = descriptor.get('FontName'); + var baseFont = dict.get('BaseFont'); + // Some bad PDFs have a string as the font name. + if (isString(fontName)) { + fontName = Name.get(fontName); + } + if (isString(baseFont)) { + baseFont = Name.get(baseFont); + } + + if (type !== 'Type3') { + var fontNameStr = fontName && fontName.name; + var baseFontStr = baseFont && baseFont.name; + if (fontNameStr !== baseFontStr) { + info('The FontDescriptor\'s FontName is "' + fontNameStr + + '" but should be the same as the Font\'s BaseFont "' + + baseFontStr + '"'); + // Workaround for cases where e.g. fontNameStr = 'Arial' and + // baseFontStr = 'Arial,Bold' (needed when no font file is embedded). + if (fontNameStr && baseFontStr && + baseFontStr.indexOf(fontNameStr) === 0) { + fontName = baseFont; + } + } + } + fontName = (fontName || baseFont); + + assert(isName(fontName), 'invalid font name'); + + var fontFile = descriptor.get('FontFile', 'FontFile2', 'FontFile3'); + if (fontFile) { + if (fontFile.dict) { + var subtype = fontFile.dict.get('Subtype'); + if (subtype) { + subtype = subtype.name; + } + var length1 = fontFile.dict.get('Length1'); + var length2 = fontFile.dict.get('Length2'); + } + } + + properties = { + type: type, + name: fontName.name, + subtype: subtype, + file: fontFile, + length1: length1, + length2: length2, + loadedName: baseDict.loadedName, + composite: composite, + wideChars: composite, + fixedPitch: false, + fontMatrix: (dict.get('FontMatrix') || FONT_IDENTITY_MATRIX), + firstChar: firstChar || 0, + lastChar: (lastChar || maxCharIndex), + bbox: descriptor.get('FontBBox'), + ascent: descriptor.get('Ascent'), + descent: descriptor.get('Descent'), + xHeight: descriptor.get('XHeight'), + capHeight: descriptor.get('CapHeight'), + flags: descriptor.get('Flags'), + italicAngle: descriptor.get('ItalicAngle'), + coded: false + }; + + if (composite) { + var cidEncoding = baseDict.get('Encoding'); + if (isName(cidEncoding)) { + properties.cidEncoding = cidEncoding.name; + } + properties.cMap = CMapFactory.create(cidEncoding, + { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null); + properties.vertical = properties.cMap.vertical; + } + this.extractDataStructures(dict, baseDict, xref, properties); + this.extractWidths(dict, xref, descriptor, properties); + + if (type === 'Type3') { + properties.isType3Font = true; + } + + return new Font(fontName.name, fontFile, properties); + } + }; + + return PartialEvaluator; +})(); + +var TranslatedFont = (function TranslatedFontClosure() { + function TranslatedFont(loadedName, font, dict) { + this.loadedName = loadedName; + this.font = font; + this.dict = dict; + this.type3Loaded = null; + this.sent = false; + } + TranslatedFont.prototype = { + send: function (handler) { + if (this.sent) { + return; + } + var fontData = this.font.exportData(); + handler.send('commonobj', [ + this.loadedName, + 'Font', + fontData + ]); + this.sent = true; + }, + loadType3Data: function (evaluator, resources, parentOperatorList) { + assert(this.font.isType3Font); + + if (this.type3Loaded) { + return this.type3Loaded; + } + + var translatedFont = this.font; + var loadCharProcsPromise = Promise.resolve(); + var charProcs = this.dict.get('CharProcs').getAll(); + var fontResources = this.dict.get('Resources') || resources; + var charProcKeys = Object.keys(charProcs); + var charProcOperatorList = {}; + for (var i = 0, n = charProcKeys.length; i < n; ++i) { + loadCharProcsPromise = loadCharProcsPromise.then(function (key) { + var glyphStream = charProcs[key]; + var operatorList = new OperatorList(); + return evaluator.getOperatorList(glyphStream, fontResources, + operatorList).then(function () { + charProcOperatorList[key] = operatorList.getIR(); + + // Add the dependencies to the parent operator list so they are + // resolved before sub operator list is executed synchronously. + parentOperatorList.addDependencies(operatorList.dependencies); + }, function (reason) { + warn('Type3 font resource \"' + key + '\" is not available'); + var operatorList = new OperatorList(); + charProcOperatorList[key] = operatorList.getIR(); + }); + }.bind(this, charProcKeys[i])); + } + this.type3Loaded = loadCharProcsPromise.then(function () { + translatedFont.charProcOperatorList = charProcOperatorList; + }); + return this.type3Loaded; + } + }; + return TranslatedFont; +})(); + +var OperatorList = (function OperatorListClosure() { + var CHUNK_SIZE = 1000; + var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5; // close to chunk size + + function getTransfers(queue) { + var transfers = []; + var fnArray = queue.fnArray, argsArray = queue.argsArray; + for (var i = 0, ii = queue.length; i < ii; i++) { + switch (fnArray[i]) { + case OPS.paintInlineImageXObject: + case OPS.paintInlineImageXObjectGroup: + case OPS.paintImageMaskXObject: + var arg = argsArray[i][0]; // first param in imgData + if (!arg.cached) { + transfers.push(arg.data.buffer); + } + break; + } + } + return transfers; + } + + function OperatorList(intent, messageHandler, pageIndex) { + this.messageHandler = messageHandler; + this.fnArray = []; + this.argsArray = []; + this.dependencies = {}; + this.pageIndex = pageIndex; + this.intent = intent; + } + + OperatorList.prototype = { + get length() { + return this.argsArray.length; + }, + + addOp: function(fn, args) { + this.fnArray.push(fn); + this.argsArray.push(args); + if (this.messageHandler) { + if (this.fnArray.length >= CHUNK_SIZE) { + this.flush(); + } else if (this.fnArray.length >= CHUNK_SIZE_ABOUT && + (fn === OPS.restore || fn === OPS.endText)) { + // heuristic to flush on boundary of restore or endText + this.flush(); + } + } + }, + + addDependency: function(dependency) { + if (dependency in this.dependencies) { + return; + } + this.dependencies[dependency] = true; + this.addOp(OPS.dependency, [dependency]); + }, + + addDependencies: function(dependencies) { + for (var key in dependencies) { + this.addDependency(key); + } + }, + + addOpList: function(opList) { + Util.extendObj(this.dependencies, opList.dependencies); + for (var i = 0, ii = opList.length; i < ii; i++) { + this.addOp(opList.fnArray[i], opList.argsArray[i]); + } + }, + + getIR: function() { + return { + fnArray: this.fnArray, + argsArray: this.argsArray, + length: this.length + }; + }, + + flush: function(lastChunk) { + if (this.intent !== 'oplist') { + new QueueOptimizer().optimize(this); + } + var transfers = getTransfers(this); + this.messageHandler.send('RenderPageChunk', { + operatorList: { + fnArray: this.fnArray, + argsArray: this.argsArray, + lastChunk: lastChunk, + length: this.length + }, + pageIndex: this.pageIndex, + intent: this.intent + }, transfers); + this.dependencies = {}; + this.fnArray.length = 0; + this.argsArray.length = 0; + } + }; + + return OperatorList; +})(); + +var StateManager = (function StateManagerClosure() { + function StateManager(initialState) { + this.state = initialState; + this.stateStack = []; + } + StateManager.prototype = { + save: function () { + var old = this.state; + this.stateStack.push(this.state); + this.state = old.clone(); + }, + restore: function () { + var prev = this.stateStack.pop(); + if (prev) { + this.state = prev; + } + }, + transform: function (args) { + this.state.ctm = Util.transform(this.state.ctm, args); + } + }; + return StateManager; +})(); + +var TextState = (function TextStateClosure() { + function TextState() { + this.ctm = new Float32Array(IDENTITY_MATRIX); + this.fontSize = 0; + this.font = null; + this.fontMatrix = FONT_IDENTITY_MATRIX; + this.textMatrix = IDENTITY_MATRIX.slice(); + this.textLineMatrix = IDENTITY_MATRIX.slice(); + this.charSpacing = 0; + this.wordSpacing = 0; + this.leading = 0; + this.textHScale = 1; + this.textRise = 0; + } + + TextState.prototype = { + setTextMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) { + var m = this.textMatrix; + m[0] = a; m[1] = b; m[2] = c; m[3] = d; m[4] = e; m[5] = f; + }, + setTextLineMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) { + var m = this.textLineMatrix; + m[0] = a; m[1] = b; m[2] = c; m[3] = d; m[4] = e; m[5] = f; + }, + translateTextMatrix: function TextState_translateTextMatrix(x, y) { + var m = this.textMatrix; + m[4] = m[0] * x + m[2] * y + m[4]; + m[5] = m[1] * x + m[3] * y + m[5]; + }, + translateTextLineMatrix: function TextState_translateTextMatrix(x, y) { + var m = this.textLineMatrix; + m[4] = m[0] * x + m[2] * y + m[4]; + m[5] = m[1] * x + m[3] * y + m[5]; + }, + calcRenderMatrix: function TextState_calcRendeMatrix(ctm) { + // 9.4.4 Text Space Details + var tsm = [this.fontSize * this.textHScale, 0, + 0, this.fontSize, + 0, this.textRise]; + return Util.transform(ctm, Util.transform(this.textMatrix, tsm)); + }, + carriageReturn: function TextState_carriageReturn() { + this.translateTextLineMatrix(0, -this.leading); + this.textMatrix = this.textLineMatrix.slice(); + }, + clone: function TextState_clone() { + var clone = Object.create(this); + clone.textMatrix = this.textMatrix.slice(); + clone.textLineMatrix = this.textLineMatrix.slice(); + clone.fontMatrix = this.fontMatrix.slice(); + return clone; + } + }; + return TextState; +})(); + +var EvalState = (function EvalStateClosure() { + function EvalState() { + this.ctm = new Float32Array(IDENTITY_MATRIX); + this.font = null; + this.textRenderingMode = TextRenderingMode.FILL; + this.fillColorSpace = ColorSpace.singletons.gray; + this.strokeColorSpace = ColorSpace.singletons.gray; + } + EvalState.prototype = { + clone: function CanvasExtraState_clone() { + return Object.create(this); + }, + }; + return EvalState; +})(); + +var EvaluatorPreprocessor = (function EvaluatorPreprocessorClosure() { + // Specifies properties for each command + // + // If variableArgs === true: [0, `numArgs`] expected + // If variableArgs === false: exactly `numArgs` expected + var OP_MAP = { + // Graphic state + w: { id: OPS.setLineWidth, numArgs: 1, variableArgs: false }, + J: { id: OPS.setLineCap, numArgs: 1, variableArgs: false }, + j: { id: OPS.setLineJoin, numArgs: 1, variableArgs: false }, + M: { id: OPS.setMiterLimit, numArgs: 1, variableArgs: false }, + d: { id: OPS.setDash, numArgs: 2, variableArgs: false }, + ri: { id: OPS.setRenderingIntent, numArgs: 1, variableArgs: false }, + i: { id: OPS.setFlatness, numArgs: 1, variableArgs: false }, + gs: { id: OPS.setGState, numArgs: 1, variableArgs: false }, + q: { id: OPS.save, numArgs: 0, variableArgs: false }, + Q: { id: OPS.restore, numArgs: 0, variableArgs: false }, + cm: { id: OPS.transform, numArgs: 6, variableArgs: false }, + + // Path + m: { id: OPS.moveTo, numArgs: 2, variableArgs: false }, + l: { id: OPS.lineTo, numArgs: 2, variableArgs: false }, + c: { id: OPS.curveTo, numArgs: 6, variableArgs: false }, + v: { id: OPS.curveTo2, numArgs: 4, variableArgs: false }, + y: { id: OPS.curveTo3, numArgs: 4, variableArgs: false }, + h: { id: OPS.closePath, numArgs: 0, variableArgs: false }, + re: { id: OPS.rectangle, numArgs: 4, variableArgs: false }, + S: { id: OPS.stroke, numArgs: 0, variableArgs: false }, + s: { id: OPS.closeStroke, numArgs: 0, variableArgs: false }, + f: { id: OPS.fill, numArgs: 0, variableArgs: false }, + F: { id: OPS.fill, numArgs: 0, variableArgs: false }, + 'f*': { id: OPS.eoFill, numArgs: 0, variableArgs: false }, + B: { id: OPS.fillStroke, numArgs: 0, variableArgs: false }, + 'B*': { id: OPS.eoFillStroke, numArgs: 0, variableArgs: false }, + b: { id: OPS.closeFillStroke, numArgs: 0, variableArgs: false }, + 'b*': { id: OPS.closeEOFillStroke, numArgs: 0, variableArgs: false }, + n: { id: OPS.endPath, numArgs: 0, variableArgs: false }, + + // Clipping + W: { id: OPS.clip, numArgs: 0, variableArgs: false }, + 'W*': { id: OPS.eoClip, numArgs: 0, variableArgs: false }, + + // Text + BT: { id: OPS.beginText, numArgs: 0, variableArgs: false }, + ET: { id: OPS.endText, numArgs: 0, variableArgs: false }, + Tc: { id: OPS.setCharSpacing, numArgs: 1, variableArgs: false }, + Tw: { id: OPS.setWordSpacing, numArgs: 1, variableArgs: false }, + Tz: { id: OPS.setHScale, numArgs: 1, variableArgs: false }, + TL: { id: OPS.setLeading, numArgs: 1, variableArgs: false }, + Tf: { id: OPS.setFont, numArgs: 2, variableArgs: false }, + Tr: { id: OPS.setTextRenderingMode, numArgs: 1, variableArgs: false }, + Ts: { id: OPS.setTextRise, numArgs: 1, variableArgs: false }, + Td: { id: OPS.moveText, numArgs: 2, variableArgs: false }, + TD: { id: OPS.setLeadingMoveText, numArgs: 2, variableArgs: false }, + Tm: { id: OPS.setTextMatrix, numArgs: 6, variableArgs: false }, + 'T*': { id: OPS.nextLine, numArgs: 0, variableArgs: false }, + Tj: { id: OPS.showText, numArgs: 1, variableArgs: false }, + TJ: { id: OPS.showSpacedText, numArgs: 1, variableArgs: false }, + '\'': { id: OPS.nextLineShowText, numArgs: 1, variableArgs: false }, + '"': { id: OPS.nextLineSetSpacingShowText, numArgs: 3, + variableArgs: false }, + + // Type3 fonts + d0: { id: OPS.setCharWidth, numArgs: 2, variableArgs: false }, + d1: { id: OPS.setCharWidthAndBounds, numArgs: 6, variableArgs: false }, + + // Color + CS: { id: OPS.setStrokeColorSpace, numArgs: 1, variableArgs: false }, + cs: { id: OPS.setFillColorSpace, numArgs: 1, variableArgs: false }, + SC: { id: OPS.setStrokeColor, numArgs: 4, variableArgs: true }, + SCN: { id: OPS.setStrokeColorN, numArgs: 33, variableArgs: true }, + sc: { id: OPS.setFillColor, numArgs: 4, variableArgs: true }, + scn: { id: OPS.setFillColorN, numArgs: 33, variableArgs: true }, + G: { id: OPS.setStrokeGray, numArgs: 1, variableArgs: false }, + g: { id: OPS.setFillGray, numArgs: 1, variableArgs: false }, + RG: { id: OPS.setStrokeRGBColor, numArgs: 3, variableArgs: false }, + rg: { id: OPS.setFillRGBColor, numArgs: 3, variableArgs: false }, + K: { id: OPS.setStrokeCMYKColor, numArgs: 4, variableArgs: false }, + k: { id: OPS.setFillCMYKColor, numArgs: 4, variableArgs: false }, + + // Shading + sh: { id: OPS.shadingFill, numArgs: 1, variableArgs: false }, + + // Images + BI: { id: OPS.beginInlineImage, numArgs: 0, variableArgs: false }, + ID: { id: OPS.beginImageData, numArgs: 0, variableArgs: false }, + EI: { id: OPS.endInlineImage, numArgs: 1, variableArgs: false }, + + // XObjects + Do: { id: OPS.paintXObject, numArgs: 1, variableArgs: false }, + MP: { id: OPS.markPoint, numArgs: 1, variableArgs: false }, + DP: { id: OPS.markPointProps, numArgs: 2, variableArgs: false }, + BMC: { id: OPS.beginMarkedContent, numArgs: 1, variableArgs: false }, + BDC: { id: OPS.beginMarkedContentProps, numArgs: 2, + variableArgs: false }, + EMC: { id: OPS.endMarkedContent, numArgs: 0, variableArgs: false }, + + // Compatibility + BX: { id: OPS.beginCompat, numArgs: 0, variableArgs: false }, + EX: { id: OPS.endCompat, numArgs: 0, variableArgs: false }, + + // (reserved partial commands for the lexer) + BM: null, + BD: null, + 'true': null, + fa: null, + fal: null, + fals: null, + 'false': null, + nu: null, + nul: null, + 'null': null + }; + + function EvaluatorPreprocessor(stream, xref, stateManager) { + // TODO(mduan): pass array of knownCommands rather than OP_MAP + // dictionary + this.parser = new Parser(new Lexer(stream, OP_MAP), false, xref); + this.stateManager = stateManager; + this.nonProcessedArgs = []; + } + + EvaluatorPreprocessor.prototype = { + get savedStatesDepth() { + return this.stateManager.stateStack.length; + }, + + // |operation| is an object with two fields: + // + // - |fn| is an out param. + // + // - |args| is an inout param. On entry, it should have one of two values. + // + // - An empty array. This indicates that the caller is providing the + // array in which the args will be stored in. The caller should use + // this value if it can reuse a single array for each call to read(). + // + // - |null|. This indicates that the caller needs this function to create + // the array in which any args are stored in. If there are zero args, + // this function will leave |operation.args| as |null| (thus avoiding + // allocations that would occur if we used an empty array to represent + // zero arguments). Otherwise, it will replace |null| with a new array + // containing the arguments. The caller should use this value if it + // cannot reuse an array for each call to read(). + // + // These two modes are present because this function is very hot and so + // avoiding allocations where possible is worthwhile. + // + read: function EvaluatorPreprocessor_read(operation) { + var args = operation.args; + while (true) { + var obj = this.parser.getObj(); + if (isCmd(obj)) { + var cmd = obj.cmd; + // Check that the command is valid + var opSpec = OP_MAP[cmd]; + if (!opSpec) { + warn('Unknown command "' + cmd + '"'); + continue; + } + + var fn = opSpec.id; + var numArgs = opSpec.numArgs; + var argsLength = args !== null ? args.length : 0; + + if (!opSpec.variableArgs) { + // Postscript commands can be nested, e.g. /F2 /GS2 gs 5.711 Tf + if (argsLength !== numArgs) { + var nonProcessedArgs = this.nonProcessedArgs; + while (argsLength > numArgs) { + nonProcessedArgs.push(args.shift()); + argsLength--; + } + while (argsLength < numArgs && nonProcessedArgs.length !== 0) { + if (!args) { + args = []; + } + args.unshift(nonProcessedArgs.pop()); + argsLength++; + } + } + + if (argsLength < numArgs) { + // If we receive too few args, it's not possible to possible + // to execute the command, so skip the command + info('Command ' + fn + ': because expected ' + + numArgs + ' args, but received ' + argsLength + + ' args; skipping'); + args = null; + continue; + } + } else if (argsLength > numArgs) { + info('Command ' + fn + ': expected [0,' + numArgs + + '] args, but received ' + argsLength + ' args'); + } + + // TODO figure out how to type-check vararg functions + this.preprocessCommand(fn, args); + + operation.fn = fn; + operation.args = args; + return true; + } else { + if (isEOF(obj)) { + return false; // no more commands + } + // argument + if (obj !== null) { + if (!args) { + args = []; + } + args.push((obj instanceof Dict ? obj.getAll() : obj)); + assert(args.length <= 33, 'Too many arguments'); + } + } + } + }, + + preprocessCommand: + function EvaluatorPreprocessor_preprocessCommand(fn, args) { + switch (fn | 0) { + case OPS.save: + this.stateManager.save(); + break; + case OPS.restore: + this.stateManager.restore(); + break; + case OPS.transform: + this.stateManager.transform(args); + break; + } + } + }; + return EvaluatorPreprocessor; +})(); + +var QueueOptimizer = (function QueueOptimizerClosure() { + function addState(parentState, pattern, fn) { + var state = parentState; + for (var i = 0, ii = pattern.length - 1; i < ii; i++) { + var item = pattern[i]; + state = (state[item] || (state[item] = [])); + } + state[pattern[pattern.length - 1]] = fn; + } + + function handlePaintSolidColorImageMask(iFirstSave, count, fnArray, + argsArray) { + // Handles special case of mainly LaTeX documents which use image masks to + // draw lines with the current fill style. + // 'count' groups of (save, transform, paintImageMaskXObject, restore)+ + // have been found at iFirstSave. + var iFirstPIMXO = iFirstSave + 2; + for (var i = 0; i < count; i++) { + var arg = argsArray[iFirstPIMXO + 4 * i]; + var imageMask = arg.length === 1 && arg[0]; + if (imageMask && imageMask.width === 1 && imageMask.height === 1 && + (!imageMask.data.length || + (imageMask.data.length === 1 && imageMask.data[0] === 0))) { + fnArray[iFirstPIMXO + 4 * i] = OPS.paintSolidColorImageMask; + continue; + } + break; + } + return count - i; + } + + var InitialState = []; + + // This replaces (save, transform, paintInlineImageXObject, restore)+ + // sequences with one |paintInlineImageXObjectGroup| operation. + addState(InitialState, + [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore], + function foundInlineImageGroup(context) { + var MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10; + var MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200; + var MAX_WIDTH = 1000; + var IMAGE_PADDING = 1; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var curr = context.iCurr; + var iFirstSave = curr - 3; + var iFirstTransform = curr - 2; + var iFirstPIIXO = curr - 1; + + // Look for the quartets. + var i = iFirstSave + 4; + var ii = fnArray.length; + while (i + 3 < ii) { + if (fnArray[i] !== OPS.save || + fnArray[i + 1] !== OPS.transform || + fnArray[i + 2] !== OPS.paintInlineImageXObject || + fnArray[i + 3] !== OPS.restore) { + break; // ops don't match + } + i += 4; + } + + // At this point, i is the index of the first op past the last valid + // quartet. + var count = Math.min((i - iFirstSave) / 4, + MAX_IMAGES_IN_INLINE_IMAGES_BLOCK); + if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) { + return i; + } + + // assuming that heights of those image is too small (~1 pixel) + // packing as much as possible by lines + var maxX = 0; + var map = [], maxLineHeight = 0; + var currentX = IMAGE_PADDING, currentY = IMAGE_PADDING; + var q; + for (q = 0; q < count; q++) { + var transform = argsArray[iFirstTransform + (q << 2)]; + var img = argsArray[iFirstPIIXO + (q << 2)][0]; + if (currentX + img.width > MAX_WIDTH) { + // starting new line + maxX = Math.max(maxX, currentX); + currentY += maxLineHeight + 2 * IMAGE_PADDING; + currentX = 0; + maxLineHeight = 0; + } + map.push({ + transform: transform, + x: currentX, y: currentY, + w: img.width, h: img.height + }); + currentX += img.width + 2 * IMAGE_PADDING; + maxLineHeight = Math.max(maxLineHeight, img.height); + } + var imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING; + var imgHeight = currentY + maxLineHeight + IMAGE_PADDING; + var imgData = new Uint8Array(imgWidth * imgHeight * 4); + var imgRowSize = imgWidth << 2; + for (q = 0; q < count; q++) { + var data = argsArray[iFirstPIIXO + (q << 2)][0].data; + // Copy image by lines and extends pixels into padding. + var rowSize = map[q].w << 2; + var dataOffset = 0; + var offset = (map[q].x + map[q].y * imgWidth) << 2; + imgData.set(data.subarray(0, rowSize), offset - imgRowSize); + for (var k = 0, kk = map[q].h; k < kk; k++) { + imgData.set(data.subarray(dataOffset, dataOffset + rowSize), offset); + dataOffset += rowSize; + offset += imgRowSize; + } + imgData.set(data.subarray(dataOffset - rowSize, dataOffset), offset); + while (offset >= 0) { + data[offset - 4] = data[offset]; + data[offset - 3] = data[offset + 1]; + data[offset - 2] = data[offset + 2]; + data[offset - 1] = data[offset + 3]; + data[offset + rowSize] = data[offset + rowSize - 4]; + data[offset + rowSize + 1] = data[offset + rowSize - 3]; + data[offset + rowSize + 2] = data[offset + rowSize - 2]; + data[offset + rowSize + 3] = data[offset + rowSize - 1]; + offset -= imgRowSize; + } + } + + // Replace queue items. + fnArray.splice(iFirstSave, count * 4, OPS.paintInlineImageXObjectGroup); + argsArray.splice(iFirstSave, count * 4, + [{ width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP, + data: imgData }, map]); + + return iFirstSave + 1; + }); + + // This replaces (save, transform, paintImageMaskXObject, restore)+ + // sequences with one |paintImageMaskXObjectGroup| or one + // |paintImageMaskXObjectRepeat| operation. + addState(InitialState, + [OPS.save, OPS.transform, OPS.paintImageMaskXObject, OPS.restore], + function foundImageMaskGroup(context) { + var MIN_IMAGES_IN_MASKS_BLOCK = 10; + var MAX_IMAGES_IN_MASKS_BLOCK = 100; + var MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var curr = context.iCurr; + var iFirstSave = curr - 3; + var iFirstTransform = curr - 2; + var iFirstPIMXO = curr - 1; + + // Look for the quartets. + var i = iFirstSave + 4; + var ii = fnArray.length; + while (i + 3 < ii) { + if (fnArray[i] !== OPS.save || + fnArray[i + 1] !== OPS.transform || + fnArray[i + 2] !== OPS.paintImageMaskXObject || + fnArray[i + 3] !== OPS.restore) { + break; // ops don't match + } + i += 4; + } + + // At this point, i is the index of the first op past the last valid + // quartet. + var count = (i - iFirstSave) / 4; + count = handlePaintSolidColorImageMask(iFirstSave, count, fnArray, + argsArray); + if (count < MIN_IMAGES_IN_MASKS_BLOCK) { + return i; + } + + var q; + var isSameImage = false; + var iTransform, transformArgs; + var firstPIMXOArg0 = argsArray[iFirstPIMXO][0]; + if (argsArray[iFirstTransform][1] === 0 && + argsArray[iFirstTransform][2] === 0) { + isSameImage = true; + var firstTransformArg0 = argsArray[iFirstTransform][0]; + var firstTransformArg3 = argsArray[iFirstTransform][3]; + iTransform = iFirstTransform + 4; + var iPIMXO = iFirstPIMXO + 4; + for (q = 1; q < count; q++, iTransform += 4, iPIMXO += 4) { + transformArgs = argsArray[iTransform]; + if (argsArray[iPIMXO][0] !== firstPIMXOArg0 || + transformArgs[0] !== firstTransformArg0 || + transformArgs[1] !== 0 || + transformArgs[2] !== 0 || + transformArgs[3] !== firstTransformArg3) { + if (q < MIN_IMAGES_IN_MASKS_BLOCK) { + isSameImage = false; + } else { + count = q; + } + break; // different image or transform + } + } + } + + if (isSameImage) { + count = Math.min(count, MAX_SAME_IMAGES_IN_MASKS_BLOCK); + var positions = new Float32Array(count * 2); + iTransform = iFirstTransform; + for (q = 0; q < count; q++, iTransform += 4) { + transformArgs = argsArray[iTransform]; + positions[(q << 1)] = transformArgs[4]; + positions[(q << 1) + 1] = transformArgs[5]; + } + + // Replace queue items. + fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectRepeat); + argsArray.splice(iFirstSave, count * 4, + [firstPIMXOArg0, firstTransformArg0, firstTransformArg3, positions]); + } else { + count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK); + var images = []; + for (q = 0; q < count; q++) { + transformArgs = argsArray[iFirstTransform + (q << 2)]; + var maskParams = argsArray[iFirstPIMXO + (q << 2)][0]; + images.push({ data: maskParams.data, width: maskParams.width, + height: maskParams.height, + transform: transformArgs }); + } + + // Replace queue items. + fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectGroup); + argsArray.splice(iFirstSave, count * 4, [images]); + } + + return iFirstSave + 1; + }); + + // This replaces (save, transform, paintImageXObject, restore)+ sequences + // with one paintImageXObjectRepeat operation, if the |transform| and + // |paintImageXObjectRepeat| ops are appropriate. + addState(InitialState, + [OPS.save, OPS.transform, OPS.paintImageXObject, OPS.restore], + function (context) { + var MIN_IMAGES_IN_BLOCK = 3; + var MAX_IMAGES_IN_BLOCK = 1000; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var curr = context.iCurr; + var iFirstSave = curr - 3; + var iFirstTransform = curr - 2; + var iFirstPIXO = curr - 1; + var iFirstRestore = curr; + + if (argsArray[iFirstTransform][1] !== 0 || + argsArray[iFirstTransform][2] !== 0) { + return iFirstRestore + 1; // transform has the wrong form + } + + // Look for the quartets. + var firstPIXOArg0 = argsArray[iFirstPIXO][0]; + var firstTransformArg0 = argsArray[iFirstTransform][0]; + var firstTransformArg3 = argsArray[iFirstTransform][3]; + var i = iFirstSave + 4; + var ii = fnArray.length; + while (i + 3 < ii) { + if (fnArray[i] !== OPS.save || + fnArray[i + 1] !== OPS.transform || + fnArray[i + 2] !== OPS.paintImageXObject || + fnArray[i + 3] !== OPS.restore) { + break; // ops don't match + } + if (argsArray[i + 1][0] !== firstTransformArg0 || + argsArray[i + 1][1] !== 0 || + argsArray[i + 1][2] !== 0 || + argsArray[i + 1][3] !== firstTransformArg3) { + break; // transforms don't match + } + if (argsArray[i + 2][0] !== firstPIXOArg0) { + break; // images don't match + } + i += 4; + } + + // At this point, i is the index of the first op past the last valid + // quartet. + var count = Math.min((i - iFirstSave) / 4, MAX_IMAGES_IN_BLOCK); + if (count < MIN_IMAGES_IN_BLOCK) { + return i; + } + + // Extract the (x,y) positions from all of the matching transforms. + var positions = new Float32Array(count * 2); + var iTransform = iFirstTransform; + for (var q = 0; q < count; q++, iTransform += 4) { + var transformArgs = argsArray[iTransform]; + positions[(q << 1)] = transformArgs[4]; + positions[(q << 1) + 1] = transformArgs[5]; + } + + // Replace queue items. + var args = [firstPIXOArg0, firstTransformArg0, firstTransformArg3, + positions]; + fnArray.splice(iFirstSave, count * 4, OPS.paintImageXObjectRepeat); + argsArray.splice(iFirstSave, count * 4, args); + + return iFirstSave + 1; + }); + + // This replaces (beginText, setFont, setTextMatrix, showText, endText)+ + // sequences with (beginText, setFont, (setTextMatrix, showText)+, endText)+ + // sequences, if the font for each one is the same. + addState(InitialState, + [OPS.beginText, OPS.setFont, OPS.setTextMatrix, OPS.showText, OPS.endText], + function (context) { + var MIN_CHARS_IN_BLOCK = 3; + var MAX_CHARS_IN_BLOCK = 1000; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var curr = context.iCurr; + var iFirstBeginText = curr - 4; + var iFirstSetFont = curr - 3; + var iFirstSetTextMatrix = curr - 2; + var iFirstShowText = curr - 1; + var iFirstEndText = curr; + + // Look for the quintets. + var firstSetFontArg0 = argsArray[iFirstSetFont][0]; + var firstSetFontArg1 = argsArray[iFirstSetFont][1]; + var i = iFirstBeginText + 5; + var ii = fnArray.length; + while (i + 4 < ii) { + if (fnArray[i] !== OPS.beginText || + fnArray[i + 1] !== OPS.setFont || + fnArray[i + 2] !== OPS.setTextMatrix || + fnArray[i + 3] !== OPS.showText || + fnArray[i + 4] !== OPS.endText) { + break; // ops don't match + } + if (argsArray[i + 1][0] !== firstSetFontArg0 || + argsArray[i + 1][1] !== firstSetFontArg1) { + break; // fonts don't match + } + i += 5; + } + + // At this point, i is the index of the first op past the last valid + // quintet. + var count = Math.min(((i - iFirstBeginText) / 5), MAX_CHARS_IN_BLOCK); + if (count < MIN_CHARS_IN_BLOCK) { + return i; + } + + // If the preceding quintet is (, setFont, setTextMatrix, + // showText, endText), include that as well. (E.g. might be + // |dependency|.) + var iFirst = iFirstBeginText; + if (iFirstBeginText >= 4 && + fnArray[iFirstBeginText - 4] === fnArray[iFirstSetFont] && + fnArray[iFirstBeginText - 3] === fnArray[iFirstSetTextMatrix] && + fnArray[iFirstBeginText - 2] === fnArray[iFirstShowText] && + fnArray[iFirstBeginText - 1] === fnArray[iFirstEndText] && + argsArray[iFirstBeginText - 4][0] === firstSetFontArg0 && + argsArray[iFirstBeginText - 4][1] === firstSetFontArg1) { + count++; + iFirst -= 5; + } + + // Remove (endText, beginText, setFont) trios. + var iEndText = iFirst + 4; + for (var q = 1; q < count; q++) { + fnArray.splice(iEndText, 3); + argsArray.splice(iEndText, 3); + iEndText += 2; + } + + return iEndText + 1; + }); + + function QueueOptimizer() {} + + QueueOptimizer.prototype = { + optimize: function QueueOptimizer_optimize(queue) { + var fnArray = queue.fnArray, argsArray = queue.argsArray; + var context = { + iCurr: 0, + fnArray: fnArray, + argsArray: argsArray + }; + var state; + var i = 0, ii = fnArray.length; + while (i < ii) { + state = (state || InitialState)[fnArray[i]]; + if (typeof state === 'function') { // we found some handler + context.iCurr = i; + // state() returns the index of the first non-matching op (if we + // didn't match) or the first op past the modified ops (if we did + // match and replace). + i = state(context); + state = undefined; // reset the state machine + ii = context.fnArray.length; + } else { + i++; + } + } + } + }; + return QueueOptimizer; +})(); + + +var BUILT_IN_CMAPS = [ +// << Start unicode maps. +'Adobe-GB1-UCS2', +'Adobe-CNS1-UCS2', +'Adobe-Japan1-UCS2', +'Adobe-Korea1-UCS2', +// >> End unicode maps. +'78-EUC-H', +'78-EUC-V', +'78-H', +'78-RKSJ-H', +'78-RKSJ-V', +'78-V', +'78ms-RKSJ-H', +'78ms-RKSJ-V', +'83pv-RKSJ-H', +'90ms-RKSJ-H', +'90ms-RKSJ-V', +'90msp-RKSJ-H', +'90msp-RKSJ-V', +'90pv-RKSJ-H', +'90pv-RKSJ-V', +'Add-H', +'Add-RKSJ-H', +'Add-RKSJ-V', +'Add-V', +'Adobe-CNS1-0', +'Adobe-CNS1-1', +'Adobe-CNS1-2', +'Adobe-CNS1-3', +'Adobe-CNS1-4', +'Adobe-CNS1-5', +'Adobe-CNS1-6', +'Adobe-GB1-0', +'Adobe-GB1-1', +'Adobe-GB1-2', +'Adobe-GB1-3', +'Adobe-GB1-4', +'Adobe-GB1-5', +'Adobe-Japan1-0', +'Adobe-Japan1-1', +'Adobe-Japan1-2', +'Adobe-Japan1-3', +'Adobe-Japan1-4', +'Adobe-Japan1-5', +'Adobe-Japan1-6', +'Adobe-Korea1-0', +'Adobe-Korea1-1', +'Adobe-Korea1-2', +'B5-H', +'B5-V', +'B5pc-H', +'B5pc-V', +'CNS-EUC-H', +'CNS-EUC-V', +'CNS1-H', +'CNS1-V', +'CNS2-H', +'CNS2-V', +'ETHK-B5-H', +'ETHK-B5-V', +'ETen-B5-H', +'ETen-B5-V', +'ETenms-B5-H', +'ETenms-B5-V', +'EUC-H', +'EUC-V', +'Ext-H', +'Ext-RKSJ-H', +'Ext-RKSJ-V', +'Ext-V', +'GB-EUC-H', +'GB-EUC-V', +'GB-H', +'GB-V', +'GBK-EUC-H', +'GBK-EUC-V', +'GBK2K-H', +'GBK2K-V', +'GBKp-EUC-H', +'GBKp-EUC-V', +'GBT-EUC-H', +'GBT-EUC-V', +'GBT-H', +'GBT-V', +'GBTpc-EUC-H', +'GBTpc-EUC-V', +'GBpc-EUC-H', +'GBpc-EUC-V', +'H', +'HKdla-B5-H', +'HKdla-B5-V', +'HKdlb-B5-H', +'HKdlb-B5-V', +'HKgccs-B5-H', +'HKgccs-B5-V', +'HKm314-B5-H', +'HKm314-B5-V', +'HKm471-B5-H', +'HKm471-B5-V', +'HKscs-B5-H', +'HKscs-B5-V', +'Hankaku', +'Hiragana', +'KSC-EUC-H', +'KSC-EUC-V', +'KSC-H', +'KSC-Johab-H', +'KSC-Johab-V', +'KSC-V', +'KSCms-UHC-H', +'KSCms-UHC-HW-H', +'KSCms-UHC-HW-V', +'KSCms-UHC-V', +'KSCpc-EUC-H', +'KSCpc-EUC-V', +'Katakana', +'NWP-H', +'NWP-V', +'RKSJ-H', +'RKSJ-V', +'Roman', +'UniCNS-UCS2-H', +'UniCNS-UCS2-V', +'UniCNS-UTF16-H', +'UniCNS-UTF16-V', +'UniCNS-UTF32-H', +'UniCNS-UTF32-V', +'UniCNS-UTF8-H', +'UniCNS-UTF8-V', +'UniGB-UCS2-H', +'UniGB-UCS2-V', +'UniGB-UTF16-H', +'UniGB-UTF16-V', +'UniGB-UTF32-H', +'UniGB-UTF32-V', +'UniGB-UTF8-H', +'UniGB-UTF8-V', +'UniJIS-UCS2-H', +'UniJIS-UCS2-HW-H', +'UniJIS-UCS2-HW-V', +'UniJIS-UCS2-V', +'UniJIS-UTF16-H', +'UniJIS-UTF16-V', +'UniJIS-UTF32-H', +'UniJIS-UTF32-V', +'UniJIS-UTF8-H', +'UniJIS-UTF8-V', +'UniJIS2004-UTF16-H', +'UniJIS2004-UTF16-V', +'UniJIS2004-UTF32-H', +'UniJIS2004-UTF32-V', +'UniJIS2004-UTF8-H', +'UniJIS2004-UTF8-V', +'UniJISPro-UCS2-HW-V', +'UniJISPro-UCS2-V', +'UniJISPro-UTF8-V', +'UniJISX0213-UTF32-H', +'UniJISX0213-UTF32-V', +'UniJISX02132004-UTF32-H', +'UniJISX02132004-UTF32-V', +'UniKS-UCS2-H', +'UniKS-UCS2-V', +'UniKS-UTF16-H', +'UniKS-UTF16-V', +'UniKS-UTF32-H', +'UniKS-UTF32-V', +'UniKS-UTF8-H', +'UniKS-UTF8-V', +'V', +'WP-Symbol']; + +// CMap, not to be confused with TrueType's cmap. +var CMap = (function CMapClosure() { + function CMap(builtInCMap) { + // Codespace ranges are stored as follows: + // [[1BytePairs], [2BytePairs], [3BytePairs], [4BytePairs]] + // where nBytePairs are ranges e.g. [low1, high1, low2, high2, ...] + this.codespaceRanges = [[], [], [], []]; + this.numCodespaceRanges = 0; + // Map entries have one of two forms. + // - cid chars are 16-bit unsigned integers, stored as integers. + // - bf chars are variable-length byte sequences, stored as strings, with + // one byte per character. + this._map = []; + this.name = ''; + this.vertical = false; + this.useCMap = null; + this.builtInCMap = builtInCMap; + } + CMap.prototype = { + addCodespaceRange: function(n, low, high) { + this.codespaceRanges[n - 1].push(low, high); + this.numCodespaceRanges++; + }, + + mapCidRange: function(low, high, dstLow) { + while (low <= high) { + this._map[low++] = dstLow++; + } + }, + + mapBfRange: function(low, high, dstLow) { + var lastByte = dstLow.length - 1; + while (low <= high) { + this._map[low++] = dstLow; + // Only the last byte has to be incremented. + dstLow = dstLow.substr(0, lastByte) + + String.fromCharCode(dstLow.charCodeAt(lastByte) + 1); + } + }, + + mapBfRangeToArray: function(low, high, array) { + var i = 0, ii = array.length; + while (low <= high && i < ii) { + this._map[low] = array[i++]; + ++low; + } + }, + + // This is used for both bf and cid chars. + mapOne: function(src, dst) { + this._map[src] = dst; + }, + + lookup: function(code) { + return this._map[code]; + }, + + contains: function(code) { + return this._map[code] !== undefined; + }, + + forEach: function(callback) { + // Most maps have fewer than 65536 entries, and for those we use normal + // array iteration. But really sparse tables are possible -- e.g. with + // indices in the *billions*. For such tables we use for..in, which isn't + // ideal because it stringifies the indices for all present elements, but + // it does avoid iterating over every undefined entry. + var map = this._map; + var length = map.length; + var i; + if (length <= 0x10000) { + for (i = 0; i < length; i++) { + if (map[i] !== undefined) { + callback(i, map[i]); + } + } + } else { + for (i in this._map) { + callback(i, map[i]); + } + } + }, + + charCodeOf: function(value) { + return this._map.indexOf(value); + }, + + getMap: function() { + return this._map; + }, + + readCharCode: function(str, offset, out) { + var c = 0; + var codespaceRanges = this.codespaceRanges; + var codespaceRangesLen = this.codespaceRanges.length; + // 9.7.6.2 CMap Mapping + // The code length is at most 4. + for (var n = 0; n < codespaceRangesLen; n++) { + c = ((c << 8) | str.charCodeAt(offset + n)) >>> 0; + // Check each codespace range to see if it falls within. + var codespaceRange = codespaceRanges[n]; + for (var k = 0, kk = codespaceRange.length; k < kk;) { + var low = codespaceRange[k++]; + var high = codespaceRange[k++]; + if (c >= low && c <= high) { + out.charcode = c; + out.length = n + 1; + return; + } + } + } + out.charcode = 0; + out.length = 1; + }, + + get isIdentityCMap() { + if (!(this.name === 'Identity-H' || this.name === 'Identity-V')) { + return false; + } + if (this._map.length !== 0x10000) { + return false; + } + for (var i = 0; i < 0x10000; i++) { + if (this._map[i] !== i) { + return false; + } + } + return true; + } + }; + return CMap; +})(); + +// A special case of CMap, where the _map array implicitly has a length of +// 65536 and each element is equal to its index. +var IdentityCMap = (function IdentityCMapClosure() { + function IdentityCMap(vertical, n) { + CMap.call(this); + this.vertical = vertical; + this.addCodespaceRange(n, 0, 0xffff); + } + Util.inherit(IdentityCMap, CMap, {}); + + IdentityCMap.prototype = { + addCodespaceRange: CMap.prototype.addCodespaceRange, + + mapCidRange: function(low, high, dstLow) { + error('should not call mapCidRange'); + }, + + mapBfRange: function(low, high, dstLow) { + error('should not call mapBfRange'); + }, + + mapBfRangeToArray: function(low, high, array) { + error('should not call mapBfRangeToArray'); + }, + + mapOne: function(src, dst) { + error('should not call mapCidOne'); + }, + + lookup: function(code) { + return (isInt(code) && code <= 0xffff) ? code : undefined; + }, + + contains: function(code) { + return isInt(code) && code <= 0xffff; + }, + + forEach: function(callback) { + for (var i = 0; i <= 0xffff; i++) { + callback(i, i); + } + }, + + charCodeOf: function(value) { + return (isInt(value) && value <= 0xffff) ? value : -1; + }, + + getMap: function() { + // Sometimes identity maps must be instantiated, but it's rare. + var map = new Array(0x10000); + for (var i = 0; i <= 0xffff; i++) { + map[i] = i; + } + return map; + }, + + readCharCode: CMap.prototype.readCharCode, + + get isIdentityCMap() { + error('should not access .isIdentityCMap'); + } + }; + + return IdentityCMap; +})(); + +var BinaryCMapReader = (function BinaryCMapReaderClosure() { + function fetchBinaryData(url) { + var nonBinaryRequest = PDFJS.disableWorker; + var request = new XMLHttpRequest(); + request.open('GET', url, false); + if (!nonBinaryRequest) { + try { + request.responseType = 'arraybuffer'; + nonBinaryRequest = request.responseType !== 'arraybuffer'; + } catch (e) { + nonBinaryRequest = true; + } + } + if (nonBinaryRequest && request.overrideMimeType) { + request.overrideMimeType('text/plain; charset=x-user-defined'); + } + request.send(null); + if (nonBinaryRequest ? !request.responseText : !request.response) { + error('Unable to get binary cMap at: ' + url); + } + if (nonBinaryRequest) { + var data = Array.prototype.map.call(request.responseText, function (ch) { + return ch.charCodeAt(0) & 255; + }); + return new Uint8Array(data); + } + return new Uint8Array(request.response); + } + + function hexToInt(a, size) { + var n = 0; + for (var i = 0; i <= size; i++) { + n = (n << 8) | a[i]; + } + return n >>> 0; + } + + function hexToStr(a, size) { + // This code is hot. Special-case some common values to avoid creating an + // object with subarray(). + if (size === 1) { + return String.fromCharCode(a[0], a[1]); + } + if (size === 3) { + return String.fromCharCode(a[0], a[1], a[2], a[3]); + } + return String.fromCharCode.apply(null, a.subarray(0, size + 1)); + } + + function addHex(a, b, size) { + var c = 0; + for (var i = size; i >= 0; i--) { + c += a[i] + b[i]; + a[i] = c & 255; + c >>= 8; + } + } + + function incHex(a, size) { + var c = 1; + for (var i = size; i >= 0 && c > 0; i--) { + c += a[i]; + a[i] = c & 255; + c >>= 8; + } + } + + var MAX_NUM_SIZE = 16; + var MAX_ENCODED_NUM_SIZE = 19; // ceil(MAX_NUM_SIZE * 7 / 8) + + function BinaryCMapStream(data) { + this.buffer = data; + this.pos = 0; + this.end = data.length; + this.tmpBuf = new Uint8Array(MAX_ENCODED_NUM_SIZE); + } + + BinaryCMapStream.prototype = { + readByte: function () { + if (this.pos >= this.end) { + return -1; + } + return this.buffer[this.pos++]; + }, + readNumber: function () { + var n = 0; + var last; + do { + var b = this.readByte(); + if (b < 0) { + error('unexpected EOF in bcmap'); + } + last = !(b & 0x80); + n = (n << 7) | (b & 0x7F); + } while (!last); + return n; + }, + readSigned: function () { + var n = this.readNumber(); + return (n & 1) ? ~(n >>> 1) : n >>> 1; + }, + readHex: function (num, size) { + num.set(this.buffer.subarray(this.pos, + this.pos + size + 1)); + this.pos += size + 1; + }, + readHexNumber: function (num, size) { + var last; + var stack = this.tmpBuf, sp = 0; + do { + var b = this.readByte(); + if (b < 0) { + error('unexpected EOF in bcmap'); + } + last = !(b & 0x80); + stack[sp++] = b & 0x7F; + } while (!last); + var i = size, buffer = 0, bufferSize = 0; + while (i >= 0) { + while (bufferSize < 8 && stack.length > 0) { + buffer = (stack[--sp] << bufferSize) | buffer; + bufferSize += 7; + } + num[i] = buffer & 255; + i--; + buffer >>= 8; + bufferSize -= 8; + } + }, + readHexSigned: function (num, size) { + this.readHexNumber(num, size); + var sign = num[size] & 1 ? 255 : 0; + var c = 0; + for (var i = 0; i <= size; i++) { + c = ((c & 1) << 8) | num[i]; + num[i] = (c >> 1) ^ sign; + } + }, + readString: function () { + var len = this.readNumber(); + var s = ''; + for (var i = 0; i < len; i++) { + s += String.fromCharCode(this.readNumber()); + } + return s; + } + }; + + function processBinaryCMap(url, cMap, extend) { + var data = fetchBinaryData(url); + var stream = new BinaryCMapStream(data); + + var header = stream.readByte(); + cMap.vertical = !!(header & 1); + + var useCMap = null; + var start = new Uint8Array(MAX_NUM_SIZE); + var end = new Uint8Array(MAX_NUM_SIZE); + var char = new Uint8Array(MAX_NUM_SIZE); + var charCode = new Uint8Array(MAX_NUM_SIZE); + var tmp = new Uint8Array(MAX_NUM_SIZE); + var code; + + var b; + while ((b = stream.readByte()) >= 0) { + var type = b >> 5; + if (type === 7) { // metadata, e.g. comment or usecmap + switch (b & 0x1F) { + case 0: + stream.readString(); // skipping comment + break; + case 1: + useCMap = stream.readString(); + break; + } + continue; + } + var sequence = !!(b & 0x10); + var dataSize = b & 15; + + assert(dataSize + 1 <= MAX_NUM_SIZE); + + var ucs2DataSize = 1; + var subitemsCount = stream.readNumber(); + var i; + switch (type) { + case 0: // codespacerange + stream.readHex(start, dataSize); + stream.readHexNumber(end, dataSize); + addHex(end, start, dataSize); + cMap.addCodespaceRange(dataSize + 1, hexToInt(start, dataSize), + hexToInt(end, dataSize)); + for (i = 1; i < subitemsCount; i++) { + incHex(end, dataSize); + stream.readHexNumber(start, dataSize); + addHex(start, end, dataSize); + stream.readHexNumber(end, dataSize); + addHex(end, start, dataSize); + cMap.addCodespaceRange(dataSize + 1, hexToInt(start, dataSize), + hexToInt(end, dataSize)); + } + break; + case 1: // notdefrange + stream.readHex(start, dataSize); + stream.readHexNumber(end, dataSize); + addHex(end, start, dataSize); + code = stream.readNumber(); + // undefined range, skipping + for (i = 1; i < subitemsCount; i++) { + incHex(end, dataSize); + stream.readHexNumber(start, dataSize); + addHex(start, end, dataSize); + stream.readHexNumber(end, dataSize); + addHex(end, start, dataSize); + code = stream.readNumber(); + // nop + } + break; + case 2: // cidchar + stream.readHex(char, dataSize); + code = stream.readNumber(); + cMap.mapOne(hexToInt(char, dataSize), code); + for (i = 1; i < subitemsCount; i++) { + incHex(char, dataSize); + if (!sequence) { + stream.readHexNumber(tmp, dataSize); + addHex(char, tmp, dataSize); + } + code = stream.readSigned() + (code + 1); + cMap.mapOne(hexToInt(char, dataSize), code); + } + break; + case 3: // cidrange + stream.readHex(start, dataSize); + stream.readHexNumber(end, dataSize); + addHex(end, start, dataSize); + code = stream.readNumber(); + cMap.mapCidRange(hexToInt(start, dataSize), hexToInt(end, dataSize), + code); + for (i = 1; i < subitemsCount; i++) { + incHex(end, dataSize); + if (!sequence) { + stream.readHexNumber(start, dataSize); + addHex(start, end, dataSize); + } else { + start.set(end); + } + stream.readHexNumber(end, dataSize); + addHex(end, start, dataSize); + code = stream.readNumber(); + cMap.mapCidRange(hexToInt(start, dataSize), hexToInt(end, dataSize), + code); + } + break; + case 4: // bfchar + stream.readHex(char, ucs2DataSize); + stream.readHex(charCode, dataSize); + cMap.mapOne(hexToInt(char, ucs2DataSize), + hexToStr(charCode, dataSize)); + for (i = 1; i < subitemsCount; i++) { + incHex(char, ucs2DataSize); + if (!sequence) { + stream.readHexNumber(tmp, ucs2DataSize); + addHex(char, tmp, ucs2DataSize); + } + incHex(charCode, dataSize); + stream.readHexSigned(tmp, dataSize); + addHex(charCode, tmp, dataSize); + cMap.mapOne(hexToInt(char, ucs2DataSize), + hexToStr(charCode, dataSize)); + } + break; + case 5: // bfrange + stream.readHex(start, ucs2DataSize); + stream.readHexNumber(end, ucs2DataSize); + addHex(end, start, ucs2DataSize); + stream.readHex(charCode, dataSize); + cMap.mapBfRange(hexToInt(start, ucs2DataSize), + hexToInt(end, ucs2DataSize), + hexToStr(charCode, dataSize)); + for (i = 1; i < subitemsCount; i++) { + incHex(end, ucs2DataSize); + if (!sequence) { + stream.readHexNumber(start, ucs2DataSize); + addHex(start, end, ucs2DataSize); + } else { + start.set(end); + } + stream.readHexNumber(end, ucs2DataSize); + addHex(end, start, ucs2DataSize); + stream.readHex(charCode, dataSize); + cMap.mapBfRange(hexToInt(start, ucs2DataSize), + hexToInt(end, ucs2DataSize), + hexToStr(charCode, dataSize)); + } + break; + default: + error('Unknown type: ' + type); + break; + } + } + + if (useCMap) { + extend(useCMap); + } + return cMap; + } + + function BinaryCMapReader() {} + + BinaryCMapReader.prototype = { + read: processBinaryCMap + }; + + return BinaryCMapReader; +})(); + +var CMapFactory = (function CMapFactoryClosure() { + function strToInt(str) { + var a = 0; + for (var i = 0; i < str.length; i++) { + a = (a << 8) | str.charCodeAt(i); + } + return a >>> 0; + } + + function expectString(obj) { + if (!isString(obj)) { + error('Malformed CMap: expected string.'); + } + } + + function expectInt(obj) { + if (!isInt(obj)) { + error('Malformed CMap: expected int.'); + } + } + + function parseBfChar(cMap, lexer) { + while (true) { + var obj = lexer.getObj(); + if (isEOF(obj)) { + break; + } + if (isCmd(obj, 'endbfchar')) { + return; + } + expectString(obj); + var src = strToInt(obj); + obj = lexer.getObj(); + // TODO are /dstName used? + expectString(obj); + var dst = obj; + cMap.mapOne(src, dst); + } + } + + function parseBfRange(cMap, lexer) { + while (true) { + var obj = lexer.getObj(); + if (isEOF(obj)) { + break; + } + if (isCmd(obj, 'endbfrange')) { + return; + } + expectString(obj); + var low = strToInt(obj); + obj = lexer.getObj(); + expectString(obj); + var high = strToInt(obj); + obj = lexer.getObj(); + if (isInt(obj) || isString(obj)) { + var dstLow = isInt(obj) ? String.fromCharCode(obj) : obj; + cMap.mapBfRange(low, high, dstLow); + } else if (isCmd(obj, '[')) { + obj = lexer.getObj(); + var array = []; + while (!isCmd(obj, ']') && !isEOF(obj)) { + array.push(obj); + obj = lexer.getObj(); + } + cMap.mapBfRangeToArray(low, high, array); + } else { + break; + } + } + error('Invalid bf range.'); + } + + function parseCidChar(cMap, lexer) { + while (true) { + var obj = lexer.getObj(); + if (isEOF(obj)) { + break; + } + if (isCmd(obj, 'endcidchar')) { + return; + } + expectString(obj); + var src = strToInt(obj); + obj = lexer.getObj(); + expectInt(obj); + var dst = obj; + cMap.mapOne(src, dst); + } + } + + function parseCidRange(cMap, lexer) { + while (true) { + var obj = lexer.getObj(); + if (isEOF(obj)) { + break; + } + if (isCmd(obj, 'endcidrange')) { + return; + } + expectString(obj); + var low = strToInt(obj); + obj = lexer.getObj(); + expectString(obj); + var high = strToInt(obj); + obj = lexer.getObj(); + expectInt(obj); + var dstLow = obj; + cMap.mapCidRange(low, high, dstLow); + } + } + + function parseCodespaceRange(cMap, lexer) { + while (true) { + var obj = lexer.getObj(); + if (isEOF(obj)) { + break; + } + if (isCmd(obj, 'endcodespacerange')) { + return; + } + if (!isString(obj)) { + break; + } + var low = strToInt(obj); + obj = lexer.getObj(); + if (!isString(obj)) { + break; + } + var high = strToInt(obj); + cMap.addCodespaceRange(obj.length, low, high); + } + error('Invalid codespace range.'); + } + + function parseWMode(cMap, lexer) { + var obj = lexer.getObj(); + if (isInt(obj)) { + cMap.vertical = !!obj; + } + } + + function parseCMapName(cMap, lexer) { + var obj = lexer.getObj(); + if (isName(obj) && isString(obj.name)) { + cMap.name = obj.name; + } + } + + function parseCMap(cMap, lexer, builtInCMapParams, useCMap) { + var previous; + var embededUseCMap; + objLoop: while (true) { + var obj = lexer.getObj(); + if (isEOF(obj)) { + break; + } else if (isName(obj)) { + if (obj.name === 'WMode') { + parseWMode(cMap, lexer); + } else if (obj.name === 'CMapName') { + parseCMapName(cMap, lexer); + } + previous = obj; + } else if (isCmd(obj)) { + switch (obj.cmd) { + case 'endcmap': + break objLoop; + case 'usecmap': + if (isName(previous)) { + embededUseCMap = previous.name; + } + break; + case 'begincodespacerange': + parseCodespaceRange(cMap, lexer); + break; + case 'beginbfchar': + parseBfChar(cMap, lexer); + break; + case 'begincidchar': + parseCidChar(cMap, lexer); + break; + case 'beginbfrange': + parseBfRange(cMap, lexer); + break; + case 'begincidrange': + parseCidRange(cMap, lexer); + break; + } + } + } + + if (!useCMap && embededUseCMap) { + // Load the usecmap definition from the file only if there wasn't one + // specified. + useCMap = embededUseCMap; + } + if (useCMap) { + extendCMap(cMap, builtInCMapParams, useCMap); + } + } + + function extendCMap(cMap, builtInCMapParams, useCMap) { + cMap.useCMap = createBuiltInCMap(useCMap, builtInCMapParams); + // If there aren't any code space ranges defined clone all the parent ones + // into this cMap. + if (cMap.numCodespaceRanges === 0) { + var useCodespaceRanges = cMap.useCMap.codespaceRanges; + for (var i = 0; i < useCodespaceRanges.length; i++) { + cMap.codespaceRanges[i] = useCodespaceRanges[i].slice(); + } + cMap.numCodespaceRanges = cMap.useCMap.numCodespaceRanges; + } + // Merge the map into the current one, making sure not to override + // any previously defined entries. + cMap.useCMap.forEach(function(key, value) { + if (!cMap.contains(key)) { + cMap.mapOne(key, cMap.useCMap.lookup(key)); + } + }); + } + + function parseBinaryCMap(name, builtInCMapParams) { + var url = builtInCMapParams.url + name + '.bcmap'; + var cMap = new CMap(true); + new BinaryCMapReader().read(url, cMap, function (useCMap) { + extendCMap(cMap, builtInCMapParams, useCMap); + }); + return cMap; + } + + function createBuiltInCMap(name, builtInCMapParams) { + if (name === 'Identity-H') { + return new IdentityCMap(false, 2); + } else if (name === 'Identity-V') { + return new IdentityCMap(true, 2); + } + if (BUILT_IN_CMAPS.indexOf(name) === -1) { + error('Unknown cMap name: ' + name); + } + assert(builtInCMapParams, 'built-in cMap parameters are not provided'); + + if (builtInCMapParams.packed) { + return parseBinaryCMap(name, builtInCMapParams); + } + + var request = new XMLHttpRequest(); + var url = builtInCMapParams.url + name; + request.open('GET', url, false); + request.send(null); + if (!request.responseText) { + error('Unable to get cMap at: ' + url); + } + var cMap = new CMap(true); + var lexer = new Lexer(new StringStream(request.responseText)); + parseCMap(cMap, lexer, builtInCMapParams, null); + return cMap; + } + + return { + create: function (encoding, builtInCMapParams, useCMap) { + if (isName(encoding)) { + return createBuiltInCMap(encoding.name, builtInCMapParams); + } else if (isStream(encoding)) { + var cMap = new CMap(); + var lexer = new Lexer(encoding); + try { + parseCMap(cMap, lexer, builtInCMapParams, useCMap); + } catch (e) { + warn('Invalid CMap data. ' + e); + } + if (cMap.isIdentityCMap) { + return createBuiltInCMap(cMap.name, builtInCMapParams); + } + return cMap; + } + error('Encoding required.'); + } + }; +})(); + + +// Unicode Private Use Area +var PRIVATE_USE_OFFSET_START = 0xE000; +var PRIVATE_USE_OFFSET_END = 0xF8FF; +var SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = false; + +// PDF Glyph Space Units are one Thousandth of a TextSpace Unit +// except for Type 3 fonts +var PDF_GLYPH_SPACE_UNITS = 1000; + +// Hinting is currently disabled due to unknown problems on windows +// in tracemonkey and various other pdfs with type1 fonts. +var HINTING_ENABLED = false; + +// Accented charactars are not displayed properly on windows, using this flag +// to control analysis of seac charstrings. +var SEAC_ANALYSIS_ENABLED = false; + +var FontFlags = { + FixedPitch: 1, + Serif: 2, + Symbolic: 4, + Script: 8, + Nonsymbolic: 32, + Italic: 64, + AllCap: 65536, + SmallCap: 131072, + ForceBold: 262144 +}; + +var Encodings = { + ExpertEncoding: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'space', 'exclamsmall', 'Hungarumlautsmall', '', 'dollaroldstyle', + 'dollarsuperior', 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', + 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma', + 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', + 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', + 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon', + 'semicolon', 'commasuperior', 'threequartersemdash', 'periodsuperior', + 'questionsmall', '', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', + 'esuperior', '', '', 'isuperior', '', '', 'lsuperior', 'msuperior', + 'nsuperior', 'osuperior', '', '', 'rsuperior', 'ssuperior', 'tsuperior', + '', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', '', + 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', + 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', + 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', + 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', + 'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', + 'onefitted', 'rupiah', 'Tildesmall', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', + '', '', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', + 'Caronsmall', '', 'Dotaccentsmall', '', '', 'Macronsmall', '', '', + 'figuredash', 'hypheninferior', '', '', 'Ogoneksmall', 'Ringsmall', + 'Cedillasmall', '', '', '', 'onequarter', 'onehalf', 'threequarters', + 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', + 'seveneighths', 'onethird', 'twothirds', '', '', 'zerosuperior', + 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', + 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', + 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior', + 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', + 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', + 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall', + 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', + 'Aringsmall', 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', + 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall', + 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', + 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', + 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall', + 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', + 'Ydieresissmall'], + MacExpertEncoding: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'space', 'exclamsmall', 'Hungarumlautsmall', 'centoldstyle', + 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall', + 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', + 'onedotenleader', 'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', + 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', + 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', + 'nineoldstyle', 'colon', 'semicolon', '', 'threequartersemdash', '', + 'questionsmall', '', '', '', '', 'Ethsmall', '', '', 'onequarter', + 'onehalf', 'threequarters', 'oneeighth', 'threeeighths', 'fiveeighths', + 'seveneighths', 'onethird', 'twothirds', '', '', '', '', '', '', 'ff', + 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', '', 'parenrightinferior', + 'Circumflexsmall', 'hypheninferior', 'Gravesmall', 'Asmall', 'Bsmall', + 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', + 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', + 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', + 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', + 'Tildesmall', '', '', 'asuperior', 'centsuperior', '', '', '', '', + 'Aacutesmall', 'Agravesmall', 'Acircumflexsmall', 'Adieresissmall', + 'Atildesmall', 'Aringsmall', 'Ccedillasmall', 'Eacutesmall', 'Egravesmall', + 'Ecircumflexsmall', 'Edieresissmall', 'Iacutesmall', 'Igravesmall', + 'Icircumflexsmall', 'Idieresissmall', 'Ntildesmall', 'Oacutesmall', + 'Ogravesmall', 'Ocircumflexsmall', 'Odieresissmall', 'Otildesmall', + 'Uacutesmall', 'Ugravesmall', 'Ucircumflexsmall', 'Udieresissmall', '', + 'eightsuperior', 'fourinferior', 'threeinferior', 'sixinferior', + 'eightinferior', 'seveninferior', 'Scaronsmall', '', 'centinferior', + 'twoinferior', '', 'Dieresissmall', '', 'Caronsmall', 'osuperior', + 'fiveinferior', '', 'commainferior', 'periodinferior', 'Yacutesmall', '', + 'dollarinferior', '', 'Thornsmall', '', 'nineinferior', 'zeroinferior', + 'Zcaronsmall', 'AEsmall', 'Oslashsmall', 'questiondownsmall', + 'oneinferior', 'Lslashsmall', '', '', '', '', '', '', 'Cedillasmall', '', + '', '', '', '', 'OEsmall', 'figuredash', 'hyphensuperior', '', '', '', '', + 'exclamdownsmall', '', 'Ydieresissmall', '', 'onesuperior', 'twosuperior', + 'threesuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', + 'sevensuperior', 'ninesuperior', 'zerosuperior', '', 'esuperior', + 'rsuperior', 'tsuperior', '', '', 'isuperior', 'ssuperior', 'dsuperior', + '', '', '', '', '', 'lsuperior', 'Ogoneksmall', 'Brevesmall', + 'Macronsmall', 'bsuperior', 'nsuperior', 'msuperior', 'commasuperior', + 'periodsuperior', 'Dotaccentsmall', 'Ringsmall'], + MacRomanEncoding: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', + 'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', + 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', + 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', + 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', + 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', '', + 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', 'Odieresis', + 'Udieresis', 'aacute', 'agrave', 'acircumflex', 'adieresis', 'atilde', + 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis', + 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', + 'ograve', 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', + 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', + 'section', 'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', + 'trademark', 'acute', 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', + 'plusminus', 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', + 'summation', 'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', + 'Omega', 'ae', 'oslash', 'questiondown', 'exclamdown', 'logicalnot', + 'radical', 'florin', 'approxequal', 'Delta', 'guillemotleft', + 'guillemotright', 'ellipsis', 'space', 'Agrave', 'Atilde', 'Otilde', 'OE', + 'oe', 'endash', 'emdash', 'quotedblleft', 'quotedblright', 'quoteleft', + 'quoteright', 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction', + 'currency', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', + 'periodcentered', 'quotesinglbase', 'quotedblbase', 'perthousand', + 'Acircumflex', 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute', + 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', + 'Ograve', 'Uacute', 'Ucircumflex', 'Ugrave', 'dotlessi', 'circumflex', + 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', + 'ogonek', 'caron'], + StandardEncoding: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', + 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', + 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', + 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', + 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', + 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', + 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'exclamdown', + 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', + 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft', + 'guilsinglright', 'fi', 'fl', '', 'endash', 'dagger', 'daggerdbl', + 'periodcentered', '', 'paragraph', 'bullet', 'quotesinglbase', + 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', + 'perthousand', '', 'questiondown', '', 'grave', 'acute', 'circumflex', + 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', '', 'ring', 'cedilla', + '', 'hungarumlaut', 'ogonek', 'caron', 'emdash', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', 'AE', '', 'ordfeminine', '', '', + '', '', 'Lslash', 'Oslash', 'OE', 'ordmasculine', '', '', '', '', '', 'ae', + '', '', '', 'dotlessi', '', '', 'lslash', 'oslash', 'oe', 'germandbls'], + WinAnsiEncoding: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', + 'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', + 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', + 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', + 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', + 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', + 'bullet', 'Euro', 'bullet', 'quotesinglbase', 'florin', 'quotedblbase', + 'ellipsis', 'dagger', 'daggerdbl', 'circumflex', 'perthousand', 'Scaron', + 'guilsinglleft', 'OE', 'bullet', 'Zcaron', 'bullet', 'bullet', 'quoteleft', + 'quoteright', 'quotedblleft', 'quotedblright', 'bullet', 'endash', + 'emdash', 'tilde', 'trademark', 'scaron', 'guilsinglright', 'oe', 'bullet', + 'zcaron', 'Ydieresis', 'space', 'exclamdown', 'cent', 'sterling', + 'currency', 'yen', 'brokenbar', 'section', 'dieresis', 'copyright', + 'ordfeminine', 'guillemotleft', 'logicalnot', 'hyphen', 'registered', + 'macron', 'degree', 'plusminus', 'twosuperior', 'threesuperior', 'acute', + 'mu', 'paragraph', 'periodcentered', 'cedilla', 'onesuperior', + 'ordmasculine', 'guillemotright', 'onequarter', 'onehalf', 'threequarters', + 'questiondown', 'Agrave', 'Aacute', 'Acircumflex', 'Atilde', 'Adieresis', + 'Aring', 'AE', 'Ccedilla', 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis', + 'Igrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Eth', 'Ntilde', 'Ograve', + 'Oacute', 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply', 'Oslash', + 'Ugrave', 'Uacute', 'Ucircumflex', 'Udieresis', 'Yacute', 'Thorn', + 'germandbls', 'agrave', 'aacute', 'acircumflex', 'atilde', 'adieresis', + 'aring', 'ae', 'ccedilla', 'egrave', 'eacute', 'ecircumflex', 'edieresis', + 'igrave', 'iacute', 'icircumflex', 'idieresis', 'eth', 'ntilde', 'ograve', + 'oacute', 'ocircumflex', 'otilde', 'odieresis', 'divide', 'oslash', + 'ugrave', 'uacute', 'ucircumflex', 'udieresis', 'yacute', 'thorn', + 'ydieresis'], + SymbolSetEncoding: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'space', 'exclam', 'universal', 'numbersign', 'existential', 'percent', + 'ampersand', 'suchthat', 'parenleft', 'parenright', 'asteriskmath', 'plus', + 'comma', 'minus', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', + 'equal', 'greater', 'question', 'congruent', 'Alpha', 'Beta', 'Chi', + 'Delta', 'Epsilon', 'Phi', 'Gamma', 'Eta', 'Iota', 'theta1', 'Kappa', + 'Lambda', 'Mu', 'Nu', 'Omicron', 'Pi', 'Theta', 'Rho', 'Sigma', 'Tau', + 'Upsilon', 'sigma1', 'Omega', 'Xi', 'Psi', 'Zeta', 'bracketleft', + 'therefore', 'bracketright', 'perpendicular', 'underscore', 'radicalex', + 'alpha', 'beta', 'chi', 'delta', 'epsilon', 'phi', 'gamma', 'eta', 'iota', + 'phi1', 'kappa', 'lambda', 'mu', 'nu', 'omicron', 'pi', 'theta', 'rho', + 'sigma', 'tau', 'upsilon', 'omega1', 'omega', 'xi', 'psi', 'zeta', + 'braceleft', 'bar', 'braceright', 'similar', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', 'Euro', 'Upsilon1', 'minute', 'lessequal', + 'fraction', 'infinity', 'florin', 'club', 'diamond', 'heart', 'spade', + 'arrowboth', 'arrowleft', 'arrowup', 'arrowright', 'arrowdown', 'degree', + 'plusminus', 'second', 'greaterequal', 'multiply', 'proportional', + 'partialdiff', 'bullet', 'divide', 'notequal', 'equivalence', + 'approxequal', 'ellipsis', 'arrowvertex', 'arrowhorizex', 'carriagereturn', + 'aleph', 'Ifraktur', 'Rfraktur', 'weierstrass', 'circlemultiply', + 'circleplus', 'emptyset', 'intersection', 'union', 'propersuperset', + 'reflexsuperset', 'notsubset', 'propersubset', 'reflexsubset', 'element', + 'notelement', 'angle', 'gradient', 'registerserif', 'copyrightserif', + 'trademarkserif', 'product', 'radical', 'dotmath', 'logicalnot', + 'logicaland', 'logicalor', 'arrowdblboth', 'arrowdblleft', 'arrowdblup', + 'arrowdblright', 'arrowdbldown', 'lozenge', 'angleleft', 'registersans', + 'copyrightsans', 'trademarksans', 'summation', 'parenlefttp', + 'parenleftex', 'parenleftbt', 'bracketlefttp', 'bracketleftex', + 'bracketleftbt', 'bracelefttp', 'braceleftmid', 'braceleftbt', 'braceex', + '', 'angleright', 'integral', 'integraltp', 'integralex', 'integralbt', + 'parenrighttp', 'parenrightex', 'parenrightbt', 'bracketrighttp', + 'bracketrightex', 'bracketrightbt', 'bracerighttp', 'bracerightmid', + 'bracerightbt'], + ZapfDingbatsEncoding: ['', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'space', 'a1', 'a2', 'a202', 'a3', 'a4', 'a5', 'a119', 'a118', 'a117', + 'a11', 'a12', 'a13', 'a14', 'a15', 'a16', 'a105', 'a17', 'a18', 'a19', + 'a20', 'a21', 'a22', 'a23', 'a24', 'a25', 'a26', 'a27', 'a28', 'a6', 'a7', + 'a8', 'a9', 'a10', 'a29', 'a30', 'a31', 'a32', 'a33', 'a34', 'a35', 'a36', + 'a37', 'a38', 'a39', 'a40', 'a41', 'a42', 'a43', 'a44', 'a45', 'a46', + 'a47', 'a48', 'a49', 'a50', 'a51', 'a52', 'a53', 'a54', 'a55', 'a56', + 'a57', 'a58', 'a59', 'a60', 'a61', 'a62', 'a63', 'a64', 'a65', 'a66', + 'a67', 'a68', 'a69', 'a70', 'a71', 'a72', 'a73', 'a74', 'a203', 'a75', + 'a204', 'a76', 'a77', 'a78', 'a79', 'a81', 'a82', 'a83', 'a84', 'a97', + 'a98', 'a99', 'a100', '', 'a89', 'a90', 'a93', 'a94', 'a91', 'a92', 'a205', + 'a85', 'a206', 'a86', 'a87', 'a88', 'a95', 'a96', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', 'a101', 'a102', 'a103', + 'a104', 'a106', 'a107', 'a108', 'a112', 'a111', 'a110', 'a109', 'a120', + 'a121', 'a122', 'a123', 'a124', 'a125', 'a126', 'a127', 'a128', 'a129', + 'a130', 'a131', 'a132', 'a133', 'a134', 'a135', 'a136', 'a137', 'a138', + 'a139', 'a140', 'a141', 'a142', 'a143', 'a144', 'a145', 'a146', 'a147', + 'a148', 'a149', 'a150', 'a151', 'a152', 'a153', 'a154', 'a155', 'a156', + 'a157', 'a158', 'a159', 'a160', 'a161', 'a163', 'a164', 'a196', 'a165', + 'a192', 'a166', 'a167', 'a168', 'a169', 'a170', 'a171', 'a172', 'a173', + 'a162', 'a174', 'a175', 'a176', 'a177', 'a178', 'a179', 'a193', 'a180', + 'a199', 'a181', 'a200', 'a182', '', 'a201', 'a183', 'a184', 'a197', 'a185', + 'a194', 'a198', 'a186', 'a195', 'a187', 'a188', 'a189', 'a190', 'a191'] +}; + +/** + * Hold a map of decoded fonts and of the standard fourteen Type1 + * fonts and their acronyms. + */ +var stdFontMap = { + 'ArialNarrow': 'Helvetica', + 'ArialNarrow-Bold': 'Helvetica-Bold', + 'ArialNarrow-BoldItalic': 'Helvetica-BoldOblique', + 'ArialNarrow-Italic': 'Helvetica-Oblique', + 'ArialBlack': 'Helvetica', + 'ArialBlack-Bold': 'Helvetica-Bold', + 'ArialBlack-BoldItalic': 'Helvetica-BoldOblique', + 'ArialBlack-Italic': 'Helvetica-Oblique', + 'Arial': 'Helvetica', + 'Arial-Bold': 'Helvetica-Bold', + 'Arial-BoldItalic': 'Helvetica-BoldOblique', + 'Arial-Italic': 'Helvetica-Oblique', + 'Arial-BoldItalicMT': 'Helvetica-BoldOblique', + 'Arial-BoldMT': 'Helvetica-Bold', + 'Arial-ItalicMT': 'Helvetica-Oblique', + 'ArialMT': 'Helvetica', + 'Courier-Bold': 'Courier-Bold', + 'Courier-BoldItalic': 'Courier-BoldOblique', + 'Courier-Italic': 'Courier-Oblique', + 'CourierNew': 'Courier', + 'CourierNew-Bold': 'Courier-Bold', + 'CourierNew-BoldItalic': 'Courier-BoldOblique', + 'CourierNew-Italic': 'Courier-Oblique', + 'CourierNewPS-BoldItalicMT': 'Courier-BoldOblique', + 'CourierNewPS-BoldMT': 'Courier-Bold', + 'CourierNewPS-ItalicMT': 'Courier-Oblique', + 'CourierNewPSMT': 'Courier', + 'Helvetica': 'Helvetica', + 'Helvetica-Bold': 'Helvetica-Bold', + 'Helvetica-BoldItalic': 'Helvetica-BoldOblique', + 'Helvetica-BoldOblique': 'Helvetica-BoldOblique', + 'Helvetica-Italic': 'Helvetica-Oblique', + 'Helvetica-Oblique':'Helvetica-Oblique', + 'Symbol-Bold': 'Symbol', + 'Symbol-BoldItalic': 'Symbol', + 'Symbol-Italic': 'Symbol', + 'TimesNewRoman': 'Times-Roman', + 'TimesNewRoman-Bold': 'Times-Bold', + 'TimesNewRoman-BoldItalic': 'Times-BoldItalic', + 'TimesNewRoman-Italic': 'Times-Italic', + 'TimesNewRomanPS': 'Times-Roman', + 'TimesNewRomanPS-Bold': 'Times-Bold', + 'TimesNewRomanPS-BoldItalic': 'Times-BoldItalic', + 'TimesNewRomanPS-BoldItalicMT': 'Times-BoldItalic', + 'TimesNewRomanPS-BoldMT': 'Times-Bold', + 'TimesNewRomanPS-Italic': 'Times-Italic', + 'TimesNewRomanPS-ItalicMT': 'Times-Italic', + 'TimesNewRomanPSMT': 'Times-Roman', + 'TimesNewRomanPSMT-Bold': 'Times-Bold', + 'TimesNewRomanPSMT-BoldItalic': 'Times-BoldItalic', + 'TimesNewRomanPSMT-Italic': 'Times-Italic' +}; + +/** + * Holds the map of the non-standard fonts that might be included as a standard + * fonts without glyph data. + */ +var nonStdFontMap = { + 'CenturyGothic': 'Helvetica', + 'CenturyGothic-Bold': 'Helvetica-Bold', + 'CenturyGothic-BoldItalic': 'Helvetica-BoldOblique', + 'CenturyGothic-Italic': 'Helvetica-Oblique', + 'ComicSansMS': 'Comic Sans MS', + 'ComicSansMS-Bold': 'Comic Sans MS-Bold', + 'ComicSansMS-BoldItalic': 'Comic Sans MS-BoldItalic', + 'ComicSansMS-Italic': 'Comic Sans MS-Italic', + 'LucidaConsole': 'Courier', + 'LucidaConsole-Bold': 'Courier-Bold', + 'LucidaConsole-BoldItalic': 'Courier-BoldOblique', + 'LucidaConsole-Italic': 'Courier-Oblique', + 'MS-Gothic': 'MS Gothic', + 'MS-Gothic-Bold': 'MS Gothic-Bold', + 'MS-Gothic-BoldItalic': 'MS Gothic-BoldItalic', + 'MS-Gothic-Italic': 'MS Gothic-Italic', + 'MS-Mincho': 'MS Mincho', + 'MS-Mincho-Bold': 'MS Mincho-Bold', + 'MS-Mincho-BoldItalic': 'MS Mincho-BoldItalic', + 'MS-Mincho-Italic': 'MS Mincho-Italic', + 'MS-PGothic': 'MS PGothic', + 'MS-PGothic-Bold': 'MS PGothic-Bold', + 'MS-PGothic-BoldItalic': 'MS PGothic-BoldItalic', + 'MS-PGothic-Italic': 'MS PGothic-Italic', + 'MS-PMincho': 'MS PMincho', + 'MS-PMincho-Bold': 'MS PMincho-Bold', + 'MS-PMincho-BoldItalic': 'MS PMincho-BoldItalic', + 'MS-PMincho-Italic': 'MS PMincho-Italic', + 'Wingdings': 'ZapfDingbats' +}; + +var serifFonts = { + 'Adobe Jenson': true, 'Adobe Text': true, 'Albertus': true, + 'Aldus': true, 'Alexandria': true, 'Algerian': true, + 'American Typewriter': true, 'Antiqua': true, 'Apex': true, + 'Arno': true, 'Aster': true, 'Aurora': true, + 'Baskerville': true, 'Bell': true, 'Bembo': true, + 'Bembo Schoolbook': true, 'Benguiat': true, 'Berkeley Old Style': true, + 'Bernhard Modern': true, 'Berthold City': true, 'Bodoni': true, + 'Bauer Bodoni': true, 'Book Antiqua': true, 'Bookman': true, + 'Bordeaux Roman': true, 'Californian FB': true, 'Calisto': true, + 'Calvert': true, 'Capitals': true, 'Cambria': true, + 'Cartier': true, 'Caslon': true, 'Catull': true, + 'Centaur': true, 'Century Old Style': true, 'Century Schoolbook': true, + 'Chaparral': true, 'Charis SIL': true, 'Cheltenham': true, + 'Cholla Slab': true, 'Clarendon': true, 'Clearface': true, + 'Cochin': true, 'Colonna': true, 'Computer Modern': true, + 'Concrete Roman': true, 'Constantia': true, 'Cooper Black': true, + 'Corona': true, 'Ecotype': true, 'Egyptienne': true, + 'Elephant': true, 'Excelsior': true, 'Fairfield': true, + 'FF Scala': true, 'Folkard': true, 'Footlight': true, + 'FreeSerif': true, 'Friz Quadrata': true, 'Garamond': true, + 'Gentium': true, 'Georgia': true, 'Gloucester': true, + 'Goudy Old Style': true, 'Goudy Schoolbook': true, 'Goudy Pro Font': true, + 'Granjon': true, 'Guardian Egyptian': true, 'Heather': true, + 'Hercules': true, 'High Tower Text': true, 'Hiroshige': true, + 'Hoefler Text': true, 'Humana Serif': true, 'Imprint': true, + 'Ionic No. 5': true, 'Janson': true, 'Joanna': true, + 'Korinna': true, 'Lexicon': true, 'Liberation Serif': true, + 'Linux Libertine': true, 'Literaturnaya': true, 'Lucida': true, + 'Lucida Bright': true, 'Melior': true, 'Memphis': true, + 'Miller': true, 'Minion': true, 'Modern': true, + 'Mona Lisa': true, 'Mrs Eaves': true, 'MS Serif': true, + 'Museo Slab': true, 'New York': true, 'Nimbus Roman': true, + 'NPS Rawlinson Roadway': true, 'Palatino': true, 'Perpetua': true, + 'Plantin': true, 'Plantin Schoolbook': true, 'Playbill': true, + 'Poor Richard': true, 'Rawlinson Roadway': true, 'Renault': true, + 'Requiem': true, 'Rockwell': true, 'Roman': true, + 'Rotis Serif': true, 'Sabon': true, 'Scala': true, + 'Seagull': true, 'Sistina': true, 'Souvenir': true, + 'STIX': true, 'Stone Informal': true, 'Stone Serif': true, + 'Sylfaen': true, 'Times': true, 'Trajan': true, + 'Trinité': true, 'Trump Mediaeval': true, 'Utopia': true, + 'Vale Type': true, 'Bitstream Vera': true, 'Vera Serif': true, + 'Versailles': true, 'Wanted': true, 'Weiss': true, + 'Wide Latin': true, 'Windsor': true, 'XITS': true +}; + +var symbolsFonts = { + 'Dingbats': true, 'Symbol': true, 'ZapfDingbats': true +}; + +// Glyph map for well-known standard fonts. Sometimes Ghostscript uses CID fonts +// but does not embed the CID to GID mapping. The mapping is incomplete for all +// glyphs, but common for some set of the standard fonts. +var GlyphMapForStandardFonts = { + '2': 10, '3': 32, '4': 33, '5': 34, '6': 35, '7': 36, '8': 37, '9': 38, + '10': 39, '11': 40, '12': 41, '13': 42, '14': 43, '15': 44, '16': 45, + '17': 46, '18': 47, '19': 48, '20': 49, '21': 50, '22': 51, '23': 52, + '24': 53, '25': 54, '26': 55, '27': 56, '28': 57, '29': 58, '30': 894, + '31': 60, '32': 61, '33': 62, '34': 63, '35': 64, '36': 65, '37': 66, + '38': 67, '39': 68, '40': 69, '41': 70, '42': 71, '43': 72, '44': 73, + '45': 74, '46': 75, '47': 76, '48': 77, '49': 78, '50': 79, '51': 80, + '52': 81, '53': 82, '54': 83, '55': 84, '56': 85, '57': 86, '58': 87, + '59': 88, '60': 89, '61': 90, '62': 91, '63': 92, '64': 93, '65': 94, + '66': 95, '67': 96, '68': 97, '69': 98, '70': 99, '71': 100, '72': 101, + '73': 102, '74': 103, '75': 104, '76': 105, '77': 106, '78': 107, '79': 108, + '80': 109, '81': 110, '82': 111, '83': 112, '84': 113, '85': 114, '86': 115, + '87': 116, '88': 117, '89': 118, '90': 119, '91': 120, '92': 121, '93': 122, + '94': 123, '95': 124, '96': 125, '97': 126, '98': 196, '99': 197, '100': 199, + '101': 201, '102': 209, '103': 214, '104': 220, '105': 225, '106': 224, + '107': 226, '108': 228, '109': 227, '110': 229, '111': 231, '112': 233, + '113': 232, '114': 234, '115': 235, '116': 237, '117': 236, '118': 238, + '119': 239, '120': 241, '121': 243, '122': 242, '123': 244, '124': 246, + '125': 245, '126': 250, '127': 249, '128': 251, '129': 252, '130': 8224, + '131': 176, '132': 162, '133': 163, '134': 167, '135': 8226, '136': 182, + '137': 223, '138': 174, '139': 169, '140': 8482, '141': 180, '142': 168, + '143': 8800, '144': 198, '145': 216, '146': 8734, '147': 177, '148': 8804, + '149': 8805, '150': 165, '151': 181, '152': 8706, '153': 8721, '154': 8719, + '156': 8747, '157': 170, '158': 186, '159': 8486, '160': 230, '161': 248, + '162': 191, '163': 161, '164': 172, '165': 8730, '166': 402, '167': 8776, + '168': 8710, '169': 171, '170': 187, '171': 8230, '210': 218, '223': 711, + '224': 321, '225': 322, '227': 353, '229': 382, '234': 253, '252': 263, + '253': 268, '254': 269, '258': 258, '260': 260, '261': 261, '265': 280, + '266': 281, '268': 283, '269': 313, '275': 323, '276': 324, '278': 328, + '284': 345, '285': 346, '286': 347, '292': 367, '295': 377, '296': 378, + '298': 380, '305': 963, + '306': 964, '307': 966, '308': 8215, '309': 8252, '310': 8319, '311': 8359, + '312': 8592, '313': 8593, '337': 9552, '493': 1039, '494': 1040, '705': 1524, + '706': 8362, '710': 64288, '711': 64298, '759': 1617, '761': 1776, + '763': 1778, '775': 1652, '777': 1764, '778': 1780, '779': 1781, '780': 1782, + '782': 771, '783': 64726, '786': 8363, '788': 8532, '790': 768, '791': 769, + '792': 768, '795': 803, '797': 64336, '798': 64337, '799': 64342, + '800': 64343, '801': 64344, '802': 64345, '803': 64362, '804': 64363, + '805': 64364, '2424': 7821, '2425': 7822, '2426': 7823, '2427': 7824, + '2428': 7825, '2429': 7826, '2430': 7827, '2433': 7682, '2678': 8045, + '2679': 8046, '2830': 1552, '2838': 686, '2840': 751, '2842': 753, + '2843': 754, '2844': 755, '2846': 757, '2856': 767, '2857': 848, '2858': 849, + '2862': 853, '2863': 854, '2864': 855, '2865': 861, '2866': 862, '2906': 7460, + '2908': 7462, '2909': 7463, '2910': 7464, '2912': 7466, '2913': 7467, + '2914': 7468, '2916': 7470, '2917': 7471, '2918': 7472, '2920': 7474, + '2921': 7475, '2922': 7476, '2924': 7478, '2925': 7479, '2926': 7480, + '2928': 7482, '2929': 7483, '2930': 7484, '2932': 7486, '2933': 7487, + '2934': 7488, '2936': 7490, '2937': 7491, '2938': 7492, '2940': 7494, + '2941': 7495, '2942': 7496, '2944': 7498, '2946': 7500, '2948': 7502, + '2950': 7504, '2951': 7505, '2952': 7506, '2954': 7508, '2955': 7509, + '2956': 7510, '2958': 7512, '2959': 7513, '2960': 7514, '2962': 7516, + '2963': 7517, '2964': 7518, '2966': 7520, '2967': 7521, '2968': 7522, + '2970': 7524, '2971': 7525, '2972': 7526, '2974': 7528, '2975': 7529, + '2976': 7530, '2978': 1537, '2979': 1538, '2980': 1539, '2982': 1549, + '2983': 1551, '2984': 1552, '2986': 1554, '2987': 1555, '2988': 1556, + '2990': 1623, '2991': 1624, '2995': 1775, '2999': 1791, '3002': 64290, + '3003': 64291, '3004': 64292, '3006': 64294, '3007': 64295, '3008': 64296, + '3011': 1900, '3014': 8223, '3015': 8244, '3017': 7532, '3018': 7533, + '3019': 7534, '3075': 7590, '3076': 7591, '3079': 7594, '3080': 7595, + '3083': 7598, '3084': 7599, '3087': 7602, '3088': 7603, '3091': 7606, + '3092': 7607, '3095': 7610, '3096': 7611, '3099': 7614, '3100': 7615, + '3103': 7618, '3104': 7619, '3107': 8337, '3108': 8338, '3116': 1884, + '3119': 1885, '3120': 1885, '3123': 1886, '3124': 1886, '3127': 1887, + '3128': 1887, '3131': 1888, '3132': 1888, '3135': 1889, '3136': 1889, + '3139': 1890, '3140': 1890, '3143': 1891, '3144': 1891, '3147': 1892, + '3148': 1892, '3153': 580, '3154': 581, '3157': 584, '3158': 585, '3161': 588, + '3162': 589, '3165': 891, '3166': 892, '3169': 1274, '3170': 1275, + '3173': 1278, '3174': 1279, '3181': 7622, '3182': 7623, '3282': 11799, + '3316': 578, '3379': 42785, '3393': 1159, '3416': 8377 +}; + +// Some characters, e.g. copyrightserif, are mapped to the private use area and +// might not be displayed using standard fonts. Mapping/hacking well-known chars +// to the similar equivalents in the normal characters range. +var SpecialPUASymbols = { + '63721': 0x00A9, // copyrightsans (0xF8E9) => copyright + '63193': 0x00A9, // copyrightserif (0xF6D9) => copyright + '63720': 0x00AE, // registersans (0xF8E8) => registered + '63194': 0x00AE, // registerserif (0xF6DA) => registered + '63722': 0x2122, // trademarksans (0xF8EA) => trademark + '63195': 0x2122, // trademarkserif (0xF6DB) => trademark + '63729': 0x23A7, // bracelefttp (0xF8F1) + '63730': 0x23A8, // braceleftmid (0xF8F2) + '63731': 0x23A9, // braceleftbt (0xF8F3) + '63740': 0x23AB, // bracerighttp (0xF8FC) + '63741': 0x23AC, // bracerightmid (0xF8FD) + '63742': 0x23AD, // bracerightbt (0xF8FE) + '63726': 0x23A1, // bracketlefttp (0xF8EE) + '63727': 0x23A2, // bracketleftex (0xF8EF) + '63728': 0x23A3, // bracketleftbt (0xF8F0) + '63737': 0x23A4, // bracketrighttp (0xF8F9) + '63738': 0x23A5, // bracketrightex (0xF8FA) + '63739': 0x23A6, // bracketrightbt (0xF8FB) + '63723': 0x239B, // parenlefttp (0xF8EB) + '63724': 0x239C, // parenleftex (0xF8EC) + '63725': 0x239D, // parenleftbt (0xF8ED) + '63734': 0x239E, // parenrighttp (0xF8F6) + '63735': 0x239F, // parenrightex (0xF8F7) + '63736': 0x23A0, // parenrightbt (0xF8F8) +}; +function mapSpecialUnicodeValues(code) { + if (code >= 0xFFF0 && code <= 0xFFFF) { // Specials unicode block. + return 0; + } else if (code >= 0xF600 && code <= 0xF8FF) { + return (SpecialPUASymbols[code] || code); + } + return code; +} + +var UnicodeRanges = [ + { 'begin': 0x0000, 'end': 0x007F }, // Basic Latin + { 'begin': 0x0080, 'end': 0x00FF }, // Latin-1 Supplement + { 'begin': 0x0100, 'end': 0x017F }, // Latin Extended-A + { 'begin': 0x0180, 'end': 0x024F }, // Latin Extended-B + { 'begin': 0x0250, 'end': 0x02AF }, // IPA Extensions + { 'begin': 0x02B0, 'end': 0x02FF }, // Spacing Modifier Letters + { 'begin': 0x0300, 'end': 0x036F }, // Combining Diacritical Marks + { 'begin': 0x0370, 'end': 0x03FF }, // Greek and Coptic + { 'begin': 0x2C80, 'end': 0x2CFF }, // Coptic + { 'begin': 0x0400, 'end': 0x04FF }, // Cyrillic + { 'begin': 0x0530, 'end': 0x058F }, // Armenian + { 'begin': 0x0590, 'end': 0x05FF }, // Hebrew + { 'begin': 0xA500, 'end': 0xA63F }, // Vai + { 'begin': 0x0600, 'end': 0x06FF }, // Arabic + { 'begin': 0x07C0, 'end': 0x07FF }, // NKo + { 'begin': 0x0900, 'end': 0x097F }, // Devanagari + { 'begin': 0x0980, 'end': 0x09FF }, // Bengali + { 'begin': 0x0A00, 'end': 0x0A7F }, // Gurmukhi + { 'begin': 0x0A80, 'end': 0x0AFF }, // Gujarati + { 'begin': 0x0B00, 'end': 0x0B7F }, // Oriya + { 'begin': 0x0B80, 'end': 0x0BFF }, // Tamil + { 'begin': 0x0C00, 'end': 0x0C7F }, // Telugu + { 'begin': 0x0C80, 'end': 0x0CFF }, // Kannada + { 'begin': 0x0D00, 'end': 0x0D7F }, // Malayalam + { 'begin': 0x0E00, 'end': 0x0E7F }, // Thai + { 'begin': 0x0E80, 'end': 0x0EFF }, // Lao + { 'begin': 0x10A0, 'end': 0x10FF }, // Georgian + { 'begin': 0x1B00, 'end': 0x1B7F }, // Balinese + { 'begin': 0x1100, 'end': 0x11FF }, // Hangul Jamo + { 'begin': 0x1E00, 'end': 0x1EFF }, // Latin Extended Additional + { 'begin': 0x1F00, 'end': 0x1FFF }, // Greek Extended + { 'begin': 0x2000, 'end': 0x206F }, // General Punctuation + { 'begin': 0x2070, 'end': 0x209F }, // Superscripts And Subscripts + { 'begin': 0x20A0, 'end': 0x20CF }, // Currency Symbol + { 'begin': 0x20D0, 'end': 0x20FF }, // Combining Diacritical Marks For Symbols + { 'begin': 0x2100, 'end': 0x214F }, // Letterlike Symbols + { 'begin': 0x2150, 'end': 0x218F }, // Number Forms + { 'begin': 0x2190, 'end': 0x21FF }, // Arrows + { 'begin': 0x2200, 'end': 0x22FF }, // Mathematical Operators + { 'begin': 0x2300, 'end': 0x23FF }, // Miscellaneous Technical + { 'begin': 0x2400, 'end': 0x243F }, // Control Pictures + { 'begin': 0x2440, 'end': 0x245F }, // Optical Character Recognition + { 'begin': 0x2460, 'end': 0x24FF }, // Enclosed Alphanumerics + { 'begin': 0x2500, 'end': 0x257F }, // Box Drawing + { 'begin': 0x2580, 'end': 0x259F }, // Block Elements + { 'begin': 0x25A0, 'end': 0x25FF }, // Geometric Shapes + { 'begin': 0x2600, 'end': 0x26FF }, // Miscellaneous Symbols + { 'begin': 0x2700, 'end': 0x27BF }, // Dingbats + { 'begin': 0x3000, 'end': 0x303F }, // CJK Symbols And Punctuation + { 'begin': 0x3040, 'end': 0x309F }, // Hiragana + { 'begin': 0x30A0, 'end': 0x30FF }, // Katakana + { 'begin': 0x3100, 'end': 0x312F }, // Bopomofo + { 'begin': 0x3130, 'end': 0x318F }, // Hangul Compatibility Jamo + { 'begin': 0xA840, 'end': 0xA87F }, // Phags-pa + { 'begin': 0x3200, 'end': 0x32FF }, // Enclosed CJK Letters And Months + { 'begin': 0x3300, 'end': 0x33FF }, // CJK Compatibility + { 'begin': 0xAC00, 'end': 0xD7AF }, // Hangul Syllables + { 'begin': 0xD800, 'end': 0xDFFF }, // Non-Plane 0 * + { 'begin': 0x10900, 'end': 0x1091F }, // Phoenicia + { 'begin': 0x4E00, 'end': 0x9FFF }, // CJK Unified Ideographs + { 'begin': 0xE000, 'end': 0xF8FF }, // Private Use Area (plane 0) + { 'begin': 0x31C0, 'end': 0x31EF }, // CJK Strokes + { 'begin': 0xFB00, 'end': 0xFB4F }, // Alphabetic Presentation Forms + { 'begin': 0xFB50, 'end': 0xFDFF }, // Arabic Presentation Forms-A + { 'begin': 0xFE20, 'end': 0xFE2F }, // Combining Half Marks + { 'begin': 0xFE10, 'end': 0xFE1F }, // Vertical Forms + { 'begin': 0xFE50, 'end': 0xFE6F }, // Small Form Variants + { 'begin': 0xFE70, 'end': 0xFEFF }, // Arabic Presentation Forms-B + { 'begin': 0xFF00, 'end': 0xFFEF }, // Halfwidth And Fullwidth Forms + { 'begin': 0xFFF0, 'end': 0xFFFF }, // Specials + { 'begin': 0x0F00, 'end': 0x0FFF }, // Tibetan + { 'begin': 0x0700, 'end': 0x074F }, // Syriac + { 'begin': 0x0780, 'end': 0x07BF }, // Thaana + { 'begin': 0x0D80, 'end': 0x0DFF }, // Sinhala + { 'begin': 0x1000, 'end': 0x109F }, // Myanmar + { 'begin': 0x1200, 'end': 0x137F }, // Ethiopic + { 'begin': 0x13A0, 'end': 0x13FF }, // Cherokee + { 'begin': 0x1400, 'end': 0x167F }, // Unified Canadian Aboriginal Syllabics + { 'begin': 0x1680, 'end': 0x169F }, // Ogham + { 'begin': 0x16A0, 'end': 0x16FF }, // Runic + { 'begin': 0x1780, 'end': 0x17FF }, // Khmer + { 'begin': 0x1800, 'end': 0x18AF }, // Mongolian + { 'begin': 0x2800, 'end': 0x28FF }, // Braille Patterns + { 'begin': 0xA000, 'end': 0xA48F }, // Yi Syllables + { 'begin': 0x1700, 'end': 0x171F }, // Tagalog + { 'begin': 0x10300, 'end': 0x1032F }, // Old Italic + { 'begin': 0x10330, 'end': 0x1034F }, // Gothic + { 'begin': 0x10400, 'end': 0x1044F }, // Deseret + { 'begin': 0x1D000, 'end': 0x1D0FF }, // Byzantine Musical Symbols + { 'begin': 0x1D400, 'end': 0x1D7FF }, // Mathematical Alphanumeric Symbols + { 'begin': 0xFF000, 'end': 0xFFFFD }, // Private Use (plane 15) + { 'begin': 0xFE00, 'end': 0xFE0F }, // Variation Selectors + { 'begin': 0xE0000, 'end': 0xE007F }, // Tags + { 'begin': 0x1900, 'end': 0x194F }, // Limbu + { 'begin': 0x1950, 'end': 0x197F }, // Tai Le + { 'begin': 0x1980, 'end': 0x19DF }, // New Tai Lue + { 'begin': 0x1A00, 'end': 0x1A1F }, // Buginese + { 'begin': 0x2C00, 'end': 0x2C5F }, // Glagolitic + { 'begin': 0x2D30, 'end': 0x2D7F }, // Tifinagh + { 'begin': 0x4DC0, 'end': 0x4DFF }, // Yijing Hexagram Symbols + { 'begin': 0xA800, 'end': 0xA82F }, // Syloti Nagri + { 'begin': 0x10000, 'end': 0x1007F }, // Linear B Syllabary + { 'begin': 0x10140, 'end': 0x1018F }, // Ancient Greek Numbers + { 'begin': 0x10380, 'end': 0x1039F }, // Ugaritic + { 'begin': 0x103A0, 'end': 0x103DF }, // Old Persian + { 'begin': 0x10450, 'end': 0x1047F }, // Shavian + { 'begin': 0x10480, 'end': 0x104AF }, // Osmanya + { 'begin': 0x10800, 'end': 0x1083F }, // Cypriot Syllabary + { 'begin': 0x10A00, 'end': 0x10A5F }, // Kharoshthi + { 'begin': 0x1D300, 'end': 0x1D35F }, // Tai Xuan Jing Symbols + { 'begin': 0x12000, 'end': 0x123FF }, // Cuneiform + { 'begin': 0x1D360, 'end': 0x1D37F }, // Counting Rod Numerals + { 'begin': 0x1B80, 'end': 0x1BBF }, // Sundanese + { 'begin': 0x1C00, 'end': 0x1C4F }, // Lepcha + { 'begin': 0x1C50, 'end': 0x1C7F }, // Ol Chiki + { 'begin': 0xA880, 'end': 0xA8DF }, // Saurashtra + { 'begin': 0xA900, 'end': 0xA92F }, // Kayah Li + { 'begin': 0xA930, 'end': 0xA95F }, // Rejang + { 'begin': 0xAA00, 'end': 0xAA5F }, // Cham + { 'begin': 0x10190, 'end': 0x101CF }, // Ancient Symbols + { 'begin': 0x101D0, 'end': 0x101FF }, // Phaistos Disc + { 'begin': 0x102A0, 'end': 0x102DF }, // Carian + { 'begin': 0x1F030, 'end': 0x1F09F } // Domino Tiles +]; + +var MacStandardGlyphOrdering = [ + '.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl', + 'numbersign', 'dollar', 'percent', 'ampersand', 'quotesingle', 'parenleft', + 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', + 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', + 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', 'question', 'at', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', + 'backslash', 'bracketright', 'asciicircum', 'underscore', 'grave', 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', + 'asciitilde', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', + 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', 'adieresis', + 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis', + 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve', + 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', 'ucircumflex', + 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', 'bullet', + 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute', + 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal', + 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', 'product', 'pi', + 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash', + 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin', + 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis', + 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', + 'emdash', 'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright', + 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction', 'currency', + 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', + 'quotesinglbase', 'quotedblbase', 'perthousand', 'Acircumflex', + 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', + 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute', + 'Ucircumflex', 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', + 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', + 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar', + 'Eth', 'eth', 'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply', + 'onesuperior', 'twosuperior', 'threesuperior', 'onehalf', 'onequarter', + 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla', + 'scedilla', 'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat']; + +function getUnicodeRangeFor(value) { + for (var i = 0, ii = UnicodeRanges.length; i < ii; i++) { + var range = UnicodeRanges[i]; + if (value >= range.begin && value < range.end) { + return i; + } + } + return -1; +} + +function isRTLRangeFor(value) { + var range = UnicodeRanges[13]; + if (value >= range.begin && value < range.end) { + return true; + } + range = UnicodeRanges[11]; + if (value >= range.begin && value < range.end) { + return true; + } + return false; +} + +// The normalization table is obtained by filtering the Unicode characters +// database with entries. +var NormalizedUnicodes = { + '\u00A8': '\u0020\u0308', + '\u00AF': '\u0020\u0304', + '\u00B4': '\u0020\u0301', + '\u00B5': '\u03BC', + '\u00B8': '\u0020\u0327', + '\u0132': '\u0049\u004A', + '\u0133': '\u0069\u006A', + '\u013F': '\u004C\u00B7', + '\u0140': '\u006C\u00B7', + '\u0149': '\u02BC\u006E', + '\u017F': '\u0073', + '\u01C4': '\u0044\u017D', + '\u01C5': '\u0044\u017E', + '\u01C6': '\u0064\u017E', + '\u01C7': '\u004C\u004A', + '\u01C8': '\u004C\u006A', + '\u01C9': '\u006C\u006A', + '\u01CA': '\u004E\u004A', + '\u01CB': '\u004E\u006A', + '\u01CC': '\u006E\u006A', + '\u01F1': '\u0044\u005A', + '\u01F2': '\u0044\u007A', + '\u01F3': '\u0064\u007A', + '\u02D8': '\u0020\u0306', + '\u02D9': '\u0020\u0307', + '\u02DA': '\u0020\u030A', + '\u02DB': '\u0020\u0328', + '\u02DC': '\u0020\u0303', + '\u02DD': '\u0020\u030B', + '\u037A': '\u0020\u0345', + '\u0384': '\u0020\u0301', + '\u03D0': '\u03B2', + '\u03D1': '\u03B8', + '\u03D2': '\u03A5', + '\u03D5': '\u03C6', + '\u03D6': '\u03C0', + '\u03F0': '\u03BA', + '\u03F1': '\u03C1', + '\u03F2': '\u03C2', + '\u03F4': '\u0398', + '\u03F5': '\u03B5', + '\u03F9': '\u03A3', + '\u0587': '\u0565\u0582', + '\u0675': '\u0627\u0674', + '\u0676': '\u0648\u0674', + '\u0677': '\u06C7\u0674', + '\u0678': '\u064A\u0674', + '\u0E33': '\u0E4D\u0E32', + '\u0EB3': '\u0ECD\u0EB2', + '\u0EDC': '\u0EAB\u0E99', + '\u0EDD': '\u0EAB\u0EA1', + '\u0F77': '\u0FB2\u0F81', + '\u0F79': '\u0FB3\u0F81', + '\u1E9A': '\u0061\u02BE', + '\u1FBD': '\u0020\u0313', + '\u1FBF': '\u0020\u0313', + '\u1FC0': '\u0020\u0342', + '\u1FFE': '\u0020\u0314', + '\u2002': '\u0020', + '\u2003': '\u0020', + '\u2004': '\u0020', + '\u2005': '\u0020', + '\u2006': '\u0020', + '\u2008': '\u0020', + '\u2009': '\u0020', + '\u200A': '\u0020', + '\u2017': '\u0020\u0333', + '\u2024': '\u002E', + '\u2025': '\u002E\u002E', + '\u2026': '\u002E\u002E\u002E', + '\u2033': '\u2032\u2032', + '\u2034': '\u2032\u2032\u2032', + '\u2036': '\u2035\u2035', + '\u2037': '\u2035\u2035\u2035', + '\u203C': '\u0021\u0021', + '\u203E': '\u0020\u0305', + '\u2047': '\u003F\u003F', + '\u2048': '\u003F\u0021', + '\u2049': '\u0021\u003F', + '\u2057': '\u2032\u2032\u2032\u2032', + '\u205F': '\u0020', + '\u20A8': '\u0052\u0073', + '\u2100': '\u0061\u002F\u0063', + '\u2101': '\u0061\u002F\u0073', + '\u2103': '\u00B0\u0043', + '\u2105': '\u0063\u002F\u006F', + '\u2106': '\u0063\u002F\u0075', + '\u2107': '\u0190', + '\u2109': '\u00B0\u0046', + '\u2116': '\u004E\u006F', + '\u2121': '\u0054\u0045\u004C', + '\u2135': '\u05D0', + '\u2136': '\u05D1', + '\u2137': '\u05D2', + '\u2138': '\u05D3', + '\u213B': '\u0046\u0041\u0058', + '\u2160': '\u0049', + '\u2161': '\u0049\u0049', + '\u2162': '\u0049\u0049\u0049', + '\u2163': '\u0049\u0056', + '\u2164': '\u0056', + '\u2165': '\u0056\u0049', + '\u2166': '\u0056\u0049\u0049', + '\u2167': '\u0056\u0049\u0049\u0049', + '\u2168': '\u0049\u0058', + '\u2169': '\u0058', + '\u216A': '\u0058\u0049', + '\u216B': '\u0058\u0049\u0049', + '\u216C': '\u004C', + '\u216D': '\u0043', + '\u216E': '\u0044', + '\u216F': '\u004D', + '\u2170': '\u0069', + '\u2171': '\u0069\u0069', + '\u2172': '\u0069\u0069\u0069', + '\u2173': '\u0069\u0076', + '\u2174': '\u0076', + '\u2175': '\u0076\u0069', + '\u2176': '\u0076\u0069\u0069', + '\u2177': '\u0076\u0069\u0069\u0069', + '\u2178': '\u0069\u0078', + '\u2179': '\u0078', + '\u217A': '\u0078\u0069', + '\u217B': '\u0078\u0069\u0069', + '\u217C': '\u006C', + '\u217D': '\u0063', + '\u217E': '\u0064', + '\u217F': '\u006D', + '\u222C': '\u222B\u222B', + '\u222D': '\u222B\u222B\u222B', + '\u222F': '\u222E\u222E', + '\u2230': '\u222E\u222E\u222E', + '\u2474': '\u0028\u0031\u0029', + '\u2475': '\u0028\u0032\u0029', + '\u2476': '\u0028\u0033\u0029', + '\u2477': '\u0028\u0034\u0029', + '\u2478': '\u0028\u0035\u0029', + '\u2479': '\u0028\u0036\u0029', + '\u247A': '\u0028\u0037\u0029', + '\u247B': '\u0028\u0038\u0029', + '\u247C': '\u0028\u0039\u0029', + '\u247D': '\u0028\u0031\u0030\u0029', + '\u247E': '\u0028\u0031\u0031\u0029', + '\u247F': '\u0028\u0031\u0032\u0029', + '\u2480': '\u0028\u0031\u0033\u0029', + '\u2481': '\u0028\u0031\u0034\u0029', + '\u2482': '\u0028\u0031\u0035\u0029', + '\u2483': '\u0028\u0031\u0036\u0029', + '\u2484': '\u0028\u0031\u0037\u0029', + '\u2485': '\u0028\u0031\u0038\u0029', + '\u2486': '\u0028\u0031\u0039\u0029', + '\u2487': '\u0028\u0032\u0030\u0029', + '\u2488': '\u0031\u002E', + '\u2489': '\u0032\u002E', + '\u248A': '\u0033\u002E', + '\u248B': '\u0034\u002E', + '\u248C': '\u0035\u002E', + '\u248D': '\u0036\u002E', + '\u248E': '\u0037\u002E', + '\u248F': '\u0038\u002E', + '\u2490': '\u0039\u002E', + '\u2491': '\u0031\u0030\u002E', + '\u2492': '\u0031\u0031\u002E', + '\u2493': '\u0031\u0032\u002E', + '\u2494': '\u0031\u0033\u002E', + '\u2495': '\u0031\u0034\u002E', + '\u2496': '\u0031\u0035\u002E', + '\u2497': '\u0031\u0036\u002E', + '\u2498': '\u0031\u0037\u002E', + '\u2499': '\u0031\u0038\u002E', + '\u249A': '\u0031\u0039\u002E', + '\u249B': '\u0032\u0030\u002E', + '\u249C': '\u0028\u0061\u0029', + '\u249D': '\u0028\u0062\u0029', + '\u249E': '\u0028\u0063\u0029', + '\u249F': '\u0028\u0064\u0029', + '\u24A0': '\u0028\u0065\u0029', + '\u24A1': '\u0028\u0066\u0029', + '\u24A2': '\u0028\u0067\u0029', + '\u24A3': '\u0028\u0068\u0029', + '\u24A4': '\u0028\u0069\u0029', + '\u24A5': '\u0028\u006A\u0029', + '\u24A6': '\u0028\u006B\u0029', + '\u24A7': '\u0028\u006C\u0029', + '\u24A8': '\u0028\u006D\u0029', + '\u24A9': '\u0028\u006E\u0029', + '\u24AA': '\u0028\u006F\u0029', + '\u24AB': '\u0028\u0070\u0029', + '\u24AC': '\u0028\u0071\u0029', + '\u24AD': '\u0028\u0072\u0029', + '\u24AE': '\u0028\u0073\u0029', + '\u24AF': '\u0028\u0074\u0029', + '\u24B0': '\u0028\u0075\u0029', + '\u24B1': '\u0028\u0076\u0029', + '\u24B2': '\u0028\u0077\u0029', + '\u24B3': '\u0028\u0078\u0029', + '\u24B4': '\u0028\u0079\u0029', + '\u24B5': '\u0028\u007A\u0029', + '\u2A0C': '\u222B\u222B\u222B\u222B', + '\u2A74': '\u003A\u003A\u003D', + '\u2A75': '\u003D\u003D', + '\u2A76': '\u003D\u003D\u003D', + '\u2E9F': '\u6BCD', + '\u2EF3': '\u9F9F', + '\u2F00': '\u4E00', + '\u2F01': '\u4E28', + '\u2F02': '\u4E36', + '\u2F03': '\u4E3F', + '\u2F04': '\u4E59', + '\u2F05': '\u4E85', + '\u2F06': '\u4E8C', + '\u2F07': '\u4EA0', + '\u2F08': '\u4EBA', + '\u2F09': '\u513F', + '\u2F0A': '\u5165', + '\u2F0B': '\u516B', + '\u2F0C': '\u5182', + '\u2F0D': '\u5196', + '\u2F0E': '\u51AB', + '\u2F0F': '\u51E0', + '\u2F10': '\u51F5', + '\u2F11': '\u5200', + '\u2F12': '\u529B', + '\u2F13': '\u52F9', + '\u2F14': '\u5315', + '\u2F15': '\u531A', + '\u2F16': '\u5338', + '\u2F17': '\u5341', + '\u2F18': '\u535C', + '\u2F19': '\u5369', + '\u2F1A': '\u5382', + '\u2F1B': '\u53B6', + '\u2F1C': '\u53C8', + '\u2F1D': '\u53E3', + '\u2F1E': '\u56D7', + '\u2F1F': '\u571F', + '\u2F20': '\u58EB', + '\u2F21': '\u5902', + '\u2F22': '\u590A', + '\u2F23': '\u5915', + '\u2F24': '\u5927', + '\u2F25': '\u5973', + '\u2F26': '\u5B50', + '\u2F27': '\u5B80', + '\u2F28': '\u5BF8', + '\u2F29': '\u5C0F', + '\u2F2A': '\u5C22', + '\u2F2B': '\u5C38', + '\u2F2C': '\u5C6E', + '\u2F2D': '\u5C71', + '\u2F2E': '\u5DDB', + '\u2F2F': '\u5DE5', + '\u2F30': '\u5DF1', + '\u2F31': '\u5DFE', + '\u2F32': '\u5E72', + '\u2F33': '\u5E7A', + '\u2F34': '\u5E7F', + '\u2F35': '\u5EF4', + '\u2F36': '\u5EFE', + '\u2F37': '\u5F0B', + '\u2F38': '\u5F13', + '\u2F39': '\u5F50', + '\u2F3A': '\u5F61', + '\u2F3B': '\u5F73', + '\u2F3C': '\u5FC3', + '\u2F3D': '\u6208', + '\u2F3E': '\u6236', + '\u2F3F': '\u624B', + '\u2F40': '\u652F', + '\u2F41': '\u6534', + '\u2F42': '\u6587', + '\u2F43': '\u6597', + '\u2F44': '\u65A4', + '\u2F45': '\u65B9', + '\u2F46': '\u65E0', + '\u2F47': '\u65E5', + '\u2F48': '\u66F0', + '\u2F49': '\u6708', + '\u2F4A': '\u6728', + '\u2F4B': '\u6B20', + '\u2F4C': '\u6B62', + '\u2F4D': '\u6B79', + '\u2F4E': '\u6BB3', + '\u2F4F': '\u6BCB', + '\u2F50': '\u6BD4', + '\u2F51': '\u6BDB', + '\u2F52': '\u6C0F', + '\u2F53': '\u6C14', + '\u2F54': '\u6C34', + '\u2F55': '\u706B', + '\u2F56': '\u722A', + '\u2F57': '\u7236', + '\u2F58': '\u723B', + '\u2F59': '\u723F', + '\u2F5A': '\u7247', + '\u2F5B': '\u7259', + '\u2F5C': '\u725B', + '\u2F5D': '\u72AC', + '\u2F5E': '\u7384', + '\u2F5F': '\u7389', + '\u2F60': '\u74DC', + '\u2F61': '\u74E6', + '\u2F62': '\u7518', + '\u2F63': '\u751F', + '\u2F64': '\u7528', + '\u2F65': '\u7530', + '\u2F66': '\u758B', + '\u2F67': '\u7592', + '\u2F68': '\u7676', + '\u2F69': '\u767D', + '\u2F6A': '\u76AE', + '\u2F6B': '\u76BF', + '\u2F6C': '\u76EE', + '\u2F6D': '\u77DB', + '\u2F6E': '\u77E2', + '\u2F6F': '\u77F3', + '\u2F70': '\u793A', + '\u2F71': '\u79B8', + '\u2F72': '\u79BE', + '\u2F73': '\u7A74', + '\u2F74': '\u7ACB', + '\u2F75': '\u7AF9', + '\u2F76': '\u7C73', + '\u2F77': '\u7CF8', + '\u2F78': '\u7F36', + '\u2F79': '\u7F51', + '\u2F7A': '\u7F8A', + '\u2F7B': '\u7FBD', + '\u2F7C': '\u8001', + '\u2F7D': '\u800C', + '\u2F7E': '\u8012', + '\u2F7F': '\u8033', + '\u2F80': '\u807F', + '\u2F81': '\u8089', + '\u2F82': '\u81E3', + '\u2F83': '\u81EA', + '\u2F84': '\u81F3', + '\u2F85': '\u81FC', + '\u2F86': '\u820C', + '\u2F87': '\u821B', + '\u2F88': '\u821F', + '\u2F89': '\u826E', + '\u2F8A': '\u8272', + '\u2F8B': '\u8278', + '\u2F8C': '\u864D', + '\u2F8D': '\u866B', + '\u2F8E': '\u8840', + '\u2F8F': '\u884C', + '\u2F90': '\u8863', + '\u2F91': '\u897E', + '\u2F92': '\u898B', + '\u2F93': '\u89D2', + '\u2F94': '\u8A00', + '\u2F95': '\u8C37', + '\u2F96': '\u8C46', + '\u2F97': '\u8C55', + '\u2F98': '\u8C78', + '\u2F99': '\u8C9D', + '\u2F9A': '\u8D64', + '\u2F9B': '\u8D70', + '\u2F9C': '\u8DB3', + '\u2F9D': '\u8EAB', + '\u2F9E': '\u8ECA', + '\u2F9F': '\u8F9B', + '\u2FA0': '\u8FB0', + '\u2FA1': '\u8FB5', + '\u2FA2': '\u9091', + '\u2FA3': '\u9149', + '\u2FA4': '\u91C6', + '\u2FA5': '\u91CC', + '\u2FA6': '\u91D1', + '\u2FA7': '\u9577', + '\u2FA8': '\u9580', + '\u2FA9': '\u961C', + '\u2FAA': '\u96B6', + '\u2FAB': '\u96B9', + '\u2FAC': '\u96E8', + '\u2FAD': '\u9751', + '\u2FAE': '\u975E', + '\u2FAF': '\u9762', + '\u2FB0': '\u9769', + '\u2FB1': '\u97CB', + '\u2FB2': '\u97ED', + '\u2FB3': '\u97F3', + '\u2FB4': '\u9801', + '\u2FB5': '\u98A8', + '\u2FB6': '\u98DB', + '\u2FB7': '\u98DF', + '\u2FB8': '\u9996', + '\u2FB9': '\u9999', + '\u2FBA': '\u99AC', + '\u2FBB': '\u9AA8', + '\u2FBC': '\u9AD8', + '\u2FBD': '\u9ADF', + '\u2FBE': '\u9B25', + '\u2FBF': '\u9B2F', + '\u2FC0': '\u9B32', + '\u2FC1': '\u9B3C', + '\u2FC2': '\u9B5A', + '\u2FC3': '\u9CE5', + '\u2FC4': '\u9E75', + '\u2FC5': '\u9E7F', + '\u2FC6': '\u9EA5', + '\u2FC7': '\u9EBB', + '\u2FC8': '\u9EC3', + '\u2FC9': '\u9ECD', + '\u2FCA': '\u9ED1', + '\u2FCB': '\u9EF9', + '\u2FCC': '\u9EFD', + '\u2FCD': '\u9F0E', + '\u2FCE': '\u9F13', + '\u2FCF': '\u9F20', + '\u2FD0': '\u9F3B', + '\u2FD1': '\u9F4A', + '\u2FD2': '\u9F52', + '\u2FD3': '\u9F8D', + '\u2FD4': '\u9F9C', + '\u2FD5': '\u9FA0', + '\u3036': '\u3012', + '\u3038': '\u5341', + '\u3039': '\u5344', + '\u303A': '\u5345', + '\u309B': '\u0020\u3099', + '\u309C': '\u0020\u309A', + '\u3131': '\u1100', + '\u3132': '\u1101', + '\u3133': '\u11AA', + '\u3134': '\u1102', + '\u3135': '\u11AC', + '\u3136': '\u11AD', + '\u3137': '\u1103', + '\u3138': '\u1104', + '\u3139': '\u1105', + '\u313A': '\u11B0', + '\u313B': '\u11B1', + '\u313C': '\u11B2', + '\u313D': '\u11B3', + '\u313E': '\u11B4', + '\u313F': '\u11B5', + '\u3140': '\u111A', + '\u3141': '\u1106', + '\u3142': '\u1107', + '\u3143': '\u1108', + '\u3144': '\u1121', + '\u3145': '\u1109', + '\u3146': '\u110A', + '\u3147': '\u110B', + '\u3148': '\u110C', + '\u3149': '\u110D', + '\u314A': '\u110E', + '\u314B': '\u110F', + '\u314C': '\u1110', + '\u314D': '\u1111', + '\u314E': '\u1112', + '\u314F': '\u1161', + '\u3150': '\u1162', + '\u3151': '\u1163', + '\u3152': '\u1164', + '\u3153': '\u1165', + '\u3154': '\u1166', + '\u3155': '\u1167', + '\u3156': '\u1168', + '\u3157': '\u1169', + '\u3158': '\u116A', + '\u3159': '\u116B', + '\u315A': '\u116C', + '\u315B': '\u116D', + '\u315C': '\u116E', + '\u315D': '\u116F', + '\u315E': '\u1170', + '\u315F': '\u1171', + '\u3160': '\u1172', + '\u3161': '\u1173', + '\u3162': '\u1174', + '\u3163': '\u1175', + '\u3164': '\u1160', + '\u3165': '\u1114', + '\u3166': '\u1115', + '\u3167': '\u11C7', + '\u3168': '\u11C8', + '\u3169': '\u11CC', + '\u316A': '\u11CE', + '\u316B': '\u11D3', + '\u316C': '\u11D7', + '\u316D': '\u11D9', + '\u316E': '\u111C', + '\u316F': '\u11DD', + '\u3170': '\u11DF', + '\u3171': '\u111D', + '\u3172': '\u111E', + '\u3173': '\u1120', + '\u3174': '\u1122', + '\u3175': '\u1123', + '\u3176': '\u1127', + '\u3177': '\u1129', + '\u3178': '\u112B', + '\u3179': '\u112C', + '\u317A': '\u112D', + '\u317B': '\u112E', + '\u317C': '\u112F', + '\u317D': '\u1132', + '\u317E': '\u1136', + '\u317F': '\u1140', + '\u3180': '\u1147', + '\u3181': '\u114C', + '\u3182': '\u11F1', + '\u3183': '\u11F2', + '\u3184': '\u1157', + '\u3185': '\u1158', + '\u3186': '\u1159', + '\u3187': '\u1184', + '\u3188': '\u1185', + '\u3189': '\u1188', + '\u318A': '\u1191', + '\u318B': '\u1192', + '\u318C': '\u1194', + '\u318D': '\u119E', + '\u318E': '\u11A1', + '\u3200': '\u0028\u1100\u0029', + '\u3201': '\u0028\u1102\u0029', + '\u3202': '\u0028\u1103\u0029', + '\u3203': '\u0028\u1105\u0029', + '\u3204': '\u0028\u1106\u0029', + '\u3205': '\u0028\u1107\u0029', + '\u3206': '\u0028\u1109\u0029', + '\u3207': '\u0028\u110B\u0029', + '\u3208': '\u0028\u110C\u0029', + '\u3209': '\u0028\u110E\u0029', + '\u320A': '\u0028\u110F\u0029', + '\u320B': '\u0028\u1110\u0029', + '\u320C': '\u0028\u1111\u0029', + '\u320D': '\u0028\u1112\u0029', + '\u320E': '\u0028\u1100\u1161\u0029', + '\u320F': '\u0028\u1102\u1161\u0029', + '\u3210': '\u0028\u1103\u1161\u0029', + '\u3211': '\u0028\u1105\u1161\u0029', + '\u3212': '\u0028\u1106\u1161\u0029', + '\u3213': '\u0028\u1107\u1161\u0029', + '\u3214': '\u0028\u1109\u1161\u0029', + '\u3215': '\u0028\u110B\u1161\u0029', + '\u3216': '\u0028\u110C\u1161\u0029', + '\u3217': '\u0028\u110E\u1161\u0029', + '\u3218': '\u0028\u110F\u1161\u0029', + '\u3219': '\u0028\u1110\u1161\u0029', + '\u321A': '\u0028\u1111\u1161\u0029', + '\u321B': '\u0028\u1112\u1161\u0029', + '\u321C': '\u0028\u110C\u116E\u0029', + '\u321D': '\u0028\u110B\u1169\u110C\u1165\u11AB\u0029', + '\u321E': '\u0028\u110B\u1169\u1112\u116E\u0029', + '\u3220': '\u0028\u4E00\u0029', + '\u3221': '\u0028\u4E8C\u0029', + '\u3222': '\u0028\u4E09\u0029', + '\u3223': '\u0028\u56DB\u0029', + '\u3224': '\u0028\u4E94\u0029', + '\u3225': '\u0028\u516D\u0029', + '\u3226': '\u0028\u4E03\u0029', + '\u3227': '\u0028\u516B\u0029', + '\u3228': '\u0028\u4E5D\u0029', + '\u3229': '\u0028\u5341\u0029', + '\u322A': '\u0028\u6708\u0029', + '\u322B': '\u0028\u706B\u0029', + '\u322C': '\u0028\u6C34\u0029', + '\u322D': '\u0028\u6728\u0029', + '\u322E': '\u0028\u91D1\u0029', + '\u322F': '\u0028\u571F\u0029', + '\u3230': '\u0028\u65E5\u0029', + '\u3231': '\u0028\u682A\u0029', + '\u3232': '\u0028\u6709\u0029', + '\u3233': '\u0028\u793E\u0029', + '\u3234': '\u0028\u540D\u0029', + '\u3235': '\u0028\u7279\u0029', + '\u3236': '\u0028\u8CA1\u0029', + '\u3237': '\u0028\u795D\u0029', + '\u3238': '\u0028\u52B4\u0029', + '\u3239': '\u0028\u4EE3\u0029', + '\u323A': '\u0028\u547C\u0029', + '\u323B': '\u0028\u5B66\u0029', + '\u323C': '\u0028\u76E3\u0029', + '\u323D': '\u0028\u4F01\u0029', + '\u323E': '\u0028\u8CC7\u0029', + '\u323F': '\u0028\u5354\u0029', + '\u3240': '\u0028\u796D\u0029', + '\u3241': '\u0028\u4F11\u0029', + '\u3242': '\u0028\u81EA\u0029', + '\u3243': '\u0028\u81F3\u0029', + '\u32C0': '\u0031\u6708', + '\u32C1': '\u0032\u6708', + '\u32C2': '\u0033\u6708', + '\u32C3': '\u0034\u6708', + '\u32C4': '\u0035\u6708', + '\u32C5': '\u0036\u6708', + '\u32C6': '\u0037\u6708', + '\u32C7': '\u0038\u6708', + '\u32C8': '\u0039\u6708', + '\u32C9': '\u0031\u0030\u6708', + '\u32CA': '\u0031\u0031\u6708', + '\u32CB': '\u0031\u0032\u6708', + '\u3358': '\u0030\u70B9', + '\u3359': '\u0031\u70B9', + '\u335A': '\u0032\u70B9', + '\u335B': '\u0033\u70B9', + '\u335C': '\u0034\u70B9', + '\u335D': '\u0035\u70B9', + '\u335E': '\u0036\u70B9', + '\u335F': '\u0037\u70B9', + '\u3360': '\u0038\u70B9', + '\u3361': '\u0039\u70B9', + '\u3362': '\u0031\u0030\u70B9', + '\u3363': '\u0031\u0031\u70B9', + '\u3364': '\u0031\u0032\u70B9', + '\u3365': '\u0031\u0033\u70B9', + '\u3366': '\u0031\u0034\u70B9', + '\u3367': '\u0031\u0035\u70B9', + '\u3368': '\u0031\u0036\u70B9', + '\u3369': '\u0031\u0037\u70B9', + '\u336A': '\u0031\u0038\u70B9', + '\u336B': '\u0031\u0039\u70B9', + '\u336C': '\u0032\u0030\u70B9', + '\u336D': '\u0032\u0031\u70B9', + '\u336E': '\u0032\u0032\u70B9', + '\u336F': '\u0032\u0033\u70B9', + '\u3370': '\u0032\u0034\u70B9', + '\u33E0': '\u0031\u65E5', + '\u33E1': '\u0032\u65E5', + '\u33E2': '\u0033\u65E5', + '\u33E3': '\u0034\u65E5', + '\u33E4': '\u0035\u65E5', + '\u33E5': '\u0036\u65E5', + '\u33E6': '\u0037\u65E5', + '\u33E7': '\u0038\u65E5', + '\u33E8': '\u0039\u65E5', + '\u33E9': '\u0031\u0030\u65E5', + '\u33EA': '\u0031\u0031\u65E5', + '\u33EB': '\u0031\u0032\u65E5', + '\u33EC': '\u0031\u0033\u65E5', + '\u33ED': '\u0031\u0034\u65E5', + '\u33EE': '\u0031\u0035\u65E5', + '\u33EF': '\u0031\u0036\u65E5', + '\u33F0': '\u0031\u0037\u65E5', + '\u33F1': '\u0031\u0038\u65E5', + '\u33F2': '\u0031\u0039\u65E5', + '\u33F3': '\u0032\u0030\u65E5', + '\u33F4': '\u0032\u0031\u65E5', + '\u33F5': '\u0032\u0032\u65E5', + '\u33F6': '\u0032\u0033\u65E5', + '\u33F7': '\u0032\u0034\u65E5', + '\u33F8': '\u0032\u0035\u65E5', + '\u33F9': '\u0032\u0036\u65E5', + '\u33FA': '\u0032\u0037\u65E5', + '\u33FB': '\u0032\u0038\u65E5', + '\u33FC': '\u0032\u0039\u65E5', + '\u33FD': '\u0033\u0030\u65E5', + '\u33FE': '\u0033\u0031\u65E5', + '\uFB00': '\u0066\u0066', + '\uFB01': '\u0066\u0069', + '\uFB02': '\u0066\u006C', + '\uFB03': '\u0066\u0066\u0069', + '\uFB04': '\u0066\u0066\u006C', + '\uFB05': '\u017F\u0074', + '\uFB06': '\u0073\u0074', + '\uFB13': '\u0574\u0576', + '\uFB14': '\u0574\u0565', + '\uFB15': '\u0574\u056B', + '\uFB16': '\u057E\u0576', + '\uFB17': '\u0574\u056D', + '\uFB4F': '\u05D0\u05DC', + '\uFB50': '\u0671', + '\uFB51': '\u0671', + '\uFB52': '\u067B', + '\uFB53': '\u067B', + '\uFB54': '\u067B', + '\uFB55': '\u067B', + '\uFB56': '\u067E', + '\uFB57': '\u067E', + '\uFB58': '\u067E', + '\uFB59': '\u067E', + '\uFB5A': '\u0680', + '\uFB5B': '\u0680', + '\uFB5C': '\u0680', + '\uFB5D': '\u0680', + '\uFB5E': '\u067A', + '\uFB5F': '\u067A', + '\uFB60': '\u067A', + '\uFB61': '\u067A', + '\uFB62': '\u067F', + '\uFB63': '\u067F', + '\uFB64': '\u067F', + '\uFB65': '\u067F', + '\uFB66': '\u0679', + '\uFB67': '\u0679', + '\uFB68': '\u0679', + '\uFB69': '\u0679', + '\uFB6A': '\u06A4', + '\uFB6B': '\u06A4', + '\uFB6C': '\u06A4', + '\uFB6D': '\u06A4', + '\uFB6E': '\u06A6', + '\uFB6F': '\u06A6', + '\uFB70': '\u06A6', + '\uFB71': '\u06A6', + '\uFB72': '\u0684', + '\uFB73': '\u0684', + '\uFB74': '\u0684', + '\uFB75': '\u0684', + '\uFB76': '\u0683', + '\uFB77': '\u0683', + '\uFB78': '\u0683', + '\uFB79': '\u0683', + '\uFB7A': '\u0686', + '\uFB7B': '\u0686', + '\uFB7C': '\u0686', + '\uFB7D': '\u0686', + '\uFB7E': '\u0687', + '\uFB7F': '\u0687', + '\uFB80': '\u0687', + '\uFB81': '\u0687', + '\uFB82': '\u068D', + '\uFB83': '\u068D', + '\uFB84': '\u068C', + '\uFB85': '\u068C', + '\uFB86': '\u068E', + '\uFB87': '\u068E', + '\uFB88': '\u0688', + '\uFB89': '\u0688', + '\uFB8A': '\u0698', + '\uFB8B': '\u0698', + '\uFB8C': '\u0691', + '\uFB8D': '\u0691', + '\uFB8E': '\u06A9', + '\uFB8F': '\u06A9', + '\uFB90': '\u06A9', + '\uFB91': '\u06A9', + '\uFB92': '\u06AF', + '\uFB93': '\u06AF', + '\uFB94': '\u06AF', + '\uFB95': '\u06AF', + '\uFB96': '\u06B3', + '\uFB97': '\u06B3', + '\uFB98': '\u06B3', + '\uFB99': '\u06B3', + '\uFB9A': '\u06B1', + '\uFB9B': '\u06B1', + '\uFB9C': '\u06B1', + '\uFB9D': '\u06B1', + '\uFB9E': '\u06BA', + '\uFB9F': '\u06BA', + '\uFBA0': '\u06BB', + '\uFBA1': '\u06BB', + '\uFBA2': '\u06BB', + '\uFBA3': '\u06BB', + '\uFBA4': '\u06C0', + '\uFBA5': '\u06C0', + '\uFBA6': '\u06C1', + '\uFBA7': '\u06C1', + '\uFBA8': '\u06C1', + '\uFBA9': '\u06C1', + '\uFBAA': '\u06BE', + '\uFBAB': '\u06BE', + '\uFBAC': '\u06BE', + '\uFBAD': '\u06BE', + '\uFBAE': '\u06D2', + '\uFBAF': '\u06D2', + '\uFBB0': '\u06D3', + '\uFBB1': '\u06D3', + '\uFBD3': '\u06AD', + '\uFBD4': '\u06AD', + '\uFBD5': '\u06AD', + '\uFBD6': '\u06AD', + '\uFBD7': '\u06C7', + '\uFBD8': '\u06C7', + '\uFBD9': '\u06C6', + '\uFBDA': '\u06C6', + '\uFBDB': '\u06C8', + '\uFBDC': '\u06C8', + '\uFBDD': '\u0677', + '\uFBDE': '\u06CB', + '\uFBDF': '\u06CB', + '\uFBE0': '\u06C5', + '\uFBE1': '\u06C5', + '\uFBE2': '\u06C9', + '\uFBE3': '\u06C9', + '\uFBE4': '\u06D0', + '\uFBE5': '\u06D0', + '\uFBE6': '\u06D0', + '\uFBE7': '\u06D0', + '\uFBE8': '\u0649', + '\uFBE9': '\u0649', + '\uFBEA': '\u0626\u0627', + '\uFBEB': '\u0626\u0627', + '\uFBEC': '\u0626\u06D5', + '\uFBED': '\u0626\u06D5', + '\uFBEE': '\u0626\u0648', + '\uFBEF': '\u0626\u0648', + '\uFBF0': '\u0626\u06C7', + '\uFBF1': '\u0626\u06C7', + '\uFBF2': '\u0626\u06C6', + '\uFBF3': '\u0626\u06C6', + '\uFBF4': '\u0626\u06C8', + '\uFBF5': '\u0626\u06C8', + '\uFBF6': '\u0626\u06D0', + '\uFBF7': '\u0626\u06D0', + '\uFBF8': '\u0626\u06D0', + '\uFBF9': '\u0626\u0649', + '\uFBFA': '\u0626\u0649', + '\uFBFB': '\u0626\u0649', + '\uFBFC': '\u06CC', + '\uFBFD': '\u06CC', + '\uFBFE': '\u06CC', + '\uFBFF': '\u06CC', + '\uFC00': '\u0626\u062C', + '\uFC01': '\u0626\u062D', + '\uFC02': '\u0626\u0645', + '\uFC03': '\u0626\u0649', + '\uFC04': '\u0626\u064A', + '\uFC05': '\u0628\u062C', + '\uFC06': '\u0628\u062D', + '\uFC07': '\u0628\u062E', + '\uFC08': '\u0628\u0645', + '\uFC09': '\u0628\u0649', + '\uFC0A': '\u0628\u064A', + '\uFC0B': '\u062A\u062C', + '\uFC0C': '\u062A\u062D', + '\uFC0D': '\u062A\u062E', + '\uFC0E': '\u062A\u0645', + '\uFC0F': '\u062A\u0649', + '\uFC10': '\u062A\u064A', + '\uFC11': '\u062B\u062C', + '\uFC12': '\u062B\u0645', + '\uFC13': '\u062B\u0649', + '\uFC14': '\u062B\u064A', + '\uFC15': '\u062C\u062D', + '\uFC16': '\u062C\u0645', + '\uFC17': '\u062D\u062C', + '\uFC18': '\u062D\u0645', + '\uFC19': '\u062E\u062C', + '\uFC1A': '\u062E\u062D', + '\uFC1B': '\u062E\u0645', + '\uFC1C': '\u0633\u062C', + '\uFC1D': '\u0633\u062D', + '\uFC1E': '\u0633\u062E', + '\uFC1F': '\u0633\u0645', + '\uFC20': '\u0635\u062D', + '\uFC21': '\u0635\u0645', + '\uFC22': '\u0636\u062C', + '\uFC23': '\u0636\u062D', + '\uFC24': '\u0636\u062E', + '\uFC25': '\u0636\u0645', + '\uFC26': '\u0637\u062D', + '\uFC27': '\u0637\u0645', + '\uFC28': '\u0638\u0645', + '\uFC29': '\u0639\u062C', + '\uFC2A': '\u0639\u0645', + '\uFC2B': '\u063A\u062C', + '\uFC2C': '\u063A\u0645', + '\uFC2D': '\u0641\u062C', + '\uFC2E': '\u0641\u062D', + '\uFC2F': '\u0641\u062E', + '\uFC30': '\u0641\u0645', + '\uFC31': '\u0641\u0649', + '\uFC32': '\u0641\u064A', + '\uFC33': '\u0642\u062D', + '\uFC34': '\u0642\u0645', + '\uFC35': '\u0642\u0649', + '\uFC36': '\u0642\u064A', + '\uFC37': '\u0643\u0627', + '\uFC38': '\u0643\u062C', + '\uFC39': '\u0643\u062D', + '\uFC3A': '\u0643\u062E', + '\uFC3B': '\u0643\u0644', + '\uFC3C': '\u0643\u0645', + '\uFC3D': '\u0643\u0649', + '\uFC3E': '\u0643\u064A', + '\uFC3F': '\u0644\u062C', + '\uFC40': '\u0644\u062D', + '\uFC41': '\u0644\u062E', + '\uFC42': '\u0644\u0645', + '\uFC43': '\u0644\u0649', + '\uFC44': '\u0644\u064A', + '\uFC45': '\u0645\u062C', + '\uFC46': '\u0645\u062D', + '\uFC47': '\u0645\u062E', + '\uFC48': '\u0645\u0645', + '\uFC49': '\u0645\u0649', + '\uFC4A': '\u0645\u064A', + '\uFC4B': '\u0646\u062C', + '\uFC4C': '\u0646\u062D', + '\uFC4D': '\u0646\u062E', + '\uFC4E': '\u0646\u0645', + '\uFC4F': '\u0646\u0649', + '\uFC50': '\u0646\u064A', + '\uFC51': '\u0647\u062C', + '\uFC52': '\u0647\u0645', + '\uFC53': '\u0647\u0649', + '\uFC54': '\u0647\u064A', + '\uFC55': '\u064A\u062C', + '\uFC56': '\u064A\u062D', + '\uFC57': '\u064A\u062E', + '\uFC58': '\u064A\u0645', + '\uFC59': '\u064A\u0649', + '\uFC5A': '\u064A\u064A', + '\uFC5B': '\u0630\u0670', + '\uFC5C': '\u0631\u0670', + '\uFC5D': '\u0649\u0670', + '\uFC5E': '\u0020\u064C\u0651', + '\uFC5F': '\u0020\u064D\u0651', + '\uFC60': '\u0020\u064E\u0651', + '\uFC61': '\u0020\u064F\u0651', + '\uFC62': '\u0020\u0650\u0651', + '\uFC63': '\u0020\u0651\u0670', + '\uFC64': '\u0626\u0631', + '\uFC65': '\u0626\u0632', + '\uFC66': '\u0626\u0645', + '\uFC67': '\u0626\u0646', + '\uFC68': '\u0626\u0649', + '\uFC69': '\u0626\u064A', + '\uFC6A': '\u0628\u0631', + '\uFC6B': '\u0628\u0632', + '\uFC6C': '\u0628\u0645', + '\uFC6D': '\u0628\u0646', + '\uFC6E': '\u0628\u0649', + '\uFC6F': '\u0628\u064A', + '\uFC70': '\u062A\u0631', + '\uFC71': '\u062A\u0632', + '\uFC72': '\u062A\u0645', + '\uFC73': '\u062A\u0646', + '\uFC74': '\u062A\u0649', + '\uFC75': '\u062A\u064A', + '\uFC76': '\u062B\u0631', + '\uFC77': '\u062B\u0632', + '\uFC78': '\u062B\u0645', + '\uFC79': '\u062B\u0646', + '\uFC7A': '\u062B\u0649', + '\uFC7B': '\u062B\u064A', + '\uFC7C': '\u0641\u0649', + '\uFC7D': '\u0641\u064A', + '\uFC7E': '\u0642\u0649', + '\uFC7F': '\u0642\u064A', + '\uFC80': '\u0643\u0627', + '\uFC81': '\u0643\u0644', + '\uFC82': '\u0643\u0645', + '\uFC83': '\u0643\u0649', + '\uFC84': '\u0643\u064A', + '\uFC85': '\u0644\u0645', + '\uFC86': '\u0644\u0649', + '\uFC87': '\u0644\u064A', + '\uFC88': '\u0645\u0627', + '\uFC89': '\u0645\u0645', + '\uFC8A': '\u0646\u0631', + '\uFC8B': '\u0646\u0632', + '\uFC8C': '\u0646\u0645', + '\uFC8D': '\u0646\u0646', + '\uFC8E': '\u0646\u0649', + '\uFC8F': '\u0646\u064A', + '\uFC90': '\u0649\u0670', + '\uFC91': '\u064A\u0631', + '\uFC92': '\u064A\u0632', + '\uFC93': '\u064A\u0645', + '\uFC94': '\u064A\u0646', + '\uFC95': '\u064A\u0649', + '\uFC96': '\u064A\u064A', + '\uFC97': '\u0626\u062C', + '\uFC98': '\u0626\u062D', + '\uFC99': '\u0626\u062E', + '\uFC9A': '\u0626\u0645', + '\uFC9B': '\u0626\u0647', + '\uFC9C': '\u0628\u062C', + '\uFC9D': '\u0628\u062D', + '\uFC9E': '\u0628\u062E', + '\uFC9F': '\u0628\u0645', + '\uFCA0': '\u0628\u0647', + '\uFCA1': '\u062A\u062C', + '\uFCA2': '\u062A\u062D', + '\uFCA3': '\u062A\u062E', + '\uFCA4': '\u062A\u0645', + '\uFCA5': '\u062A\u0647', + '\uFCA6': '\u062B\u0645', + '\uFCA7': '\u062C\u062D', + '\uFCA8': '\u062C\u0645', + '\uFCA9': '\u062D\u062C', + '\uFCAA': '\u062D\u0645', + '\uFCAB': '\u062E\u062C', + '\uFCAC': '\u062E\u0645', + '\uFCAD': '\u0633\u062C', + '\uFCAE': '\u0633\u062D', + '\uFCAF': '\u0633\u062E', + '\uFCB0': '\u0633\u0645', + '\uFCB1': '\u0635\u062D', + '\uFCB2': '\u0635\u062E', + '\uFCB3': '\u0635\u0645', + '\uFCB4': '\u0636\u062C', + '\uFCB5': '\u0636\u062D', + '\uFCB6': '\u0636\u062E', + '\uFCB7': '\u0636\u0645', + '\uFCB8': '\u0637\u062D', + '\uFCB9': '\u0638\u0645', + '\uFCBA': '\u0639\u062C', + '\uFCBB': '\u0639\u0645', + '\uFCBC': '\u063A\u062C', + '\uFCBD': '\u063A\u0645', + '\uFCBE': '\u0641\u062C', + '\uFCBF': '\u0641\u062D', + '\uFCC0': '\u0641\u062E', + '\uFCC1': '\u0641\u0645', + '\uFCC2': '\u0642\u062D', + '\uFCC3': '\u0642\u0645', + '\uFCC4': '\u0643\u062C', + '\uFCC5': '\u0643\u062D', + '\uFCC6': '\u0643\u062E', + '\uFCC7': '\u0643\u0644', + '\uFCC8': '\u0643\u0645', + '\uFCC9': '\u0644\u062C', + '\uFCCA': '\u0644\u062D', + '\uFCCB': '\u0644\u062E', + '\uFCCC': '\u0644\u0645', + '\uFCCD': '\u0644\u0647', + '\uFCCE': '\u0645\u062C', + '\uFCCF': '\u0645\u062D', + '\uFCD0': '\u0645\u062E', + '\uFCD1': '\u0645\u0645', + '\uFCD2': '\u0646\u062C', + '\uFCD3': '\u0646\u062D', + '\uFCD4': '\u0646\u062E', + '\uFCD5': '\u0646\u0645', + '\uFCD6': '\u0646\u0647', + '\uFCD7': '\u0647\u062C', + '\uFCD8': '\u0647\u0645', + '\uFCD9': '\u0647\u0670', + '\uFCDA': '\u064A\u062C', + '\uFCDB': '\u064A\u062D', + '\uFCDC': '\u064A\u062E', + '\uFCDD': '\u064A\u0645', + '\uFCDE': '\u064A\u0647', + '\uFCDF': '\u0626\u0645', + '\uFCE0': '\u0626\u0647', + '\uFCE1': '\u0628\u0645', + '\uFCE2': '\u0628\u0647', + '\uFCE3': '\u062A\u0645', + '\uFCE4': '\u062A\u0647', + '\uFCE5': '\u062B\u0645', + '\uFCE6': '\u062B\u0647', + '\uFCE7': '\u0633\u0645', + '\uFCE8': '\u0633\u0647', + '\uFCE9': '\u0634\u0645', + '\uFCEA': '\u0634\u0647', + '\uFCEB': '\u0643\u0644', + '\uFCEC': '\u0643\u0645', + '\uFCED': '\u0644\u0645', + '\uFCEE': '\u0646\u0645', + '\uFCEF': '\u0646\u0647', + '\uFCF0': '\u064A\u0645', + '\uFCF1': '\u064A\u0647', + '\uFCF2': '\u0640\u064E\u0651', + '\uFCF3': '\u0640\u064F\u0651', + '\uFCF4': '\u0640\u0650\u0651', + '\uFCF5': '\u0637\u0649', + '\uFCF6': '\u0637\u064A', + '\uFCF7': '\u0639\u0649', + '\uFCF8': '\u0639\u064A', + '\uFCF9': '\u063A\u0649', + '\uFCFA': '\u063A\u064A', + '\uFCFB': '\u0633\u0649', + '\uFCFC': '\u0633\u064A', + '\uFCFD': '\u0634\u0649', + '\uFCFE': '\u0634\u064A', + '\uFCFF': '\u062D\u0649', + '\uFD00': '\u062D\u064A', + '\uFD01': '\u062C\u0649', + '\uFD02': '\u062C\u064A', + '\uFD03': '\u062E\u0649', + '\uFD04': '\u062E\u064A', + '\uFD05': '\u0635\u0649', + '\uFD06': '\u0635\u064A', + '\uFD07': '\u0636\u0649', + '\uFD08': '\u0636\u064A', + '\uFD09': '\u0634\u062C', + '\uFD0A': '\u0634\u062D', + '\uFD0B': '\u0634\u062E', + '\uFD0C': '\u0634\u0645', + '\uFD0D': '\u0634\u0631', + '\uFD0E': '\u0633\u0631', + '\uFD0F': '\u0635\u0631', + '\uFD10': '\u0636\u0631', + '\uFD11': '\u0637\u0649', + '\uFD12': '\u0637\u064A', + '\uFD13': '\u0639\u0649', + '\uFD14': '\u0639\u064A', + '\uFD15': '\u063A\u0649', + '\uFD16': '\u063A\u064A', + '\uFD17': '\u0633\u0649', + '\uFD18': '\u0633\u064A', + '\uFD19': '\u0634\u0649', + '\uFD1A': '\u0634\u064A', + '\uFD1B': '\u062D\u0649', + '\uFD1C': '\u062D\u064A', + '\uFD1D': '\u062C\u0649', + '\uFD1E': '\u062C\u064A', + '\uFD1F': '\u062E\u0649', + '\uFD20': '\u062E\u064A', + '\uFD21': '\u0635\u0649', + '\uFD22': '\u0635\u064A', + '\uFD23': '\u0636\u0649', + '\uFD24': '\u0636\u064A', + '\uFD25': '\u0634\u062C', + '\uFD26': '\u0634\u062D', + '\uFD27': '\u0634\u062E', + '\uFD28': '\u0634\u0645', + '\uFD29': '\u0634\u0631', + '\uFD2A': '\u0633\u0631', + '\uFD2B': '\u0635\u0631', + '\uFD2C': '\u0636\u0631', + '\uFD2D': '\u0634\u062C', + '\uFD2E': '\u0634\u062D', + '\uFD2F': '\u0634\u062E', + '\uFD30': '\u0634\u0645', + '\uFD31': '\u0633\u0647', + '\uFD32': '\u0634\u0647', + '\uFD33': '\u0637\u0645', + '\uFD34': '\u0633\u062C', + '\uFD35': '\u0633\u062D', + '\uFD36': '\u0633\u062E', + '\uFD37': '\u0634\u062C', + '\uFD38': '\u0634\u062D', + '\uFD39': '\u0634\u062E', + '\uFD3A': '\u0637\u0645', + '\uFD3B': '\u0638\u0645', + '\uFD3C': '\u0627\u064B', + '\uFD3D': '\u0627\u064B', + '\uFD50': '\u062A\u062C\u0645', + '\uFD51': '\u062A\u062D\u062C', + '\uFD52': '\u062A\u062D\u062C', + '\uFD53': '\u062A\u062D\u0645', + '\uFD54': '\u062A\u062E\u0645', + '\uFD55': '\u062A\u0645\u062C', + '\uFD56': '\u062A\u0645\u062D', + '\uFD57': '\u062A\u0645\u062E', + '\uFD58': '\u062C\u0645\u062D', + '\uFD59': '\u062C\u0645\u062D', + '\uFD5A': '\u062D\u0645\u064A', + '\uFD5B': '\u062D\u0645\u0649', + '\uFD5C': '\u0633\u062D\u062C', + '\uFD5D': '\u0633\u062C\u062D', + '\uFD5E': '\u0633\u062C\u0649', + '\uFD5F': '\u0633\u0645\u062D', + '\uFD60': '\u0633\u0645\u062D', + '\uFD61': '\u0633\u0645\u062C', + '\uFD62': '\u0633\u0645\u0645', + '\uFD63': '\u0633\u0645\u0645', + '\uFD64': '\u0635\u062D\u062D', + '\uFD65': '\u0635\u062D\u062D', + '\uFD66': '\u0635\u0645\u0645', + '\uFD67': '\u0634\u062D\u0645', + '\uFD68': '\u0634\u062D\u0645', + '\uFD69': '\u0634\u062C\u064A', + '\uFD6A': '\u0634\u0645\u062E', + '\uFD6B': '\u0634\u0645\u062E', + '\uFD6C': '\u0634\u0645\u0645', + '\uFD6D': '\u0634\u0645\u0645', + '\uFD6E': '\u0636\u062D\u0649', + '\uFD6F': '\u0636\u062E\u0645', + '\uFD70': '\u0636\u062E\u0645', + '\uFD71': '\u0637\u0645\u062D', + '\uFD72': '\u0637\u0645\u062D', + '\uFD73': '\u0637\u0645\u0645', + '\uFD74': '\u0637\u0645\u064A', + '\uFD75': '\u0639\u062C\u0645', + '\uFD76': '\u0639\u0645\u0645', + '\uFD77': '\u0639\u0645\u0645', + '\uFD78': '\u0639\u0645\u0649', + '\uFD79': '\u063A\u0645\u0645', + '\uFD7A': '\u063A\u0645\u064A', + '\uFD7B': '\u063A\u0645\u0649', + '\uFD7C': '\u0641\u062E\u0645', + '\uFD7D': '\u0641\u062E\u0645', + '\uFD7E': '\u0642\u0645\u062D', + '\uFD7F': '\u0642\u0645\u0645', + '\uFD80': '\u0644\u062D\u0645', + '\uFD81': '\u0644\u062D\u064A', + '\uFD82': '\u0644\u062D\u0649', + '\uFD83': '\u0644\u062C\u062C', + '\uFD84': '\u0644\u062C\u062C', + '\uFD85': '\u0644\u062E\u0645', + '\uFD86': '\u0644\u062E\u0645', + '\uFD87': '\u0644\u0645\u062D', + '\uFD88': '\u0644\u0645\u062D', + '\uFD89': '\u0645\u062D\u062C', + '\uFD8A': '\u0645\u062D\u0645', + '\uFD8B': '\u0645\u062D\u064A', + '\uFD8C': '\u0645\u062C\u062D', + '\uFD8D': '\u0645\u062C\u0645', + '\uFD8E': '\u0645\u062E\u062C', + '\uFD8F': '\u0645\u062E\u0645', + '\uFD92': '\u0645\u062C\u062E', + '\uFD93': '\u0647\u0645\u062C', + '\uFD94': '\u0647\u0645\u0645', + '\uFD95': '\u0646\u062D\u0645', + '\uFD96': '\u0646\u062D\u0649', + '\uFD97': '\u0646\u062C\u0645', + '\uFD98': '\u0646\u062C\u0645', + '\uFD99': '\u0646\u062C\u0649', + '\uFD9A': '\u0646\u0645\u064A', + '\uFD9B': '\u0646\u0645\u0649', + '\uFD9C': '\u064A\u0645\u0645', + '\uFD9D': '\u064A\u0645\u0645', + '\uFD9E': '\u0628\u062E\u064A', + '\uFD9F': '\u062A\u062C\u064A', + '\uFDA0': '\u062A\u062C\u0649', + '\uFDA1': '\u062A\u062E\u064A', + '\uFDA2': '\u062A\u062E\u0649', + '\uFDA3': '\u062A\u0645\u064A', + '\uFDA4': '\u062A\u0645\u0649', + '\uFDA5': '\u062C\u0645\u064A', + '\uFDA6': '\u062C\u062D\u0649', + '\uFDA7': '\u062C\u0645\u0649', + '\uFDA8': '\u0633\u062E\u0649', + '\uFDA9': '\u0635\u062D\u064A', + '\uFDAA': '\u0634\u062D\u064A', + '\uFDAB': '\u0636\u062D\u064A', + '\uFDAC': '\u0644\u062C\u064A', + '\uFDAD': '\u0644\u0645\u064A', + '\uFDAE': '\u064A\u062D\u064A', + '\uFDAF': '\u064A\u062C\u064A', + '\uFDB0': '\u064A\u0645\u064A', + '\uFDB1': '\u0645\u0645\u064A', + '\uFDB2': '\u0642\u0645\u064A', + '\uFDB3': '\u0646\u062D\u064A', + '\uFDB4': '\u0642\u0645\u062D', + '\uFDB5': '\u0644\u062D\u0645', + '\uFDB6': '\u0639\u0645\u064A', + '\uFDB7': '\u0643\u0645\u064A', + '\uFDB8': '\u0646\u062C\u062D', + '\uFDB9': '\u0645\u062E\u064A', + '\uFDBA': '\u0644\u062C\u0645', + '\uFDBB': '\u0643\u0645\u0645', + '\uFDBC': '\u0644\u062C\u0645', + '\uFDBD': '\u0646\u062C\u062D', + '\uFDBE': '\u062C\u062D\u064A', + '\uFDBF': '\u062D\u062C\u064A', + '\uFDC0': '\u0645\u062C\u064A', + '\uFDC1': '\u0641\u0645\u064A', + '\uFDC2': '\u0628\u062D\u064A', + '\uFDC3': '\u0643\u0645\u0645', + '\uFDC4': '\u0639\u062C\u0645', + '\uFDC5': '\u0635\u0645\u0645', + '\uFDC6': '\u0633\u062E\u064A', + '\uFDC7': '\u0646\u062C\u064A', + '\uFE49': '\u203E', + '\uFE4A': '\u203E', + '\uFE4B': '\u203E', + '\uFE4C': '\u203E', + '\uFE4D': '\u005F', + '\uFE4E': '\u005F', + '\uFE4F': '\u005F', + '\uFE80': '\u0621', + '\uFE81': '\u0622', + '\uFE82': '\u0622', + '\uFE83': '\u0623', + '\uFE84': '\u0623', + '\uFE85': '\u0624', + '\uFE86': '\u0624', + '\uFE87': '\u0625', + '\uFE88': '\u0625', + '\uFE89': '\u0626', + '\uFE8A': '\u0626', + '\uFE8B': '\u0626', + '\uFE8C': '\u0626', + '\uFE8D': '\u0627', + '\uFE8E': '\u0627', + '\uFE8F': '\u0628', + '\uFE90': '\u0628', + '\uFE91': '\u0628', + '\uFE92': '\u0628', + '\uFE93': '\u0629', + '\uFE94': '\u0629', + '\uFE95': '\u062A', + '\uFE96': '\u062A', + '\uFE97': '\u062A', + '\uFE98': '\u062A', + '\uFE99': '\u062B', + '\uFE9A': '\u062B', + '\uFE9B': '\u062B', + '\uFE9C': '\u062B', + '\uFE9D': '\u062C', + '\uFE9E': '\u062C', + '\uFE9F': '\u062C', + '\uFEA0': '\u062C', + '\uFEA1': '\u062D', + '\uFEA2': '\u062D', + '\uFEA3': '\u062D', + '\uFEA4': '\u062D', + '\uFEA5': '\u062E', + '\uFEA6': '\u062E', + '\uFEA7': '\u062E', + '\uFEA8': '\u062E', + '\uFEA9': '\u062F', + '\uFEAA': '\u062F', + '\uFEAB': '\u0630', + '\uFEAC': '\u0630', + '\uFEAD': '\u0631', + '\uFEAE': '\u0631', + '\uFEAF': '\u0632', + '\uFEB0': '\u0632', + '\uFEB1': '\u0633', + '\uFEB2': '\u0633', + '\uFEB3': '\u0633', + '\uFEB4': '\u0633', + '\uFEB5': '\u0634', + '\uFEB6': '\u0634', + '\uFEB7': '\u0634', + '\uFEB8': '\u0634', + '\uFEB9': '\u0635', + '\uFEBA': '\u0635', + '\uFEBB': '\u0635', + '\uFEBC': '\u0635', + '\uFEBD': '\u0636', + '\uFEBE': '\u0636', + '\uFEBF': '\u0636', + '\uFEC0': '\u0636', + '\uFEC1': '\u0637', + '\uFEC2': '\u0637', + '\uFEC3': '\u0637', + '\uFEC4': '\u0637', + '\uFEC5': '\u0638', + '\uFEC6': '\u0638', + '\uFEC7': '\u0638', + '\uFEC8': '\u0638', + '\uFEC9': '\u0639', + '\uFECA': '\u0639', + '\uFECB': '\u0639', + '\uFECC': '\u0639', + '\uFECD': '\u063A', + '\uFECE': '\u063A', + '\uFECF': '\u063A', + '\uFED0': '\u063A', + '\uFED1': '\u0641', + '\uFED2': '\u0641', + '\uFED3': '\u0641', + '\uFED4': '\u0641', + '\uFED5': '\u0642', + '\uFED6': '\u0642', + '\uFED7': '\u0642', + '\uFED8': '\u0642', + '\uFED9': '\u0643', + '\uFEDA': '\u0643', + '\uFEDB': '\u0643', + '\uFEDC': '\u0643', + '\uFEDD': '\u0644', + '\uFEDE': '\u0644', + '\uFEDF': '\u0644', + '\uFEE0': '\u0644', + '\uFEE1': '\u0645', + '\uFEE2': '\u0645', + '\uFEE3': '\u0645', + '\uFEE4': '\u0645', + '\uFEE5': '\u0646', + '\uFEE6': '\u0646', + '\uFEE7': '\u0646', + '\uFEE8': '\u0646', + '\uFEE9': '\u0647', + '\uFEEA': '\u0647', + '\uFEEB': '\u0647', + '\uFEEC': '\u0647', + '\uFEED': '\u0648', + '\uFEEE': '\u0648', + '\uFEEF': '\u0649', + '\uFEF0': '\u0649', + '\uFEF1': '\u064A', + '\uFEF2': '\u064A', + '\uFEF3': '\u064A', + '\uFEF4': '\u064A', + '\uFEF5': '\u0644\u0622', + '\uFEF6': '\u0644\u0622', + '\uFEF7': '\u0644\u0623', + '\uFEF8': '\u0644\u0623', + '\uFEF9': '\u0644\u0625', + '\uFEFA': '\u0644\u0625', + '\uFEFB': '\u0644\u0627', + '\uFEFC': '\u0644\u0627' +}; + +function reverseIfRtl(chars) { + var charsLength = chars.length; + //reverse an arabic ligature + if (charsLength <= 1 || !isRTLRangeFor(chars.charCodeAt(0))) { + return chars; + } + var s = ''; + for (var ii = charsLength - 1; ii >= 0; ii--) { + s += chars[ii]; + } + return s; +} + +function adjustWidths(properties) { + if (properties.fontMatrix[0] === FONT_IDENTITY_MATRIX[0]) { + return; + } + // adjusting width to fontMatrix scale + var scale = 0.001 / properties.fontMatrix[0]; + var glyphsWidths = properties.widths; + for (var glyph in glyphsWidths) { + glyphsWidths[glyph] *= scale; + } + properties.defaultWidth *= scale; +} + +function getFontType(type, subtype) { + switch (type) { + case 'Type1': + return subtype === 'Type1C' ? FontType.TYPE1C : FontType.TYPE1; + case 'CIDFontType0': + return subtype === 'CIDFontType0C' ? FontType.CIDFONTTYPE0C : + FontType.CIDFONTTYPE0; + case 'OpenType': + return FontType.OPENTYPE; + case 'TrueType': + return FontType.TRUETYPE; + case 'CIDFontType2': + return FontType.CIDFONTTYPE2; + case 'MMType1': + return FontType.MMTYPE1; + case 'Type0': + return FontType.TYPE0; + default: + return FontType.UNKNOWN; + } +} + +var Glyph = (function GlyphClosure() { + function Glyph(fontChar, unicode, accent, width, vmetric, operatorListId) { + this.fontChar = fontChar; + this.unicode = unicode; + this.accent = accent; + this.width = width; + this.vmetric = vmetric; + this.operatorListId = operatorListId; + } + + Glyph.prototype.matchesForCache = + function(fontChar, unicode, accent, width, vmetric, operatorListId) { + return this.fontChar === fontChar && + this.unicode === unicode && + this.accent === accent && + this.width === width && + this.vmetric === vmetric && + this.operatorListId === operatorListId; + }; + + return Glyph; +})(); + +var ToUnicodeMap = (function ToUnicodeMapClosure() { + function ToUnicodeMap(cmap) { + // The elements of this._map can be integers or strings, depending on how + // |cmap| was created. + this._map = cmap; + } + + ToUnicodeMap.prototype = { + get length() { + return this._map.length; + }, + + forEach: function(callback) { + for (var charCode in this._map) { + callback(charCode, this._map[charCode].charCodeAt(0)); + } + }, + + has: function(i) { + return this._map[i] !== undefined; + }, + + get: function(i) { + return this._map[i]; + }, + + charCodeOf: function(v) { + return this._map.indexOf(v); + } + }; + + return ToUnicodeMap; +})(); + +var IdentityToUnicodeMap = (function IdentityToUnicodeMapClosure() { + function IdentityToUnicodeMap(firstChar, lastChar) { + this.firstChar = firstChar; + this.lastChar = lastChar; + } + + IdentityToUnicodeMap.prototype = { + get length() { + return (this.lastChar + 1) - this.firstChar; + }, + + forEach: function (callback) { + for (var i = this.firstChar, ii = this.lastChar; i <= ii; i++) { + callback(i, i); + } + }, + + has: function (i) { + return this.firstChar <= i && i <= this.lastChar; + }, + + get: function (i) { + if (this.firstChar <= i && i <= this.lastChar) { + return String.fromCharCode(i); + } + return undefined; + }, + + charCodeOf: function (v) { + error('should not call .charCodeOf'); + } + }; + + return IdentityToUnicodeMap; +})(); + +var OpenTypeFileBuilder = (function OpenTypeFileBuilderClosure() { + function writeInt16(dest, offset, num) { + dest[offset] = (num >> 8) & 0xFF; + dest[offset + 1] = num & 0xFF; + } + + function writeInt32(dest, offset, num) { + dest[offset] = (num >> 24) & 0xFF; + dest[offset + 1] = (num >> 16) & 0xFF; + dest[offset + 2] = (num >> 8) & 0xFF; + dest[offset + 3] = num & 0xFF; + } + + function writeData(dest, offset, data) { + var i, ii; + if (data instanceof Uint8Array) { + dest.set(data, offset); + } else if (typeof data === 'string') { + for (i = 0, ii = data.length; i < ii; i++) { + dest[offset++] = data.charCodeAt(i) & 0xFF; + } + } else { + // treating everything else as array + for (i = 0, ii = data.length; i < ii; i++) { + dest[offset++] = data[i] & 0xFF; + } + } + } + + function OpenTypeFileBuilder(sfnt) { + this.sfnt = sfnt; + this.tables = Object.create(null); + } + + OpenTypeFileBuilder.getSearchParams = + function OpenTypeFileBuilder_getSearchParams(entriesCount, entrySize) { + var maxPower2 = 1, log2 = 0; + while ((maxPower2 ^ entriesCount) > maxPower2) { + maxPower2 <<= 1; + log2++; + } + var searchRange = maxPower2 * entrySize; + return { + range: searchRange, + entry: log2, + rangeShift: entrySize * entriesCount - searchRange + }; + }; + + var OTF_HEADER_SIZE = 12; + var OTF_TABLE_ENTRY_SIZE = 16; + + OpenTypeFileBuilder.prototype = { + toArray: function OpenTypeFileBuilder_toArray() { + var sfnt = this.sfnt; + + // Tables needs to be written by ascendant alphabetic order + var tables = this.tables; + var tablesNames = Object.keys(tables); + tablesNames.sort(); + var numTables = tablesNames.length; + + var i, j, jj, table, tableName; + // layout the tables data + var offset = OTF_HEADER_SIZE + numTables * OTF_TABLE_ENTRY_SIZE; + var tableOffsets = [offset]; + for (i = 0; i < numTables; i++) { + table = tables[tablesNames[i]]; + var paddedLength = ((table.length + 3) & ~3) >>> 0; + offset += paddedLength; + tableOffsets.push(offset); + } + + var file = new Uint8Array(offset); + // write the table data first (mostly for checksum) + for (i = 0; i < numTables; i++) { + table = tables[tablesNames[i]]; + writeData(file, tableOffsets[i], table); + } + + // sfnt version (4 bytes) + if (sfnt === 'true') { + // Windows hates the Mac TrueType sfnt version number + sfnt = string32(0x00010000); + } + file[0] = sfnt.charCodeAt(0) & 0xFF; + file[1] = sfnt.charCodeAt(1) & 0xFF; + file[2] = sfnt.charCodeAt(2) & 0xFF; + file[3] = sfnt.charCodeAt(3) & 0xFF; + + // numTables (2 bytes) + writeInt16(file, 4, numTables); + + var searchParams = OpenTypeFileBuilder.getSearchParams(numTables, 16); + + // searchRange (2 bytes) + writeInt16(file, 6, searchParams.range); + // entrySelector (2 bytes) + writeInt16(file, 8, searchParams.entry); + // rangeShift (2 bytes) + writeInt16(file, 10, searchParams.rangeShift); + + offset = OTF_HEADER_SIZE; + // writing table entries + for (i = 0; i < numTables; i++) { + tableName = tablesNames[i]; + file[offset] = tableName.charCodeAt(0) & 0xFF; + file[offset + 1] = tableName.charCodeAt(1) & 0xFF; + file[offset + 2] = tableName.charCodeAt(2) & 0xFF; + file[offset + 3] = tableName.charCodeAt(3) & 0xFF; + + // checksum + var checksum = 0; + for (j = tableOffsets[i], jj = tableOffsets[i + 1]; j < jj; j += 4) { + var quad = (file[j] << 24) + (file[j + 1] << 16) + + (file[j + 2] << 8) + file[j + 3]; + checksum = (checksum + quad) | 0; + } + writeInt32(file, offset + 4, checksum); + + // offset + writeInt32(file, offset + 8, tableOffsets[i]); + // length + writeInt32(file, offset + 12, tables[tableName].length); + + offset += OTF_TABLE_ENTRY_SIZE; + } + return file; + }, + + addTable: function OpenTypeFileBuilder_addTable(tag, data) { + if (tag in this.tables) { + throw new Error('Table ' + tag + ' already exists'); + } + this.tables[tag] = data; + } + }; + + return OpenTypeFileBuilder; +})(); + +/** + * 'Font' is the class the outside world should use, it encapsulate all the font + * decoding logics whatever type it is (assuming the font type is supported). + * + * For example to read a Type1 font and to attach it to the document: + * var type1Font = new Font("MyFontName", binaryFile, propertiesObject); + * type1Font.bind(); + */ +var Font = (function FontClosure() { + function Font(name, file, properties) { + var charCode, glyphName, fontChar; + + this.name = name; + this.loadedName = properties.loadedName; + this.isType3Font = properties.isType3Font; + this.sizes = []; + + this.glyphCache = {}; + + var names = name.split('+'); + names = names.length > 1 ? names[1] : names[0]; + names = names.split(/[-,_]/g)[0]; + this.isSerifFont = !!(properties.flags & FontFlags.Serif); + this.isSymbolicFont = !!(properties.flags & FontFlags.Symbolic); + this.isMonospace = !!(properties.flags & FontFlags.FixedPitch); + + var type = properties.type; + var subtype = properties.subtype; + this.type = type; + + this.fallbackName = (this.isMonospace ? 'monospace' : + (this.isSerifFont ? 'serif' : 'sans-serif')); + + this.differences = properties.differences; + this.widths = properties.widths; + this.defaultWidth = properties.defaultWidth; + this.composite = properties.composite; + this.wideChars = properties.wideChars; + this.cMap = properties.cMap; + this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS; + this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS; + this.fontMatrix = properties.fontMatrix; + this.bbox = properties.bbox; + + this.toUnicode = properties.toUnicode = this.buildToUnicode(properties); + + this.toFontChar = []; + + if (properties.type === 'Type3') { + for (charCode = 0; charCode < 256; charCode++) { + this.toFontChar[charCode] = (this.differences[charCode] || + properties.defaultEncoding[charCode]); + } + this.fontType = FontType.TYPE3; + return; + } + + this.cidEncoding = properties.cidEncoding; + this.vertical = properties.vertical; + if (this.vertical) { + this.vmetrics = properties.vmetrics; + this.defaultVMetrics = properties.defaultVMetrics; + } + + if (!file || file.isEmpty) { + if (file) { + // Some bad PDF generators will include empty font files, + // attempting to recover by assuming that no file exists. + warn('Font file is empty in "' + name + '" (' + this.loadedName + ')'); + } + + this.missingFile = true; + // The file data is not specified. Trying to fix the font name + // to be used with the canvas.font. + var fontName = name.replace(/[,_]/g, '-'); + var isStandardFont = !!stdFontMap[fontName] || + !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]); + fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName; + + this.bold = (fontName.search(/bold/gi) !== -1); + this.italic = ((fontName.search(/oblique/gi) !== -1) || + (fontName.search(/italic/gi) !== -1)); + + // Use 'name' instead of 'fontName' here because the original + // name ArialBlack for example will be replaced by Helvetica. + this.black = (name.search(/Black/g) !== -1); + + // if at least one width is present, remeasure all chars when exists + this.remeasure = Object.keys(this.widths).length > 0; + if (isStandardFont && type === 'CIDFontType2' && + properties.cidEncoding.indexOf('Identity-') === 0) { + // Standard fonts might be embedded as CID font without glyph mapping. + // Building one based on GlyphMapForStandardFonts. + var map = []; + for (var code in GlyphMapForStandardFonts) { + map[+code] = GlyphMapForStandardFonts[code]; + } + var isIdentityUnicode = this.toUnicode instanceof IdentityToUnicodeMap; + if (!isIdentityUnicode) { + this.toUnicode.forEach(function(charCode, unicodeCharCode) { + map[+charCode] = unicodeCharCode; + }); + } + this.toFontChar = map; + this.toUnicode = new ToUnicodeMap(map); + } else if (/Symbol/i.test(fontName)) { + var symbols = Encodings.SymbolSetEncoding; + for (charCode in symbols) { + fontChar = GlyphsUnicode[symbols[charCode]]; + if (!fontChar) { + continue; + } + this.toFontChar[charCode] = fontChar; + } + for (charCode in properties.differences) { + fontChar = GlyphsUnicode[properties.differences[charCode]]; + if (!fontChar) { + continue; + } + this.toFontChar[charCode] = fontChar; + } + } else if (/Dingbats/i.test(fontName)) { + if (/Wingdings/i.test(name)) { + warn('Wingdings font without embedded font file, ' + + 'falling back to the ZapfDingbats encoding.'); + } + var dingbats = Encodings.ZapfDingbatsEncoding; + for (charCode in dingbats) { + fontChar = DingbatsGlyphsUnicode[dingbats[charCode]]; + if (!fontChar) { + continue; + } + this.toFontChar[charCode] = fontChar; + } + for (charCode in properties.differences) { + fontChar = DingbatsGlyphsUnicode[properties.differences[charCode]]; + if (!fontChar) { + continue; + } + this.toFontChar[charCode] = fontChar; + } + } else if (isStandardFont) { + this.toFontChar = []; + for (charCode in properties.defaultEncoding) { + glyphName = (properties.differences[charCode] || + properties.defaultEncoding[charCode]); + this.toFontChar[charCode] = GlyphsUnicode[glyphName]; + } + } else { + var unicodeCharCode, notCidFont = (type.indexOf('CIDFontType') === -1); + this.toUnicode.forEach(function(charCode, unicodeCharCode) { + if (notCidFont) { + glyphName = (properties.differences[charCode] || + properties.defaultEncoding[charCode]); + unicodeCharCode = (GlyphsUnicode[glyphName] || unicodeCharCode); + } + this.toFontChar[charCode] = unicodeCharCode; + }.bind(this)); + } + this.loadedName = fontName.split('-')[0]; + this.loading = false; + this.fontType = getFontType(type, subtype); + return; + } + + // Some fonts might use wrong font types for Type1C or CIDFontType0C + if (subtype === 'Type1C' && (type !== 'Type1' && type !== 'MMType1')) { + // Some TrueType fonts by mistake claim Type1C + if (isTrueTypeFile(file)) { + subtype = 'TrueType'; + } else { + type = 'Type1'; + } + } + if (subtype === 'CIDFontType0C' && type !== 'CIDFontType0') { + type = 'CIDFontType0'; + } + if (subtype === 'OpenType') { + type = 'OpenType'; + } + // Some CIDFontType0C fonts by mistake claim CIDFontType0. + if (type === 'CIDFontType0') { + subtype = isType1File(file) ? 'CIDFontType0' : 'CIDFontType0C'; + } + + var data; + switch (type) { + case 'MMType1': + info('MMType1 font (' + name + '), falling back to Type1.'); + /* falls through */ + case 'Type1': + case 'CIDFontType0': + this.mimetype = 'font/opentype'; + + var cff = (subtype === 'Type1C' || subtype === 'CIDFontType0C') ? + new CFFFont(file, properties) : new Type1Font(name, file, properties); + + adjustWidths(properties); + + // Wrap the CFF data inside an OTF font file + data = this.convert(name, cff, properties); + break; + + case 'OpenType': + case 'TrueType': + case 'CIDFontType2': + this.mimetype = 'font/opentype'; + + // Repair the TrueType file. It is can be damaged in the point of + // view of the sanitizer + data = this.checkAndRepair(name, file, properties); + if (this.isOpenType) { + type = 'OpenType'; + } + break; + + default: + error('Font ' + type + ' is not supported'); + break; + } + + this.data = data; + this.fontType = getFontType(type, subtype); + + // Transfer some properties again that could change during font conversion + this.fontMatrix = properties.fontMatrix; + this.widths = properties.widths; + this.defaultWidth = properties.defaultWidth; + this.encoding = properties.baseEncoding; + this.seacMap = properties.seacMap; + + this.loading = true; + } + + Font.getFontID = (function () { + var ID = 1; + return function Font_getFontID() { + return String(ID++); + }; + })(); + + function int16(b0, b1) { + return (b0 << 8) + b1; + } + + function int32(b0, b1, b2, b3) { + return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3; + } + + function string16(value) { + return String.fromCharCode((value >> 8) & 0xff, value & 0xff); + } + + function safeString16(value) { + // clamp value to the 16-bit int range + value = (value > 0x7FFF ? 0x7FFF : (value < -0x8000 ? -0x8000 : value)); + return String.fromCharCode((value >> 8) & 0xff, value & 0xff); + } + + function isTrueTypeFile(file) { + var header = file.peekBytes(4); + return readUint32(header, 0) === 0x00010000; + } + + function isType1File(file) { + var header = file.peekBytes(2); + // All Type1 font programs must begin with the comment '%!' (0x25 + 0x21). + if (header[0] === 0x25 && header[1] === 0x21) { + return true; + } + // ... obviously some fonts violate that part of the specification, + // please refer to the comment in |Type1Font| below. + if (header[0] === 0x80 && header[1] === 0x01) { // pfb file header. + return true; + } + return false; + } + + /** + * Helper function for |adjustMapping|. + * @return {boolean} + */ + function isProblematicUnicodeLocation(code) { + if (code <= 0x1F) { // Control chars + return true; + } + if (code >= 0x80 && code <= 0x9F) { // Control chars + return true; + } + if ((code >= 0x2000 && code <= 0x200F) || // General punctuation chars + (code >= 0x2028 && code <= 0x202F) || + (code >= 0x2060 && code <= 0x206F)) { + return true; + } + if (code >= 0xFFF0 && code <= 0xFFFF) { // Specials Unicode block + return true; + } + switch (code) { + case 0x7F: // Control char + case 0xA0: // Non breaking space + case 0xAD: // Soft hyphen + case 0x0E33: // Thai character SARA AM + case 0x2011: // Non breaking hyphen + case 0x205F: // Medium mathematical space + case 0x25CC: // Dotted circle (combining mark) + return true; + } + return false; + } + + /** + * Rebuilds the char code to glyph ID map by trying to replace the char codes + * with their unicode value. It also moves char codes that are in known + * problematic locations. + * @return {Object} Two properties: + * 'toFontChar' - maps original char codes(the value that will be read + * from commands such as show text) to the char codes that will be used in the + * font that we build + * 'charCodeToGlyphId' - maps the new font char codes to glyph ids + */ + function adjustMapping(charCodeToGlyphId, properties) { + var toUnicode = properties.toUnicode; + var isSymbolic = !!(properties.flags & FontFlags.Symbolic); + var isIdentityUnicode = + properties.toUnicode instanceof IdentityToUnicodeMap; + var newMap = Object.create(null); + var toFontChar = []; + var usedFontCharCodes = []; + var nextAvailableFontCharCode = PRIVATE_USE_OFFSET_START; + for (var originalCharCode in charCodeToGlyphId) { + originalCharCode |= 0; + var glyphId = charCodeToGlyphId[originalCharCode]; + var fontCharCode = originalCharCode; + // First try to map the value to a unicode position if a non identity map + // was created. + if (!isIdentityUnicode && toUnicode.has(originalCharCode)) { + var unicode = toUnicode.get(fontCharCode); + // TODO: Try to map ligatures to the correct spot. + if (unicode.length === 1) { + fontCharCode = unicode.charCodeAt(0); + } + } + // Try to move control characters, special characters and already mapped + // characters to the private use area since they will not be drawn by + // canvas if left in their current position. Also, move characters if the + // font was symbolic and there is only an identity unicode map since the + // characters probably aren't in the correct position (fixes an issue + // with firefox and thuluthfont). + if ((usedFontCharCodes[fontCharCode] !== undefined || + isProblematicUnicodeLocation(fontCharCode) || + (isSymbolic && isIdentityUnicode)) && + nextAvailableFontCharCode <= PRIVATE_USE_OFFSET_END) { // Room left. + // Loop to try and find a free spot in the private use area. + do { + fontCharCode = nextAvailableFontCharCode++; + + if (SKIP_PRIVATE_USE_RANGE_F000_TO_F01F && fontCharCode === 0xF000) { + fontCharCode = 0xF020; + nextAvailableFontCharCode = fontCharCode + 1; + } + + } while (usedFontCharCodes[fontCharCode] !== undefined && + nextAvailableFontCharCode <= PRIVATE_USE_OFFSET_END); + } + + newMap[fontCharCode] = glyphId; + toFontChar[originalCharCode] = fontCharCode; + usedFontCharCodes[fontCharCode] = true; + } + return { + toFontChar: toFontChar, + charCodeToGlyphId: newMap, + nextAvailableFontCharCode: nextAvailableFontCharCode + }; + } + + function getRanges(glyphs) { + // Array.sort() sorts by characters, not numerically, so convert to an + // array of characters. + var codes = []; + for (var charCode in glyphs) { + codes.push({ fontCharCode: charCode | 0, glyphId: glyphs[charCode] }); + } + codes.sort(function fontGetRangesSort(a, b) { + return a.fontCharCode - b.fontCharCode; + }); + + // Split the sorted codes into ranges. + var ranges = []; + var length = codes.length; + for (var n = 0; n < length; ) { + var start = codes[n].fontCharCode; + var codeIndices = [codes[n].glyphId]; + ++n; + var end = start; + while (n < length && end + 1 === codes[n].fontCharCode) { + codeIndices.push(codes[n].glyphId); + ++end; + ++n; + if (end === 0xFFFF) { + break; + } + } + ranges.push([start, end, codeIndices]); + } + + return ranges; + } + + function createCmapTable(glyphs) { + var ranges = getRanges(glyphs); + var numTables = ranges[ranges.length - 1][1] > 0xFFFF ? 2 : 1; + var cmap = '\x00\x00' + // version + string16(numTables) + // numTables + '\x00\x03' + // platformID + '\x00\x01' + // encodingID + string32(4 + numTables * 8); // start of the table record + + var i, ii, j, jj; + for (i = ranges.length - 1; i >= 0; --i) { + if (ranges[i][0] <= 0xFFFF) { break; } + } + var bmpLength = i + 1; + + if (ranges[i][0] < 0xFFFF && ranges[i][1] === 0xFFFF) { + ranges[i][1] = 0xFFFE; + } + var trailingRangesCount = ranges[i][1] < 0xFFFF ? 1 : 0; + var segCount = bmpLength + trailingRangesCount; + var searchParams = OpenTypeFileBuilder.getSearchParams(segCount, 2); + + // Fill up the 4 parallel arrays describing the segments. + var startCount = ''; + var endCount = ''; + var idDeltas = ''; + var idRangeOffsets = ''; + var glyphsIds = ''; + var bias = 0; + + var range, start, end, codes; + for (i = 0, ii = bmpLength; i < ii; i++) { + range = ranges[i]; + start = range[0]; + end = range[1]; + startCount += string16(start); + endCount += string16(end); + codes = range[2]; + var contiguous = true; + for (j = 1, jj = codes.length; j < jj; ++j) { + if (codes[j] !== codes[j - 1] + 1) { + contiguous = false; + break; + } + } + if (!contiguous) { + var offset = (segCount - i) * 2 + bias * 2; + bias += (end - start + 1); + + idDeltas += string16(0); + idRangeOffsets += string16(offset); + + for (j = 0, jj = codes.length; j < jj; ++j) { + glyphsIds += string16(codes[j]); + } + } else { + var startCode = codes[0]; + + idDeltas += string16((startCode - start) & 0xFFFF); + idRangeOffsets += string16(0); + } + } + + if (trailingRangesCount > 0) { + endCount += '\xFF\xFF'; + startCount += '\xFF\xFF'; + idDeltas += '\x00\x01'; + idRangeOffsets += '\x00\x00'; + } + + var format314 = '\x00\x00' + // language + string16(2 * segCount) + + string16(searchParams.range) + + string16(searchParams.entry) + + string16(searchParams.rangeShift) + + endCount + '\x00\x00' + startCount + + idDeltas + idRangeOffsets + glyphsIds; + + var format31012 = ''; + var header31012 = ''; + if (numTables > 1) { + cmap += '\x00\x03' + // platformID + '\x00\x0A' + // encodingID + string32(4 + numTables * 8 + + 4 + format314.length); // start of the table record + format31012 = ''; + for (i = 0, ii = ranges.length; i < ii; i++) { + range = ranges[i]; + start = range[0]; + codes = range[2]; + var code = codes[0]; + for (j = 1, jj = codes.length; j < jj; ++j) { + if (codes[j] !== codes[j - 1] + 1) { + end = range[0] + j - 1; + format31012 += string32(start) + // startCharCode + string32(end) + // endCharCode + string32(code); // startGlyphID + start = end + 1; + code = codes[j]; + } + } + format31012 += string32(start) + // startCharCode + string32(range[1]) + // endCharCode + string32(code); // startGlyphID + } + header31012 = '\x00\x0C' + // format + '\x00\x00' + // reserved + string32(format31012.length + 16) + // length + '\x00\x00\x00\x00' + // language + string32(format31012.length / 12); // nGroups + } + + return cmap + '\x00\x04' + // format + string16(format314.length + 4) + // length + format314 + header31012 + format31012; + } + + function validateOS2Table(os2) { + var stream = new Stream(os2.data); + var version = stream.getUint16(); + // TODO verify all OS/2 tables fields, but currently we validate only those + // that give us issues + stream.getBytes(60); // skipping type, misc sizes, panose, unicode ranges + var selection = stream.getUint16(); + if (version < 4 && (selection & 0x0300)) { + return false; + } + var firstChar = stream.getUint16(); + var lastChar = stream.getUint16(); + if (firstChar > lastChar) { + return false; + } + stream.getBytes(6); // skipping sTypoAscender/Descender/LineGap + var usWinAscent = stream.getUint16(); + if (usWinAscent === 0) { // makes font unreadable by windows + return false; + } + + // OS/2 appears to be valid, resetting some fields + os2.data[8] = os2.data[9] = 0; // IE rejects fonts if fsType != 0 + return true; + } + + function createOS2Table(properties, charstrings, override) { + override = override || { + unitsPerEm: 0, + yMax: 0, + yMin: 0, + ascent: 0, + descent: 0 + }; + + var ulUnicodeRange1 = 0; + var ulUnicodeRange2 = 0; + var ulUnicodeRange3 = 0; + var ulUnicodeRange4 = 0; + + var firstCharIndex = null; + var lastCharIndex = 0; + + if (charstrings) { + for (var code in charstrings) { + code |= 0; + if (firstCharIndex > code || !firstCharIndex) { + firstCharIndex = code; + } + if (lastCharIndex < code) { + lastCharIndex = code; + } + + var position = getUnicodeRangeFor(code); + if (position < 32) { + ulUnicodeRange1 |= 1 << position; + } else if (position < 64) { + ulUnicodeRange2 |= 1 << position - 32; + } else if (position < 96) { + ulUnicodeRange3 |= 1 << position - 64; + } else if (position < 123) { + ulUnicodeRange4 |= 1 << position - 96; + } else { + error('Unicode ranges Bits > 123 are reserved for internal usage'); + } + } + } else { + // TODO + firstCharIndex = 0; + lastCharIndex = 255; + } + + var bbox = properties.bbox || [0, 0, 0, 0]; + var unitsPerEm = (override.unitsPerEm || + 1 / (properties.fontMatrix || FONT_IDENTITY_MATRIX)[0]); + + // if the font units differ to the PDF glyph space units + // then scale up the values + var scale = (properties.ascentScaled ? 1.0 : + unitsPerEm / PDF_GLYPH_SPACE_UNITS); + + var typoAscent = (override.ascent || + Math.round(scale * (properties.ascent || bbox[3]))); + var typoDescent = (override.descent || + Math.round(scale * (properties.descent || bbox[1]))); + if (typoDescent > 0 && properties.descent > 0 && bbox[1] < 0) { + typoDescent = -typoDescent; // fixing incorrect descent + } + var winAscent = override.yMax || typoAscent; + var winDescent = -override.yMin || -typoDescent; + + return '\x00\x03' + // version + '\x02\x24' + // xAvgCharWidth + '\x01\xF4' + // usWeightClass + '\x00\x05' + // usWidthClass + '\x00\x00' + // fstype (0 to let the font loads via font-face on IE) + '\x02\x8A' + // ySubscriptXSize + '\x02\xBB' + // ySubscriptYSize + '\x00\x00' + // ySubscriptXOffset + '\x00\x8C' + // ySubscriptYOffset + '\x02\x8A' + // ySuperScriptXSize + '\x02\xBB' + // ySuperScriptYSize + '\x00\x00' + // ySuperScriptXOffset + '\x01\xDF' + // ySuperScriptYOffset + '\x00\x31' + // yStrikeOutSize + '\x01\x02' + // yStrikeOutPosition + '\x00\x00' + // sFamilyClass + '\x00\x00\x06' + + String.fromCharCode(properties.fixedPitch ? 0x09 : 0x00) + + '\x00\x00\x00\x00\x00\x00' + // Panose + string32(ulUnicodeRange1) + // ulUnicodeRange1 (Bits 0-31) + string32(ulUnicodeRange2) + // ulUnicodeRange2 (Bits 32-63) + string32(ulUnicodeRange3) + // ulUnicodeRange3 (Bits 64-95) + string32(ulUnicodeRange4) + // ulUnicodeRange4 (Bits 96-127) + '\x2A\x32\x31\x2A' + // achVendID + string16(properties.italicAngle ? 1 : 0) + // fsSelection + string16(firstCharIndex || + properties.firstChar) + // usFirstCharIndex + string16(lastCharIndex || properties.lastChar) + // usLastCharIndex + string16(typoAscent) + // sTypoAscender + string16(typoDescent) + // sTypoDescender + '\x00\x64' + // sTypoLineGap (7%-10% of the unitsPerEM value) + string16(winAscent) + // usWinAscent + string16(winDescent) + // usWinDescent + '\x00\x00\x00\x00' + // ulCodePageRange1 (Bits 0-31) + '\x00\x00\x00\x00' + // ulCodePageRange2 (Bits 32-63) + string16(properties.xHeight) + // sxHeight + string16(properties.capHeight) + // sCapHeight + string16(0) + // usDefaultChar + string16(firstCharIndex || properties.firstChar) + // usBreakChar + '\x00\x03'; // usMaxContext + } + + function createPostTable(properties) { + var angle = Math.floor(properties.italicAngle * (Math.pow(2, 16))); + return ('\x00\x03\x00\x00' + // Version number + string32(angle) + // italicAngle + '\x00\x00' + // underlinePosition + '\x00\x00' + // underlineThickness + string32(properties.fixedPitch) + // isFixedPitch + '\x00\x00\x00\x00' + // minMemType42 + '\x00\x00\x00\x00' + // maxMemType42 + '\x00\x00\x00\x00' + // minMemType1 + '\x00\x00\x00\x00'); // maxMemType1 + } + + function createNameTable(name, proto) { + if (!proto) { + proto = [[], []]; // no strings and unicode strings + } + + var strings = [ + proto[0][0] || 'Original licence', // 0.Copyright + proto[0][1] || name, // 1.Font family + proto[0][2] || 'Unknown', // 2.Font subfamily (font weight) + proto[0][3] || 'uniqueID', // 3.Unique ID + proto[0][4] || name, // 4.Full font name + proto[0][5] || 'Version 0.11', // 5.Version + proto[0][6] || '', // 6.Postscript name + proto[0][7] || 'Unknown', // 7.Trademark + proto[0][8] || 'Unknown', // 8.Manufacturer + proto[0][9] || 'Unknown' // 9.Designer + ]; + + // Mac want 1-byte per character strings while Windows want + // 2-bytes per character, so duplicate the names table + var stringsUnicode = []; + var i, ii, j, jj, str; + for (i = 0, ii = strings.length; i < ii; i++) { + str = proto[1][i] || strings[i]; + + var strBufUnicode = []; + for (j = 0, jj = str.length; j < jj; j++) { + strBufUnicode.push(string16(str.charCodeAt(j))); + } + stringsUnicode.push(strBufUnicode.join('')); + } + + var names = [strings, stringsUnicode]; + var platforms = ['\x00\x01', '\x00\x03']; + var encodings = ['\x00\x00', '\x00\x01']; + var languages = ['\x00\x00', '\x04\x09']; + + var namesRecordCount = strings.length * platforms.length; + var nameTable = + '\x00\x00' + // format + string16(namesRecordCount) + // Number of names Record + string16(namesRecordCount * 12 + 6); // Storage + + // Build the name records field + var strOffset = 0; + for (i = 0, ii = platforms.length; i < ii; i++) { + var strs = names[i]; + for (j = 0, jj = strs.length; j < jj; j++) { + str = strs[j]; + var nameRecord = + platforms[i] + // platform ID + encodings[i] + // encoding ID + languages[i] + // language ID + string16(j) + // name ID + string16(str.length) + + string16(strOffset); + nameTable += nameRecord; + strOffset += str.length; + } + } + + nameTable += strings.join('') + stringsUnicode.join(''); + return nameTable; + } + + Font.prototype = { + name: null, + font: null, + mimetype: null, + encoding: null, + get renderer() { + var renderer = FontRendererFactory.create(this); + return shadow(this, 'renderer', renderer); + }, + + exportData: function Font_exportData() { + var data = {}; + for (var i in this) { + if (this.hasOwnProperty(i)) { + data[i] = this[i]; + } + } + return data; + }, + + checkAndRepair: function Font_checkAndRepair(name, font, properties) { + function readTableEntry(file) { + var tag = bytesToString(file.getBytes(4)); + + var checksum = file.getInt32(); + var offset = file.getInt32() >>> 0; + var length = file.getInt32() >>> 0; + + // Read the table associated data + var previousPosition = file.pos; + file.pos = file.start ? file.start : 0; + file.skip(offset); + var data = file.getBytes(length); + file.pos = previousPosition; + + if (tag === 'head') { + // clearing checksum adjustment + data[8] = data[9] = data[10] = data[11] = 0; + data[17] |= 0x20; //Set font optimized for cleartype flag + } + + return { + tag: tag, + checksum: checksum, + length: length, + offset: offset, + data: data + }; + } + + function readOpenTypeHeader(ttf) { + return { + version: bytesToString(ttf.getBytes(4)), + numTables: ttf.getUint16(), + searchRange: ttf.getUint16(), + entrySelector: ttf.getUint16(), + rangeShift: ttf.getUint16() + }; + } + + /** + * Read the appropriate subtable from the cmap according to 9.6.6.4 from + * PDF spec + */ + function readCmapTable(cmap, font, isSymbolicFont) { + var segment; + var start = (font.start ? font.start : 0) + cmap.offset; + font.pos = start; + + var version = font.getUint16(); + var numTables = font.getUint16(); + + var potentialTable; + var canBreak = false; + // There's an order of preference in terms of which cmap subtable to + // use: + // - non-symbolic fonts the preference is a 3,1 table then a 1,0 table + // - symbolic fonts the preference is a 3,0 table then a 1,0 table + // The following takes advantage of the fact that the tables are sorted + // to work. + for (var i = 0; i < numTables; i++) { + var platformId = font.getUint16(); + var encodingId = font.getUint16(); + var offset = font.getInt32() >>> 0; + var useTable = false; + + if (platformId === 0 && encodingId === 0) { + useTable = true; + // Continue the loop since there still may be a higher priority + // table. + } else if (platformId === 1 && encodingId === 0) { + useTable = true; + // Continue the loop since there still may be a higher priority + // table. + } else if (platformId === 3 && encodingId === 1 && + (!isSymbolicFont || !potentialTable)) { + useTable = true; + if (!isSymbolicFont) { + canBreak = true; + } + } else if (isSymbolicFont && platformId === 3 && encodingId === 0) { + useTable = true; + canBreak = true; + } + + if (useTable) { + potentialTable = { + platformId: platformId, + encodingId: encodingId, + offset: offset + }; + } + if (canBreak) { + break; + } + } + + if (potentialTable) { + font.pos = start + potentialTable.offset; + } + if (!potentialTable || font.peekByte() === -1) { + warn('Could not find a preferred cmap table.'); + return { + platformId: -1, + encodingId: -1, + mappings: [], + hasShortCmap: false + }; + } + + var format = font.getUint16(); + var length = font.getUint16(); + var language = font.getUint16(); + + var hasShortCmap = false; + var mappings = []; + var j, glyphId; + + // TODO(mack): refactor this cmap subtable reading logic out + if (format === 0) { + for (j = 0; j < 256; j++) { + var index = font.getByte(); + if (!index) { + continue; + } + mappings.push({ + charCode: j, + glyphId: index + }); + } + hasShortCmap = true; + } else if (format === 4) { + // re-creating the table in format 4 since the encoding + // might be changed + var segCount = (font.getUint16() >> 1); + font.getBytes(6); // skipping range fields + var segIndex, segments = []; + for (segIndex = 0; segIndex < segCount; segIndex++) { + segments.push({ end: font.getUint16() }); + } + font.getUint16(); + for (segIndex = 0; segIndex < segCount; segIndex++) { + segments[segIndex].start = font.getUint16(); + } + + for (segIndex = 0; segIndex < segCount; segIndex++) { + segments[segIndex].delta = font.getUint16(); + } + + var offsetsCount = 0; + for (segIndex = 0; segIndex < segCount; segIndex++) { + segment = segments[segIndex]; + var rangeOffset = font.getUint16(); + if (!rangeOffset) { + segment.offsetIndex = -1; + continue; + } + + var offsetIndex = (rangeOffset >> 1) - (segCount - segIndex); + segment.offsetIndex = offsetIndex; + offsetsCount = Math.max(offsetsCount, offsetIndex + + segment.end - segment.start + 1); + } + + var offsets = []; + for (j = 0; j < offsetsCount; j++) { + offsets.push(font.getUint16()); + } + + for (segIndex = 0; segIndex < segCount; segIndex++) { + segment = segments[segIndex]; + start = segment.start; + var end = segment.end; + var delta = segment.delta; + offsetIndex = segment.offsetIndex; + + for (j = start; j <= end; j++) { + if (j === 0xFFFF) { + continue; + } + + glyphId = (offsetIndex < 0 ? + j : offsets[offsetIndex + j - start]); + glyphId = (glyphId + delta) & 0xFFFF; + if (glyphId === 0) { + continue; + } + mappings.push({ + charCode: j, + glyphId: glyphId + }); + } + } + } else if (format === 6) { + // Format 6 is a 2-bytes dense mapping, which means the font data + // lives glue together even if they are pretty far in the unicode + // table. (This looks weird, so I can have missed something), this + // works on Linux but seems to fails on Mac so let's rewrite the + // cmap table to a 3-1-4 style + var firstCode = font.getUint16(); + var entryCount = font.getUint16(); + + for (j = 0; j < entryCount; j++) { + glyphId = font.getUint16(); + var charCode = firstCode + j; + + mappings.push({ + charCode: charCode, + glyphId: glyphId + }); + } + } else { + error('cmap table has unsupported format: ' + format); + } + + // removing duplicate entries + mappings.sort(function (a, b) { + return a.charCode - b.charCode; + }); + for (i = 1; i < mappings.length; i++) { + if (mappings[i - 1].charCode === mappings[i].charCode) { + mappings.splice(i, 1); + i--; + } + } + + return { + platformId: potentialTable.platformId, + encodingId: potentialTable.encodingId, + mappings: mappings, + hasShortCmap: hasShortCmap + }; + } + + function sanitizeMetrics(font, header, metrics, numGlyphs) { + if (!header) { + if (metrics) { + metrics.data = null; + } + return; + } + + font.pos = (font.start ? font.start : 0) + header.offset; + font.pos += header.length - 2; + var numOfMetrics = font.getUint16(); + + if (numOfMetrics > numGlyphs) { + info('The numOfMetrics (' + numOfMetrics + ') should not be ' + + 'greater than the numGlyphs (' + numGlyphs + ')'); + // Reduce numOfMetrics if it is greater than numGlyphs + numOfMetrics = numGlyphs; + header.data[34] = (numOfMetrics & 0xff00) >> 8; + header.data[35] = numOfMetrics & 0x00ff; + } + + var numOfSidebearings = numGlyphs - numOfMetrics; + var numMissing = numOfSidebearings - + ((metrics.length - numOfMetrics * 4) >> 1); + + if (numMissing > 0) { + // For each missing glyph, we set both the width and lsb to 0 (zero). + // Since we need to add two properties for each glyph, this explains + // the use of |numMissing * 2| when initializing the typed array. + var entries = new Uint8Array(metrics.length + numMissing * 2); + entries.set(metrics.data); + metrics.data = entries; + } + } + + function sanitizeGlyph(source, sourceStart, sourceEnd, dest, destStart, + hintsValid) { + if (sourceEnd - sourceStart <= 12) { + // glyph with data less than 12 is invalid one + return 0; + } + var glyf = source.subarray(sourceStart, sourceEnd); + var contoursCount = (glyf[0] << 8) | glyf[1]; + if (contoursCount & 0x8000) { + // complex glyph, writing as is + dest.set(glyf, destStart); + return glyf.length; + } + + var i, j = 10, flagsCount = 0; + for (i = 0; i < contoursCount; i++) { + var endPoint = (glyf[j] << 8) | glyf[j + 1]; + flagsCount = endPoint + 1; + j += 2; + } + // skipping instructions + var instructionsStart = j; + var instructionsLength = (glyf[j] << 8) | glyf[j + 1]; + j += 2 + instructionsLength; + var instructionsEnd = j; + // validating flags + var coordinatesLength = 0; + for (i = 0; i < flagsCount; i++) { + var flag = glyf[j++]; + if (flag & 0xC0) { + // reserved flags must be zero, cleaning up + glyf[j - 1] = flag & 0x3F; + } + var xyLength = ((flag & 2) ? 1 : (flag & 16) ? 0 : 2) + + ((flag & 4) ? 1 : (flag & 32) ? 0 : 2); + coordinatesLength += xyLength; + if (flag & 8) { + var repeat = glyf[j++]; + i += repeat; + coordinatesLength += repeat * xyLength; + } + } + // glyph without coordinates will be rejected + if (coordinatesLength === 0) { + return 0; + } + var glyphDataLength = j + coordinatesLength; + if (glyphDataLength > glyf.length) { + // not enough data for coordinates + return 0; + } + if (!hintsValid && instructionsLength > 0) { + dest.set(glyf.subarray(0, instructionsStart), destStart); + dest.set([0, 0], destStart + instructionsStart); + dest.set(glyf.subarray(instructionsEnd, glyphDataLength), + destStart + instructionsStart + 2); + glyphDataLength -= instructionsLength; + if (glyf.length - glyphDataLength > 3) { + glyphDataLength = (glyphDataLength + 3) & ~3; + } + return glyphDataLength; + } + if (glyf.length - glyphDataLength > 3) { + // truncating and aligning to 4 bytes the long glyph data + glyphDataLength = (glyphDataLength + 3) & ~3; + dest.set(glyf.subarray(0, glyphDataLength), destStart); + return glyphDataLength; + } + // glyph data is fine + dest.set(glyf, destStart); + return glyf.length; + } + + function sanitizeHead(head, numGlyphs, locaLength) { + var data = head.data; + + // Validate version: + // Should always be 0x00010000 + var version = int32(data[0], data[1], data[2], data[3]); + if (version >> 16 !== 1) { + info('Attempting to fix invalid version in head table: ' + version); + data[0] = 0; + data[1] = 1; + data[2] = 0; + data[3] = 0; + } + + var indexToLocFormat = int16(data[50], data[51]); + if (indexToLocFormat < 0 || indexToLocFormat > 1) { + info('Attempting to fix invalid indexToLocFormat in head table: ' + + indexToLocFormat); + + // The value of indexToLocFormat should be 0 if the loca table + // consists of short offsets, and should be 1 if the loca table + // consists of long offsets. + // + // The number of entries in the loca table should be numGlyphs + 1. + // + // Using this information, we can work backwards to deduce if the + // size of each offset in the loca table, and thus figure out the + // appropriate value for indexToLocFormat. + + var numGlyphsPlusOne = numGlyphs + 1; + if (locaLength === numGlyphsPlusOne << 1) { + // 0x0000 indicates the loca table consists of short offsets + data[50] = 0; + data[51] = 0; + } else if (locaLength === numGlyphsPlusOne << 2) { + // 0x0001 indicates the loca table consists of long offsets + data[50] = 0; + data[51] = 1; + } else { + warn('Could not fix indexToLocFormat: ' + indexToLocFormat); + } + } + } + + function sanitizeGlyphLocations(loca, glyf, numGlyphs, + isGlyphLocationsLong, hintsValid, + dupFirstEntry) { + var itemSize, itemDecode, itemEncode; + if (isGlyphLocationsLong) { + itemSize = 4; + itemDecode = function fontItemDecodeLong(data, offset) { + return (data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]; + }; + itemEncode = function fontItemEncodeLong(data, offset, value) { + data[offset] = (value >>> 24) & 0xFF; + data[offset + 1] = (value >> 16) & 0xFF; + data[offset + 2] = (value >> 8) & 0xFF; + data[offset + 3] = value & 0xFF; + }; + } else { + itemSize = 2; + itemDecode = function fontItemDecode(data, offset) { + return (data[offset] << 9) | (data[offset + 1] << 1); + }; + itemEncode = function fontItemEncode(data, offset, value) { + data[offset] = (value >> 9) & 0xFF; + data[offset + 1] = (value >> 1) & 0xFF; + }; + } + var locaData = loca.data; + var locaDataSize = itemSize * (1 + numGlyphs); + // is loca.data too short or long? + if (locaData.length !== locaDataSize) { + locaData = new Uint8Array(locaDataSize); + locaData.set(loca.data.subarray(0, locaDataSize)); + loca.data = locaData; + } + // removing the invalid glyphs + var oldGlyfData = glyf.data; + var oldGlyfDataLength = oldGlyfData.length; + var newGlyfData = new Uint8Array(oldGlyfDataLength); + var startOffset = itemDecode(locaData, 0); + var writeOffset = 0; + var missingGlyphData = {}; + itemEncode(locaData, 0, writeOffset); + var i, j; + for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) { + var endOffset = itemDecode(locaData, j); + if (endOffset > oldGlyfDataLength && + ((oldGlyfDataLength + 3) & ~3) === endOffset) { + // Aspose breaks fonts by aligning the glyphs to the qword, but not + // the glyf table size, which makes last glyph out of range. + endOffset = oldGlyfDataLength; + } + if (endOffset > oldGlyfDataLength) { + // glyph end offset points outside glyf data, rejecting the glyph + itemEncode(locaData, j, writeOffset); + startOffset = endOffset; + continue; + } + + if (startOffset === endOffset) { + missingGlyphData[i] = true; + } + + var newLength = sanitizeGlyph(oldGlyfData, startOffset, endOffset, + newGlyfData, writeOffset, hintsValid); + writeOffset += newLength; + itemEncode(locaData, j, writeOffset); + startOffset = endOffset; + } + + if (writeOffset === 0) { + // glyf table cannot be empty -- redoing the glyf and loca tables + // to have single glyph with one point + var simpleGlyph = new Uint8Array( + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0]); + for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) { + itemEncode(locaData, j, simpleGlyph.length); + } + glyf.data = simpleGlyph; + return missingGlyphData; + } + + if (dupFirstEntry) { + var firstEntryLength = itemDecode(locaData, itemSize); + if (newGlyfData.length > firstEntryLength + writeOffset) { + glyf.data = newGlyfData.subarray(0, firstEntryLength + writeOffset); + } else { + glyf.data = new Uint8Array(firstEntryLength + writeOffset); + glyf.data.set(newGlyfData.subarray(0, writeOffset)); + } + glyf.data.set(newGlyfData.subarray(0, firstEntryLength), writeOffset); + itemEncode(loca.data, locaData.length - itemSize, + writeOffset + firstEntryLength); + } else { + glyf.data = newGlyfData.subarray(0, writeOffset); + } + return missingGlyphData; + } + + function readPostScriptTable(post, properties, maxpNumGlyphs) { + var start = (font.start ? font.start : 0) + post.offset; + font.pos = start; + + var length = post.length, end = start + length; + var version = font.getInt32(); + // skip rest to the tables + font.getBytes(28); + + var glyphNames; + var valid = true; + var i; + + switch (version) { + case 0x00010000: + glyphNames = MacStandardGlyphOrdering; + break; + case 0x00020000: + var numGlyphs = font.getUint16(); + if (numGlyphs !== maxpNumGlyphs) { + valid = false; + break; + } + var glyphNameIndexes = []; + for (i = 0; i < numGlyphs; ++i) { + var index = font.getUint16(); + if (index >= 32768) { + valid = false; + break; + } + glyphNameIndexes.push(index); + } + if (!valid) { + break; + } + var customNames = []; + var strBuf = []; + while (font.pos < end) { + var stringLength = font.getByte(); + strBuf.length = stringLength; + for (i = 0; i < stringLength; ++i) { + strBuf[i] = String.fromCharCode(font.getByte()); + } + customNames.push(strBuf.join('')); + } + glyphNames = []; + for (i = 0; i < numGlyphs; ++i) { + var j = glyphNameIndexes[i]; + if (j < 258) { + glyphNames.push(MacStandardGlyphOrdering[j]); + continue; + } + glyphNames.push(customNames[j - 258]); + } + break; + case 0x00030000: + break; + default: + warn('Unknown/unsupported post table version ' + version); + valid = false; + if (properties.defaultEncoding) { + glyphNames = properties.defaultEncoding; + } + break; + } + properties.glyphNames = glyphNames; + return valid; + } + + function readNameTable(nameTable) { + var start = (font.start ? font.start : 0) + nameTable.offset; + font.pos = start; + + var names = [[], []]; + var length = nameTable.length, end = start + length; + var format = font.getUint16(); + var FORMAT_0_HEADER_LENGTH = 6; + if (format !== 0 || length < FORMAT_0_HEADER_LENGTH) { + // unsupported name table format or table "too" small + return names; + } + var numRecords = font.getUint16(); + var stringsStart = font.getUint16(); + var records = []; + var NAME_RECORD_LENGTH = 12; + var i, ii; + + for (i = 0; i < numRecords && + font.pos + NAME_RECORD_LENGTH <= end; i++) { + var r = { + platform: font.getUint16(), + encoding: font.getUint16(), + language: font.getUint16(), + name: font.getUint16(), + length: font.getUint16(), + offset: font.getUint16() + }; + // using only Macintosh and Windows platform/encoding names + if ((r.platform === 1 && r.encoding === 0 && r.language === 0) || + (r.platform === 3 && r.encoding === 1 && r.language === 0x409)) { + records.push(r); + } + } + for (i = 0, ii = records.length; i < ii; i++) { + var record = records[i]; + var pos = start + stringsStart + record.offset; + if (pos + record.length > end) { + continue; // outside of name table, ignoring + } + font.pos = pos; + var nameIndex = record.name; + if (record.encoding) { + // unicode + var str = ''; + for (var j = 0, jj = record.length; j < jj; j += 2) { + str += String.fromCharCode(font.getUint16()); + } + names[1][nameIndex] = str; + } else { + names[0][nameIndex] = bytesToString(font.getBytes(record.length)); + } + } + return names; + } + + var TTOpsStackDeltas = [ + 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -2, -2, 0, 0, -2, -5, + -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1, + 1, -1, -999, 0, 1, 0, -1, -2, 0, -1, -2, -1, -1, 0, -1, -1, + 0, 0, -999, -999, -1, -1, -1, -1, -2, -999, -2, -2, -999, 0, -2, -2, + 0, 0, -2, 0, -2, 0, 0, 0, -2, -1, -1, 1, 1, 0, 0, -1, + -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, -999, -1, -1, + -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -2, -999, -999, -999, -999, -999, -1, -1, -2, -2, 0, 0, 0, 0, -1, -1, + -999, -2, -2, 0, 0, -1, -2, -2, 0, 0, 0, -1, -1, -1, -2]; + // 0xC0-DF == -1 and 0xE0-FF == -2 + + function sanitizeTTProgram(table, ttContext) { + var data = table.data; + var i = 0, j, n, b, funcId, pc, lastEndf = 0, lastDeff = 0; + var stack = []; + var callstack = []; + var functionsCalled = []; + var tooComplexToFollowFunctions = + ttContext.tooComplexToFollowFunctions; + var inFDEF = false, ifLevel = 0, inELSE = 0; + for (var ii = data.length; i < ii;) { + var op = data[i++]; + // The TrueType instruction set docs can be found at + // https://developer.apple.com/fonts/TTRefMan/RM05/Chap5.html + if (op === 0x40) { // NPUSHB - pushes n bytes + n = data[i++]; + if (inFDEF || inELSE) { + i += n; + } else { + for (j = 0; j < n; j++) { + stack.push(data[i++]); + } + } + } else if (op === 0x41) { // NPUSHW - pushes n words + n = data[i++]; + if (inFDEF || inELSE) { + i += n * 2; + } else { + for (j = 0; j < n; j++) { + b = data[i++]; + stack.push((b << 8) | data[i++]); + } + } + } else if ((op & 0xF8) === 0xB0) { // PUSHB - pushes bytes + n = op - 0xB0 + 1; + if (inFDEF || inELSE) { + i += n; + } else { + for (j = 0; j < n; j++) { + stack.push(data[i++]); + } + } + } else if ((op & 0xF8) === 0xB8) { // PUSHW - pushes words + n = op - 0xB8 + 1; + if (inFDEF || inELSE) { + i += n * 2; + } else { + for (j = 0; j < n; j++) { + b = data[i++]; + stack.push((b << 8) | data[i++]); + } + } + } else if (op === 0x2B && !tooComplexToFollowFunctions) { // CALL + if (!inFDEF && !inELSE) { + // collecting inforamtion about which functions are used + funcId = stack[stack.length - 1]; + ttContext.functionsUsed[funcId] = true; + if (funcId in ttContext.functionsStackDeltas) { + stack.length += ttContext.functionsStackDeltas[funcId]; + } else if (funcId in ttContext.functionsDefined && + functionsCalled.indexOf(funcId) < 0) { + callstack.push({data: data, i: i, stackTop: stack.length - 1}); + functionsCalled.push(funcId); + pc = ttContext.functionsDefined[funcId]; + if (!pc) { + warn('TT: CALL non-existent function'); + ttContext.hintsValid = false; + return; + } + data = pc.data; + i = pc.i; + } + } + } else if (op === 0x2C && !tooComplexToFollowFunctions) { // FDEF + if (inFDEF || inELSE) { + warn('TT: nested FDEFs not allowed'); + tooComplexToFollowFunctions = true; + } + inFDEF = true; + // collecting inforamtion about which functions are defined + lastDeff = i; + funcId = stack.pop(); + ttContext.functionsDefined[funcId] = {data: data, i: i}; + } else if (op === 0x2D) { // ENDF - end of function + if (inFDEF) { + inFDEF = false; + lastEndf = i; + } else { + pc = callstack.pop(); + if (!pc) { + warn('TT: ENDF bad stack'); + ttContext.hintsValid = false; + return; + } + funcId = functionsCalled.pop(); + data = pc.data; + i = pc.i; + ttContext.functionsStackDeltas[funcId] = + stack.length - pc.stackTop; + } + } else if (op === 0x89) { // IDEF - instruction definition + if (inFDEF || inELSE) { + warn('TT: nested IDEFs not allowed'); + tooComplexToFollowFunctions = true; + } + inFDEF = true; + // recording it as a function to track ENDF + lastDeff = i; + } else if (op === 0x58) { // IF + ++ifLevel; + } else if (op === 0x1B) { // ELSE + inELSE = ifLevel; + } else if (op === 0x59) { // EIF + if (inELSE === ifLevel) { + inELSE = 0; + } + --ifLevel; + } else if (op === 0x1C) { // JMPR + if (!inFDEF && !inELSE) { + var offset = stack[stack.length - 1]; + // only jumping forward to prevent infinite loop + if (offset > 0) { + i += offset - 1; + } + } + } + // Adjusting stack not extactly, but just enough to get function id + if (!inFDEF && !inELSE) { + var stackDelta = op <= 0x8E ? TTOpsStackDeltas[op] : + op >= 0xC0 && op <= 0xDF ? -1 : op >= 0xE0 ? -2 : 0; + if (op >= 0x71 && op <= 0x75) { + n = stack.pop(); + if (n === n) { + stackDelta = -n * 2; + } + } + while (stackDelta < 0 && stack.length > 0) { + stack.pop(); + stackDelta++; + } + while (stackDelta > 0) { + stack.push(NaN); // pushing any number into stack + stackDelta--; + } + } + } + ttContext.tooComplexToFollowFunctions = tooComplexToFollowFunctions; + var content = [data]; + if (i > data.length) { + content.push(new Uint8Array(i - data.length)); + } + if (lastDeff > lastEndf) { + warn('TT: complementing a missing function tail'); + // new function definition started, but not finished + // complete function by [CLEAR, ENDF] + content.push(new Uint8Array([0x22, 0x2D])); + } + foldTTTable(table, content); + } + + function checkInvalidFunctions(ttContext, maxFunctionDefs) { + if (ttContext.tooComplexToFollowFunctions) { + return; + } + if (ttContext.functionsDefined.length > maxFunctionDefs) { + warn('TT: more functions defined than expected'); + ttContext.hintsValid = false; + return; + } + for (var j = 0, jj = ttContext.functionsUsed.length; j < jj; j++) { + if (j > maxFunctionDefs) { + warn('TT: invalid function id: ' + j); + ttContext.hintsValid = false; + return; + } + if (ttContext.functionsUsed[j] && !ttContext.functionsDefined[j]) { + warn('TT: undefined function: ' + j); + ttContext.hintsValid = false; + return; + } + } + } + + function foldTTTable(table, content) { + if (content.length > 1) { + // concatenating the content items + var newLength = 0; + var j, jj; + for (j = 0, jj = content.length; j < jj; j++) { + newLength += content[j].length; + } + newLength = (newLength + 3) & ~3; + var result = new Uint8Array(newLength); + var pos = 0; + for (j = 0, jj = content.length; j < jj; j++) { + result.set(content[j], pos); + pos += content[j].length; + } + table.data = result; + table.length = newLength; + } + } + + function sanitizeTTPrograms(fpgm, prep, cvt) { + var ttContext = { + functionsDefined: [], + functionsUsed: [], + functionsStackDeltas: [], + tooComplexToFollowFunctions: false, + hintsValid: true + }; + if (fpgm) { + sanitizeTTProgram(fpgm, ttContext); + } + if (prep) { + sanitizeTTProgram(prep, ttContext); + } + if (fpgm) { + checkInvalidFunctions(ttContext, maxFunctionDefs); + } + if (cvt && (cvt.length & 1)) { + var cvtData = new Uint8Array(cvt.length + 1); + cvtData.set(cvt.data); + cvt.data = cvtData; + } + return ttContext.hintsValid; + } + + // The following steps modify the original font data, making copy + font = new Stream(new Uint8Array(font.getBytes())); + + var VALID_TABLES = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', + 'name', 'post', 'loca', 'glyf', 'fpgm', 'prep', 'cvt ', 'CFF ']; + + var header = readOpenTypeHeader(font); + var numTables = header.numTables; + var cff, cffFile; + + var tables = { 'OS/2': null, cmap: null, head: null, hhea: null, + hmtx: null, maxp: null, name: null, post: null }; + var table; + for (var i = 0; i < numTables; i++) { + table = readTableEntry(font); + if (VALID_TABLES.indexOf(table.tag) < 0) { + continue; // skipping table if it's not a required or optional table + } + if (table.length === 0) { + continue; // skipping empty tables + } + tables[table.tag] = table; + } + + var isTrueType = !tables['CFF ']; + if (!isTrueType) { + // OpenType font + if (header.version === 'OTTO' || + !tables.head || !tables.hhea || !tables.maxp || !tables.post) { + // no major tables: throwing everything at CFFFont + cffFile = new Stream(tables['CFF '].data); + cff = new CFFFont(cffFile, properties); + + return this.convert(name, cff, properties); + } + + delete tables.glyf; + delete tables.loca; + delete tables.fpgm; + delete tables.prep; + delete tables['cvt ']; + this.isOpenType = true; + } else { + if (!tables.glyf || !tables.loca) { + error('Required "glyf" or "loca" tables are not found'); + } + this.isOpenType = false; + } + + if (!tables.maxp) { + error('Required "maxp" table is not found'); + } + + font.pos = (font.start || 0) + tables.maxp.offset; + var version = font.getInt32(); + var numGlyphs = font.getUint16(); + var maxFunctionDefs = 0; + if (version >= 0x00010000 && tables.maxp.length >= 22) { + // maxZones can be invalid + font.pos += 8; + var maxZones = font.getUint16(); + if (maxZones > 2) { // reset to 2 if font has invalid maxZones + tables.maxp.data[14] = 0; + tables.maxp.data[15] = 2; + } + font.pos += 4; + maxFunctionDefs = font.getUint16(); + } + + var dupFirstEntry = false; + if (properties.type === 'CIDFontType2' && properties.toUnicode && + properties.toUnicode.get(0) > '\u0000') { + // oracle's defect (see 3427), duplicating first entry + dupFirstEntry = true; + numGlyphs++; + tables.maxp.data[4] = numGlyphs >> 8; + tables.maxp.data[5] = numGlyphs & 255; + } + + var hintsValid = sanitizeTTPrograms(tables.fpgm, tables.prep, + tables['cvt '], maxFunctionDefs); + if (!hintsValid) { + delete tables.fpgm; + delete tables.prep; + delete tables['cvt ']; + } + + // Ensure the hmtx table contains the advance width and + // sidebearings information for numGlyphs in the maxp table + sanitizeMetrics(font, tables.hhea, tables.hmtx, numGlyphs); + + if (!tables.head) { + error('Required "head" table is not found'); + } + + sanitizeHead(tables.head, numGlyphs, isTrueType ? tables.loca.length : 0); + + var missingGlyphs = {}; + if (isTrueType) { + var isGlyphLocationsLong = int16(tables.head.data[50], + tables.head.data[51]); + missingGlyphs = sanitizeGlyphLocations(tables.loca, tables.glyf, + numGlyphs, isGlyphLocationsLong, + hintsValid, dupFirstEntry); + } + + if (!tables.hhea) { + error('Required "hhea" table is not found'); + } + + // Sanitizer reduces the glyph advanceWidth to the maxAdvanceWidth + // Sometimes it's 0. That needs to be fixed + if (tables.hhea.data[10] === 0 && tables.hhea.data[11] === 0) { + tables.hhea.data[10] = 0xFF; + tables.hhea.data[11] = 0xFF; + } + + // The 'post' table has glyphs names. + if (tables.post) { + var valid = readPostScriptTable(tables.post, properties, numGlyphs); + if (!valid) { + tables.post = null; + } + } + + var charCodeToGlyphId = [], charCode, toUnicode = properties.toUnicode; + + function hasGlyph(glyphId, charCode) { + if (!missingGlyphs[glyphId]) { + return true; + } + if (charCode >= 0 && toUnicode.has(charCode)) { + return true; + } + return false; + } + + if (properties.type === 'CIDFontType2') { + var cidToGidMap = properties.cidToGidMap || []; + var isCidToGidMapEmpty = cidToGidMap.length === 0; + + properties.cMap.forEach(function(charCode, cid) { + assert(cid <= 0xffff, 'Max size of CID is 65,535'); + var glyphId = -1; + if (isCidToGidMapEmpty) { + glyphId = charCode; + } else if (cidToGidMap[cid] !== undefined) { + glyphId = cidToGidMap[cid]; + } + + if (glyphId >= 0 && glyphId < numGlyphs && + hasGlyph(glyphId, charCode)) { + charCodeToGlyphId[charCode] = glyphId; + } + }); + if (dupFirstEntry) { + charCodeToGlyphId[0] = numGlyphs - 1; + } + } else { + // Most of the following logic in this code branch is based on the + // 9.6.6.4 of the PDF spec. + var cmapTable = readCmapTable(tables.cmap, font, this.isSymbolicFont); + var cmapPlatformId = cmapTable.platformId; + var cmapEncodingId = cmapTable.encodingId; + var cmapMappings = cmapTable.mappings; + var cmapMappingsLength = cmapMappings.length; + var hasEncoding = properties.differences.length || + !!properties.baseEncodingName; + + // The spec seems to imply that if the font is symbolic the encoding + // should be ignored, this doesn't appear to work for 'preistabelle.pdf' + // where the the font is symbolic and it has an encoding. + if (hasEncoding && + (cmapPlatformId === 3 && cmapEncodingId === 1 || + cmapPlatformId === 1 && cmapEncodingId === 0) || + (cmapPlatformId === -1 && cmapEncodingId === -1 && // Temporary hack + !!Encodings[properties.baseEncodingName])) { // Temporary hack + // When no preferred cmap table was found and |baseEncodingName| is + // one of the predefined encodings, we seem to obtain a better + // |charCodeToGlyphId| map from the code below (fixes bug 1057544). + // TODO: Note that this is a hack which should be removed as soon as + // we have proper support for more exotic cmap tables. + + var baseEncoding = []; + if (properties.baseEncodingName === 'MacRomanEncoding' || + properties.baseEncodingName === 'WinAnsiEncoding') { + baseEncoding = Encodings[properties.baseEncodingName]; + } + for (charCode = 0; charCode < 256; charCode++) { + var glyphName; + if (this.differences && charCode in this.differences) { + glyphName = this.differences[charCode]; + } else if (charCode in baseEncoding && + baseEncoding[charCode] !== '') { + glyphName = baseEncoding[charCode]; + } else { + glyphName = Encodings.StandardEncoding[charCode]; + } + if (!glyphName) { + continue; + } + var unicodeOrCharCode; + if (cmapPlatformId === 3 && cmapEncodingId === 1) { + unicodeOrCharCode = GlyphsUnicode[glyphName]; + } else if (cmapPlatformId === 1 && cmapEncodingId === 0) { + // TODO: the encoding needs to be updated with mac os table. + unicodeOrCharCode = Encodings.MacRomanEncoding.indexOf(glyphName); + } + + var found = false; + for (i = 0; i < cmapMappingsLength; ++i) { + if (cmapMappings[i].charCode === unicodeOrCharCode && + hasGlyph(cmapMappings[i].glyphId, unicodeOrCharCode)) { + charCodeToGlyphId[charCode] = cmapMappings[i].glyphId; + found = true; + break; + } + } + if (!found && properties.glyphNames) { + // Try to map using the post table. There are currently no known + // pdfs that this fixes. + var glyphId = properties.glyphNames.indexOf(glyphName); + if (glyphId > 0 && hasGlyph(glyphId, -1)) { + charCodeToGlyphId[charCode] = glyphId; + } + } + } + } else if (cmapPlatformId === 0 && cmapEncodingId === 0) { + // Default Unicode semantics, use the charcodes as is. + for (i = 0; i < cmapMappingsLength; ++i) { + charCodeToGlyphId[cmapMappings[i].charCode] = + cmapMappings[i].glyphId; + } + } else { + // For (3, 0) cmap tables: + // The charcode key being stored in charCodeToGlyphId is the lower + // byte of the two-byte charcodes of the cmap table since according to + // the spec: 'each byte from the string shall be prepended with the + // high byte of the range [of charcodes in the cmap table], to form + // a two-byte character, which shall be used to select the + // associated glyph description from the subtable'. + // + // For (1, 0) cmap tables: + // 'single bytes from the string shall be used to look up the + // associated glyph descriptions from the subtable'. This means + // charcodes in the cmap will be single bytes, so no-op since + // glyph.charCode & 0xFF === glyph.charCode + for (i = 0; i < cmapMappingsLength; ++i) { + charCode = cmapMappings[i].charCode & 0xFF; + charCodeToGlyphId[charCode] = cmapMappings[i].glyphId; + } + } + } + + if (charCodeToGlyphId.length === 0) { + // defines at least one glyph + charCodeToGlyphId[0] = 0; + } + + // Converting glyphs and ids into font's cmap table + var newMapping = adjustMapping(charCodeToGlyphId, properties); + this.toFontChar = newMapping.toFontChar; + tables.cmap = { + tag: 'cmap', + data: createCmapTable(newMapping.charCodeToGlyphId) + }; + + if (!tables['OS/2'] || !validateOS2Table(tables['OS/2'])) { + // extract some more font properties from the OpenType head and + // hhea tables; yMin and descent value are always negative + var override = { + unitsPerEm: int16(tables.head.data[18], tables.head.data[19]), + yMax: int16(tables.head.data[42], tables.head.data[43]), + yMin: int16(tables.head.data[38], tables.head.data[39]) - 0x10000, + ascent: int16(tables.hhea.data[4], tables.hhea.data[5]), + descent: int16(tables.hhea.data[6], tables.hhea.data[7]) - 0x10000 + }; + + tables['OS/2'] = { + tag: 'OS/2', + data: createOS2Table(properties, newMapping.charCodeToGlyphId, + override) + }; + } + + // Rewrite the 'post' table if needed + if (!tables.post) { + tables.post = { + tag: 'post', + data: createPostTable(properties) + }; + } + + if (!isTrueType) { + try { + // Trying to repair CFF file + cffFile = new Stream(tables['CFF '].data); + var parser = new CFFParser(cffFile, properties); + cff = parser.parse(); + var compiler = new CFFCompiler(cff); + tables['CFF '].data = compiler.compile(); + } catch (e) { + warn('Failed to compile font ' + properties.loadedName); + } + } + + // Re-creating 'name' table + if (!tables.name) { + tables.name = { + tag: 'name', + data: createNameTable(this.name) + }; + } else { + // ... using existing 'name' table as prototype + var namePrototype = readNameTable(tables.name); + tables.name.data = createNameTable(name, namePrototype); + } + + var builder = new OpenTypeFileBuilder(header.version); + for (var tableTag in tables) { + builder.addTable(tableTag, tables[tableTag].data); + } + return builder.toArray(); + }, + + convert: function Font_convert(fontName, font, properties) { + // TODO: Check the charstring widths to determine this. + properties.fixedPitch = false; + + var mapping = font.getGlyphMapping(properties); + var newMapping = adjustMapping(mapping, properties); + this.toFontChar = newMapping.toFontChar; + var numGlyphs = font.numGlyphs; + + function getCharCodes(charCodeToGlyphId, glyphId) { + var charCodes = null; + for (var charCode in charCodeToGlyphId) { + if (glyphId === charCodeToGlyphId[charCode]) { + if (!charCodes) { + charCodes = []; + } + charCodes.push(charCode | 0); + } + } + return charCodes; + } + + function createCharCode(charCodeToGlyphId, glyphId) { + for (var charCode in charCodeToGlyphId) { + if (glyphId === charCodeToGlyphId[charCode]) { + return charCode | 0; + } + } + newMapping.charCodeToGlyphId[newMapping.nextAvailableFontCharCode] = + glyphId; + return newMapping.nextAvailableFontCharCode++; + } + + var seacs = font.seacs; + if (SEAC_ANALYSIS_ENABLED && seacs && seacs.length) { + var matrix = properties.fontMatrix || FONT_IDENTITY_MATRIX; + var charset = font.getCharset(); + var seacMap = Object.create(null); + for (var glyphId in seacs) { + glyphId |= 0; + var seac = seacs[glyphId]; + var baseGlyphName = Encodings.StandardEncoding[seac[2]]; + var accentGlyphName = Encodings.StandardEncoding[seac[3]]; + var baseGlyphId = charset.indexOf(baseGlyphName); + var accentGlyphId = charset.indexOf(accentGlyphName); + if (baseGlyphId < 0 || accentGlyphId < 0) { + continue; + } + var accentOffset = { + x: seac[0] * matrix[0] + seac[1] * matrix[2] + matrix[4], + y: seac[0] * matrix[1] + seac[1] * matrix[3] + matrix[5] + }; + + var charCodes = getCharCodes(mapping, glyphId); + if (!charCodes) { + // There's no point in mapping it if the char code was never mapped + // to begin with. + continue; + } + for (var i = 0, ii = charCodes.length; i < ii; i++) { + var charCode = charCodes[i]; + // Find a fontCharCode that maps to the base and accent glyphs. + // If one doesn't exists, create it. + var charCodeToGlyphId = newMapping.charCodeToGlyphId; + var baseFontCharCode = createCharCode(charCodeToGlyphId, + baseGlyphId); + var accentFontCharCode = createCharCode(charCodeToGlyphId, + accentGlyphId); + seacMap[charCode] = { + baseFontCharCode: baseFontCharCode, + accentFontCharCode: accentFontCharCode, + accentOffset: accentOffset + }; + } + } + properties.seacMap = seacMap; + } + + var unitsPerEm = 1 / (properties.fontMatrix || FONT_IDENTITY_MATRIX)[0]; + + var builder = new OpenTypeFileBuilder('\x4F\x54\x54\x4F'); + // PostScript Font Program + builder.addTable('CFF ', font.data); + // OS/2 and Windows Specific metrics + builder.addTable('OS/2', createOS2Table(properties, + newMapping.charCodeToGlyphId)); + // Character to glyphs mapping + builder.addTable('cmap', createCmapTable(newMapping.charCodeToGlyphId)); + // Font header + builder.addTable('head', + '\x00\x01\x00\x00' + // Version number + '\x00\x00\x10\x00' + // fontRevision + '\x00\x00\x00\x00' + // checksumAdjustement + '\x5F\x0F\x3C\xF5' + // magicNumber + '\x00\x00' + // Flags + safeString16(unitsPerEm) + // unitsPerEM + '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // creation date + '\x00\x00\x00\x00\x9e\x0b\x7e\x27' + // modifification date + '\x00\x00' + // xMin + safeString16(properties.descent) + // yMin + '\x0F\xFF' + // xMax + safeString16(properties.ascent) + // yMax + string16(properties.italicAngle ? 2 : 0) + // macStyle + '\x00\x11' + // lowestRecPPEM + '\x00\x00' + // fontDirectionHint + '\x00\x00' + // indexToLocFormat + '\x00\x00'); // glyphDataFormat + + // Horizontal header + builder.addTable('hhea', + '\x00\x01\x00\x00' + // Version number + safeString16(properties.ascent) + // Typographic Ascent + safeString16(properties.descent) + // Typographic Descent + '\x00\x00' + // Line Gap + '\xFF\xFF' + // advanceWidthMax + '\x00\x00' + // minLeftSidebearing + '\x00\x00' + // minRightSidebearing + '\x00\x00' + // xMaxExtent + safeString16(properties.capHeight) + // caretSlopeRise + safeString16(Math.tan(properties.italicAngle) * + properties.xHeight) + // caretSlopeRun + '\x00\x00' + // caretOffset + '\x00\x00' + // -reserved- + '\x00\x00' + // -reserved- + '\x00\x00' + // -reserved- + '\x00\x00' + // -reserved- + '\x00\x00' + // metricDataFormat + string16(numGlyphs)); // Number of HMetrics + + // Horizontal metrics + builder.addTable('hmtx', (function fontFieldsHmtx() { + var charstrings = font.charstrings; + var cffWidths = font.cff ? font.cff.widths : null; + var hmtx = '\x00\x00\x00\x00'; // Fake .notdef + for (var i = 1, ii = numGlyphs; i < ii; i++) { + var width = 0; + if (charstrings) { + var charstring = charstrings[i - 1]; + width = 'width' in charstring ? charstring.width : 0; + } else if (cffWidths) { + width = Math.ceil(cffWidths[i] || 0); + } + hmtx += string16(width) + string16(0); + } + return hmtx; + })()); + + // Maximum profile + builder.addTable('maxp', + '\x00\x00\x50\x00' + // Version number + string16(numGlyphs)); // Num of glyphs + + // Naming tables + builder.addTable('name', createNameTable(fontName)); + + // PostScript informations + builder.addTable('post', createPostTable(properties)); + + return builder.toArray(); + }, + + /** + * Builds a char code to unicode map based on section 9.10 of the spec. + * @param {Object} properties Font properties object. + * @return {Object} A ToUnicodeMap object. + */ + buildToUnicode: function Font_buildToUnicode(properties) { + // Section 9.10.2 Mapping Character Codes to Unicode Values + if (properties.toUnicode && properties.toUnicode.length !== 0) { + return properties.toUnicode; + } + // According to the spec if the font is a simple font we should only map + // to unicode if the base encoding is MacRoman, MacExpert, or WinAnsi or + // the differences array only contains adobe standard or symbol set names, + // in pratice it seems better to always try to create a toUnicode + // map based of the default encoding. + var toUnicode, charcode; + if (!properties.composite /* is simple font */) { + toUnicode = []; + var encoding = properties.defaultEncoding.slice(); + var baseEncodingName = properties.baseEncodingName; + // Merge in the differences array. + var differences = properties.differences; + for (charcode in differences) { + encoding[charcode] = differences[charcode]; + } + for (charcode in encoding) { + // a) Map the character code to a character name. + var glyphName = encoding[charcode]; + // b) Look up the character name in the Adobe Glyph List (see the + // Bibliography) to obtain the corresponding Unicode value. + if (glyphName === '') { + continue; + } else if (GlyphsUnicode[glyphName] === undefined) { + // (undocumented) c) Few heuristics to recognize unknown glyphs + // NOTE: Adobe Reader does not do this step, but OSX Preview does + var code = 0; + switch (glyphName[0]) { + case 'G': // Gxx glyph + if (glyphName.length === 3) { + code = parseInt(glyphName.substr(1), 16); + } + break; + case 'g': // g00xx glyph + if (glyphName.length === 5) { + code = parseInt(glyphName.substr(1), 16); + } + break; + case 'C': // Cddd glyph + case 'c': // cddd glyph + if (glyphName.length >= 3) { + code = +glyphName.substr(1); + } + break; + } + if (code) { + // If |baseEncodingName| is one the predefined encodings, + // and |code| equals |charcode|, using the glyph defined in the + // baseEncoding seems to yield a better |toUnicode| mapping + // (fixes issue 5070). + if (baseEncodingName && code === +charcode) { + var baseEncoding = Encodings[baseEncodingName]; + if (baseEncoding && (glyphName = baseEncoding[charcode])) { + toUnicode[charcode] = + String.fromCharCode(GlyphsUnicode[glyphName]); + continue; + } + } + toUnicode[charcode] = String.fromCharCode(code); + } + continue; + } + toUnicode[charcode] = String.fromCharCode(GlyphsUnicode[glyphName]); + } + return new ToUnicodeMap(toUnicode); + } + // If the font is a composite font that uses one of the predefined CMaps + // listed in Table 118 (except Identity–H and Identity–V) or whose + // descendant CIDFont uses the Adobe-GB1, Adobe-CNS1, Adobe-Japan1, or + // Adobe-Korea1 character collection: + if (properties.composite && ( + (properties.cMap.builtInCMap && + !(properties.cMap instanceof IdentityCMap)) || + (properties.cidSystemInfo.registry === 'Adobe' && + (properties.cidSystemInfo.ordering === 'GB1' || + properties.cidSystemInfo.ordering === 'CNS1' || + properties.cidSystemInfo.ordering === 'Japan1' || + properties.cidSystemInfo.ordering === 'Korea1')))) { + // Then: + // a) Map the character code to a character identifier (CID) according + // to the font’s CMap. + // b) Obtain the registry and ordering of the character collection used + // by the font’s CMap (for example, Adobe and Japan1) from its + // CIDSystemInfo dictionary. + var registry = properties.cidSystemInfo.registry; + var ordering = properties.cidSystemInfo.ordering; + // c) Construct a second CMap name by concatenating the registry and + // ordering obtained in step (b) in the format registry–ordering–UCS2 + // (for example, Adobe–Japan1–UCS2). + var ucs2CMapName = new Name(registry + '-' + ordering + '-UCS2'); + // d) Obtain the CMap with the name constructed in step (c) (available + // from the ASN Web site; see the Bibliography). + var ucs2CMap = CMapFactory.create(ucs2CMapName, + { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null); + var cMap = properties.cMap; + toUnicode = []; + cMap.forEach(function(charcode, cid) { + assert(cid <= 0xffff, 'Max size of CID is 65,535'); + // e) Map the CID obtained in step (a) according to the CMap obtained + // in step (d), producing a Unicode value. + var ucs2 = ucs2CMap.lookup(cid); + if (ucs2) { + toUnicode[charcode] = + String.fromCharCode((ucs2.charCodeAt(0) << 8) + + ucs2.charCodeAt(1)); + } + }); + return new ToUnicodeMap(toUnicode); + } + + // The viewer's choice, just use an identity map. + return new IdentityToUnicodeMap(properties.firstChar, + properties.lastChar); + }, + + get spaceWidth() { + if ('_shadowWidth' in this) { + return this._shadowWidth; + } + + // trying to estimate space character width + var possibleSpaceReplacements = ['space', 'minus', 'one', 'i']; + var width; + for (var i = 0, ii = possibleSpaceReplacements.length; i < ii; i++) { + var glyphName = possibleSpaceReplacements[i]; + // if possible, getting width by glyph name + if (glyphName in this.widths) { + width = this.widths[glyphName]; + break; + } + var glyphUnicode = GlyphsUnicode[glyphName]; + // finding the charcode via unicodeToCID map + var charcode = 0; + if (this.composite) { + if (this.cMap.contains(glyphUnicode)) { + charcode = this.cMap.lookup(glyphUnicode); + } + } + // ... via toUnicode map + if (!charcode && 'toUnicode' in this) { + charcode = this.toUnicode.charCodeOf(glyphUnicode); + } + // setting it to unicode if negative or undefined + if (charcode <= 0) { + charcode = glyphUnicode; + } + // trying to get width via charcode + width = this.widths[charcode]; + if (width) { + break; // the non-zero width found + } + } + width = width || this.defaultWidth; + // Do not shadow the property here. See discussion: + // https://github.com/mozilla/pdf.js/pull/2127#discussion_r1662280 + this._shadowWidth = width; + return width; + }, + + charToGlyph: function Font_charToGlyph(charcode) { + var fontCharCode, width, operatorListId; + + var widthCode = charcode; + if (this.cMap && this.cMap.contains(charcode)) { + widthCode = this.cMap.lookup(charcode); + } + width = this.widths[widthCode]; + width = isNum(width) ? width : this.defaultWidth; + var vmetric = this.vmetrics && this.vmetrics[widthCode]; + + var unicode = this.toUnicode.get(charcode) || charcode; + if (typeof unicode === 'number') { + unicode = String.fromCharCode(unicode); + } + + // First try the toFontChar map, if it's not there then try falling + // back to the char code. + fontCharCode = this.toFontChar[charcode] || charcode; + if (this.missingFile) { + fontCharCode = mapSpecialUnicodeValues(fontCharCode); + } + + if (this.isType3Font) { + // Font char code in this case is actually a glyph name. + operatorListId = fontCharCode; + } + + var accent = null; + if (this.seacMap && this.seacMap[charcode]) { + var seac = this.seacMap[charcode]; + fontCharCode = seac.baseFontCharCode; + accent = { + fontChar: String.fromCharCode(seac.accentFontCharCode), + offset: seac.accentOffset + }; + } + + var fontChar = String.fromCharCode(fontCharCode); + + var glyph = this.glyphCache[charcode]; + if (!glyph || + !glyph.matchesForCache(fontChar, unicode, accent, width, vmetric, + operatorListId)) { + glyph = new Glyph(fontChar, unicode, accent, width, vmetric, + operatorListId); + this.glyphCache[charcode] = glyph; + } + return glyph; + }, + + charsToGlyphs: function Font_charsToGlyphs(chars) { + var charsCache = this.charsCache; + var glyphs, glyph, charcode; + + // if we translated this string before, just grab it from the cache + if (charsCache) { + glyphs = charsCache[chars]; + if (glyphs) { + return glyphs; + } + } + + // lazily create the translation cache + if (!charsCache) { + charsCache = this.charsCache = Object.create(null); + } + + glyphs = []; + var charsCacheKey = chars; + var i = 0, ii; + + if (this.cMap) { + // composite fonts have multi-byte strings convert the string from + // single-byte to multi-byte + var c = {}; + while (i < chars.length) { + this.cMap.readCharCode(chars, i, c); + charcode = c.charcode; + var length = c.length; + i += length; + glyph = this.charToGlyph(charcode); + glyphs.push(glyph); + // placing null after each word break charcode (ASCII SPACE) + // Ignore occurences of 0x20 in multiple-byte codes. + if (length === 1 && chars.charCodeAt(i - 1) === 0x20) { + glyphs.push(null); + } + } + } else { + for (i = 0, ii = chars.length; i < ii; ++i) { + charcode = chars.charCodeAt(i); + glyph = this.charToGlyph(charcode); + glyphs.push(glyph); + if (charcode === 0x20) { + glyphs.push(null); + } + } + } + + // Enter the translated string into the cache + return (charsCache[charsCacheKey] = glyphs); + } + }; + + return Font; +})(); + +var ErrorFont = (function ErrorFontClosure() { + function ErrorFont(error) { + this.error = error; + this.loadedName = 'g_font_error'; + this.loading = false; + } + + ErrorFont.prototype = { + charsToGlyphs: function ErrorFont_charsToGlyphs() { + return []; + }, + exportData: function ErrorFont_exportData() { + return {error: this.error}; + } + }; + + return ErrorFont; +})(); + +/** + * Shared logic for building a char code to glyph id mapping for Type1 and + * simple CFF fonts. See section 9.6.6.2 of the spec. + * @param {Object} properties Font properties object. + * @param {Object} builtInEncoding The encoding contained within the actual font + * data. + * @param {Array} Array of glyph names where the index is the glyph ID. + * @returns {Object} A char code to glyph ID map. + */ +function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) { + var charCodeToGlyphId = Object.create(null); + var glyphId, charCode, baseEncoding; + + if (properties.baseEncodingName) { + // If a valid base encoding name was used, the mapping is initialized with + // that. + baseEncoding = Encodings[properties.baseEncodingName]; + for (charCode = 0; charCode < baseEncoding.length; charCode++) { + glyphId = glyphNames.indexOf(baseEncoding[charCode]); + if (glyphId >= 0) { + charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef + } + } + } else if (!!(properties.flags & FontFlags.Symbolic)) { + // For a symbolic font the encoding should be the fonts built-in + // encoding. + for (charCode in builtInEncoding) { + charCodeToGlyphId[charCode] = builtInEncoding[charCode]; + } + } else { + // For non-symbolic fonts that don't have a base encoding the standard + // encoding should be used. + baseEncoding = Encodings.StandardEncoding; + for (charCode = 0; charCode < baseEncoding.length; charCode++) { + glyphId = glyphNames.indexOf(baseEncoding[charCode]); + if (glyphId >= 0) { + charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef + } + } + } + + // Lastly, merge in the differences. + var differences = properties.differences; + if (differences) { + for (charCode in differences) { + var glyphName = differences[charCode]; + glyphId = glyphNames.indexOf(glyphName); + if (glyphId >= 0) { + charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef + } + } + } + return charCodeToGlyphId; +} + +/* + * CharStrings are encoded following the the CharString Encoding sequence + * describe in Chapter 6 of the "Adobe Type1 Font Format" specification. + * The value in a byte indicates a command, a number, or subsequent bytes + * that are to be interpreted in a special way. + * + * CharString Number Encoding: + * A CharString byte containing the values from 32 through 255 inclusive + * indicate an integer. These values are decoded in four ranges. + * + * 1. A CharString byte containing a value, v, between 32 and 246 inclusive, + * indicate the integer v - 139. Thus, the integer values from -107 through + * 107 inclusive may be encoded in single byte. + * + * 2. A CharString byte containing a value, v, between 247 and 250 inclusive, + * indicates an integer involving the next byte, w, according to the formula: + * [(v - 247) x 256] + w + 108 + * + * 3. A CharString byte containing a value, v, between 251 and 254 inclusive, + * indicates an integer involving the next byte, w, according to the formula: + * -[(v - 251) * 256] - w - 108 + * + * 4. A CharString containing the value 255 indicates that the next 4 bytes + * are a two complement signed integer. The first of these bytes contains the + * highest order bits, the second byte contains the next higher order bits + * and the fourth byte contain the lowest order bits. + * + * + * CharString Command Encoding: + * CharStrings commands are encoded in 1 or 2 bytes. + * + * Single byte commands are encoded in 1 byte that contains a value between + * 0 and 31 inclusive. + * If a command byte contains the value 12, then the value in the next byte + * indicates a command. This "escape" mechanism allows many extra commands + * to be encoded and this encoding technique helps to minimize the length of + * the charStrings. + */ +var Type1CharString = (function Type1CharStringClosure() { + var COMMAND_MAP = { + 'hstem': [1], + 'vstem': [3], + 'vmoveto': [4], + 'rlineto': [5], + 'hlineto': [6], + 'vlineto': [7], + 'rrcurveto': [8], + 'callsubr': [10], + 'flex': [12, 35], + 'drop' : [12, 18], + 'endchar': [14], + 'rmoveto': [21], + 'hmoveto': [22], + 'vhcurveto': [30], + 'hvcurveto': [31] + }; + + function Type1CharString() { + this.width = 0; + this.lsb = 0; + this.flexing = false; + this.output = []; + this.stack = []; + } + + Type1CharString.prototype = { + convert: function Type1CharString_convert(encoded, subrs) { + var count = encoded.length; + var error = false; + var wx, sbx, subrNumber; + for (var i = 0; i < count; i++) { + var value = encoded[i]; + if (value < 32) { + if (value === 12) { + value = (value << 8) + encoded[++i]; + } + switch (value) { + case 1: // hstem + if (!HINTING_ENABLED) { + this.stack = []; + break; + } + error = this.executeCommand(2, COMMAND_MAP.hstem); + break; + case 3: // vstem + if (!HINTING_ENABLED) { + this.stack = []; + break; + } + error = this.executeCommand(2, COMMAND_MAP.vstem); + break; + case 4: // vmoveto + if (this.flexing) { + if (this.stack.length < 1) { + error = true; + break; + } + // Add the dx for flex and but also swap the values so they are + // the right order. + var dy = this.stack.pop(); + this.stack.push(0, dy); + break; + } + error = this.executeCommand(1, COMMAND_MAP.vmoveto); + break; + case 5: // rlineto + error = this.executeCommand(2, COMMAND_MAP.rlineto); + break; + case 6: // hlineto + error = this.executeCommand(1, COMMAND_MAP.hlineto); + break; + case 7: // vlineto + error = this.executeCommand(1, COMMAND_MAP.vlineto); + break; + case 8: // rrcurveto + error = this.executeCommand(6, COMMAND_MAP.rrcurveto); + break; + case 9: // closepath + // closepath is a Type1 command that does not take argument and is + // useless in Type2 and it can simply be ignored. + this.stack = []; + break; + case 10: // callsubr + if (this.stack.length < 1) { + error = true; + break; + } + subrNumber = this.stack.pop(); + error = this.convert(subrs[subrNumber], subrs); + break; + case 11: // return + return error; + case 13: // hsbw + if (this.stack.length < 2) { + error = true; + break; + } + // To convert to type2 we have to move the width value to the + // first part of the charstring and then use hmoveto with lsb. + wx = this.stack.pop(); + sbx = this.stack.pop(); + this.lsb = sbx; + this.width = wx; + this.stack.push(wx, sbx); + error = this.executeCommand(2, COMMAND_MAP.hmoveto); + break; + case 14: // endchar + this.output.push(COMMAND_MAP.endchar[0]); + break; + case 21: // rmoveto + if (this.flexing) { + break; + } + error = this.executeCommand(2, COMMAND_MAP.rmoveto); + break; + case 22: // hmoveto + if (this.flexing) { + // Add the dy for flex. + this.stack.push(0); + break; + } + error = this.executeCommand(1, COMMAND_MAP.hmoveto); + break; + case 30: // vhcurveto + error = this.executeCommand(4, COMMAND_MAP.vhcurveto); + break; + case 31: // hvcurveto + error = this.executeCommand(4, COMMAND_MAP.hvcurveto); + break; + case (12 << 8) + 0: // dotsection + // dotsection is a Type1 command to specify some hinting feature + // for dots that do not take a parameter and it can safely be + // ignored for Type2. + this.stack = []; + break; + case (12 << 8) + 1: // vstem3 + if (!HINTING_ENABLED) { + this.stack = []; + break; + } + // [vh]stem3 are Type1 only and Type2 supports [vh]stem with + // multiple parameters, so instead of returning [vh]stem3 take a + // shortcut and return [vhstem] instead. + error = this.executeCommand(2, COMMAND_MAP.vstem); + break; + case (12 << 8) + 2: // hstem3 + if (!HINTING_ENABLED) { + this.stack = []; + break; + } + // See vstem3. + error = this.executeCommand(2, COMMAND_MAP.hstem); + break; + case (12 << 8) + 6: // seac + // seac is like type 2's special endchar but it doesn't use the + // first argument asb, so remove it. + if (SEAC_ANALYSIS_ENABLED) { + this.seac = this.stack.splice(-4, 4); + error = this.executeCommand(0, COMMAND_MAP.endchar); + } else { + error = this.executeCommand(4, COMMAND_MAP.endchar); + } + break; + case (12 << 8) + 7: // sbw + if (this.stack.length < 4) { + error = true; + break; + } + // To convert to type2 we have to move the width value to the + // first part of the charstring and then use rmoveto with + // (dx, dy). The height argument will not be used for vmtx and + // vhea tables reconstruction -- ignoring it. + var wy = this.stack.pop(); + wx = this.stack.pop(); + var sby = this.stack.pop(); + sbx = this.stack.pop(); + this.lsb = sbx; + this.width = wx; + this.stack.push(wx, sbx, sby); + error = this.executeCommand(3, COMMAND_MAP.rmoveto); + break; + case (12 << 8) + 12: // div + if (this.stack.length < 2) { + error = true; + break; + } + var num2 = this.stack.pop(); + var num1 = this.stack.pop(); + this.stack.push(num1 / num2); + break; + case (12 << 8) + 16: // callothersubr + if (this.stack.length < 2) { + error = true; + break; + } + subrNumber = this.stack.pop(); + var numArgs = this.stack.pop(); + if (subrNumber === 0 && numArgs === 3) { + var flexArgs = this.stack.splice(this.stack.length - 17, 17); + this.stack.push( + flexArgs[2] + flexArgs[0], // bcp1x + rpx + flexArgs[3] + flexArgs[1], // bcp1y + rpy + flexArgs[4], // bcp2x + flexArgs[5], // bcp2y + flexArgs[6], // p2x + flexArgs[7], // p2y + flexArgs[8], // bcp3x + flexArgs[9], // bcp3y + flexArgs[10], // bcp4x + flexArgs[11], // bcp4y + flexArgs[12], // p3x + flexArgs[13], // p3y + flexArgs[14] // flexDepth + // 15 = finalx unused by flex + // 16 = finaly unused by flex + ); + error = this.executeCommand(13, COMMAND_MAP.flex, true); + this.flexing = false; + this.stack.push(flexArgs[15], flexArgs[16]); + } else if (subrNumber === 1 && numArgs === 0) { + this.flexing = true; + } + break; + case (12 << 8) + 17: // pop + // Ignore this since it is only used with othersubr. + break; + case (12 << 8) + 33: // setcurrentpoint + // Ignore for now. + this.stack = []; + break; + default: + warn('Unknown type 1 charstring command of "' + value + '"'); + break; + } + if (error) { + break; + } + continue; + } else if (value <= 246) { + value = value - 139; + } else if (value <= 250) { + value = ((value - 247) * 256) + encoded[++i] + 108; + } else if (value <= 254) { + value = -((value - 251) * 256) - encoded[++i] - 108; + } else { + value = (encoded[++i] & 0xff) << 24 | (encoded[++i] & 0xff) << 16 | + (encoded[++i] & 0xff) << 8 | (encoded[++i] & 0xff) << 0; + } + this.stack.push(value); + } + return error; + }, + + executeCommand: function(howManyArgs, command, keepStack) { + var stackLength = this.stack.length; + if (howManyArgs > stackLength) { + return true; + } + var start = stackLength - howManyArgs; + for (var i = start; i < stackLength; i++) { + var value = this.stack[i]; + if (value === (value | 0)) { // int + this.output.push(28, (value >> 8) & 0xff, value & 0xff); + } else { // fixed point + value = (65536 * value) | 0; + this.output.push(255, + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF); + } + } + this.output.push.apply(this.output, command); + if (keepStack) { + this.stack.splice(start, howManyArgs); + } else { + this.stack.length = 0; + } + return false; + } + }; + + return Type1CharString; +})(); + +/* + * Type1Parser encapsulate the needed code for parsing a Type1 font + * program. Some of its logic depends on the Type2 charstrings + * structure. + * Note: this doesn't really parse the font since that would require evaluation + * of PostScript, but it is possible in most cases to extract what we need + * without a full parse. + */ +var Type1Parser = (function Type1ParserClosure() { + /* + * Decrypt a Sequence of Ciphertext Bytes to Produce the Original Sequence + * of Plaintext Bytes. The function took a key as a parameter which can be + * for decrypting the eexec block of for decoding charStrings. + */ + var EEXEC_ENCRYPT_KEY = 55665; + var CHAR_STRS_ENCRYPT_KEY = 4330; + + function isHexDigit(code) { + return code >= 48 && code <= 57 || // '0'-'9' + code >= 65 && code <= 70 || // 'A'-'F' + code >= 97 && code <= 102; // 'a'-'f' + } + + function decrypt(data, key, discardNumber) { + var r = key | 0, c1 = 52845, c2 = 22719; + var count = data.length; + var decrypted = new Uint8Array(count); + for (var i = 0; i < count; i++) { + var value = data[i]; + decrypted[i] = value ^ (r >> 8); + r = ((value + r) * c1 + c2) & ((1 << 16) - 1); + } + return Array.prototype.slice.call(decrypted, discardNumber); + } + + function decryptAscii(data, key, discardNumber) { + var r = key | 0, c1 = 52845, c2 = 22719; + var count = data.length, maybeLength = count >>> 1; + var decrypted = new Uint8Array(maybeLength); + var i, j; + for (i = 0, j = 0; i < count; i++) { + var digit1 = data[i]; + if (!isHexDigit(digit1)) { + continue; + } + i++; + var digit2; + while (i < count && !isHexDigit(digit2 = data[i])) { + i++; + } + if (i < count) { + var value = parseInt(String.fromCharCode(digit1, digit2), 16); + decrypted[j++] = value ^ (r >> 8); + r = ((value + r) * c1 + c2) & ((1 << 16) - 1); + } + } + return Array.prototype.slice.call(decrypted, discardNumber, j); + } + + function isSpecial(c) { + return c === 0x2F || // '/' + c === 0x5B || c === 0x5D || // '[', ']' + c === 0x7B || c === 0x7D || // '{', '}' + c === 0x28 || c === 0x29; // '(', ')' + } + + function Type1Parser(stream, encrypted) { + if (encrypted) { + var data = stream.getBytes(); + var isBinary = !(isHexDigit(data[0]) && isHexDigit(data[1]) && + isHexDigit(data[2]) && isHexDigit(data[3])); + stream = new Stream(isBinary ? decrypt(data, EEXEC_ENCRYPT_KEY, 4) : + decryptAscii(data, EEXEC_ENCRYPT_KEY, 4)); + } + this.stream = stream; + this.nextChar(); + } + + Type1Parser.prototype = { + readNumberArray: function Type1Parser_readNumberArray() { + this.getToken(); // read '[' or '{' (arrays can start with either) + var array = []; + while (true) { + var token = this.getToken(); + if (token === null || token === ']' || token === '}') { + break; + } + array.push(parseFloat(token || 0)); + } + return array; + }, + + readNumber: function Type1Parser_readNumber() { + var token = this.getToken(); + return parseFloat(token || 0); + }, + + readInt: function Type1Parser_readInt() { + // Use '| 0' to prevent setting a double into length such as the double + // does not flow into the loop variable. + var token = this.getToken(); + return parseInt(token || 0, 10) | 0; + }, + + readBoolean: function Type1Parser_readBoolean() { + var token = this.getToken(); + + // Use 1 and 0 since that's what type2 charstrings use. + return token === 'true' ? 1 : 0; + }, + + nextChar : function Type1_nextChar() { + return (this.currentChar = this.stream.getByte()); + }, + + getToken: function Type1Parser_getToken() { + // Eat whitespace and comments. + var comment = false; + var ch = this.currentChar; + while (true) { + if (ch === -1) { + return null; + } + + if (comment) { + if (ch === 0x0A || ch === 0x0D) { + comment = false; + } + } else if (ch === 0x25) { // '%' + comment = true; + } else if (!Lexer.isSpace(ch)) { + break; + } + ch = this.nextChar(); + } + if (isSpecial(ch)) { + this.nextChar(); + return String.fromCharCode(ch); + } + var token = ''; + do { + token += String.fromCharCode(ch); + ch = this.nextChar(); + } while (ch >= 0 && !Lexer.isSpace(ch) && !isSpecial(ch)); + return token; + }, + + /* + * Returns an object containing a Subrs array and a CharStrings + * array extracted from and eexec encrypted block of data + */ + extractFontProgram: function Type1Parser_extractFontProgram() { + var stream = this.stream; + + var subrs = [], charstrings = []; + var program = { + subrs: [], + charstrings: [], + properties: { + 'privateData': { + 'lenIV': 4 + } + } + }; + var token, length, data, lenIV, encoded; + while ((token = this.getToken()) !== null) { + if (token !== '/') { + continue; + } + token = this.getToken(); + switch (token) { + case 'CharStrings': + // The number immediately following CharStrings must be greater or + // equal to the number of CharStrings. + this.getToken(); + this.getToken(); // read in 'dict' + this.getToken(); // read in 'dup' + this.getToken(); // read in 'begin' + while(true) { + token = this.getToken(); + if (token === null || token === 'end') { + break; + } + + if (token !== '/') { + continue; + } + var glyph = this.getToken(); + length = this.readInt(); + this.getToken(); // read in 'RD' or '-|' + data = stream.makeSubStream(stream.pos, length); + lenIV = program.properties.privateData['lenIV']; + encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV); + // Skip past the required space and binary data. + stream.skip(length); + this.nextChar(); + token = this.getToken(); // read in 'ND' or '|-' + if (token === 'noaccess') { + this.getToken(); // read in 'def' + } + charstrings.push({ + glyph: glyph, + encoded: encoded + }); + } + break; + case 'Subrs': + var num = this.readInt(); + this.getToken(); // read in 'array' + while ((token = this.getToken()) === 'dup') { + var index = this.readInt(); + length = this.readInt(); + this.getToken(); // read in 'RD' or '-|' + data = stream.makeSubStream(stream.pos, length); + lenIV = program.properties.privateData['lenIV']; + encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV); + // Skip past the required space and binary data. + stream.skip(length); + this.nextChar(); + token = this.getToken(); // read in 'NP' or '|' + if (token === 'noaccess') { + this.getToken(); // read in 'put' + } + subrs[index] = encoded; + } + break; + case 'BlueValues': + case 'OtherBlues': + case 'FamilyBlues': + case 'FamilyOtherBlues': + var blueArray = this.readNumberArray(); + // *Blue* values may contain invalid data: disables reading of + // those values when hinting is disabled. + if (blueArray.length > 0 && (blueArray.length % 2) === 0 && + HINTING_ENABLED) { + program.properties.privateData[token] = blueArray; + } + break; + case 'StemSnapH': + case 'StemSnapV': + program.properties.privateData[token] = this.readNumberArray(); + break; + case 'StdHW': + case 'StdVW': + program.properties.privateData[token] = + this.readNumberArray()[0]; + break; + case 'BlueShift': + case 'lenIV': + case 'BlueFuzz': + case 'BlueScale': + case 'LanguageGroup': + case 'ExpansionFactor': + program.properties.privateData[token] = this.readNumber(); + break; + case 'ForceBold': + program.properties.privateData[token] = this.readBoolean(); + break; + } + } + + for (var i = 0; i < charstrings.length; i++) { + glyph = charstrings[i].glyph; + encoded = charstrings[i].encoded; + var charString = new Type1CharString(); + var error = charString.convert(encoded, subrs); + var output = charString.output; + if (error) { + // It seems when FreeType encounters an error while evaluating a glyph + // that it completely ignores the glyph so we'll mimic that behaviour + // here and put an endchar to make the validator happy. + output = [14]; + } + program.charstrings.push({ + glyphName: glyph, + charstring: output, + width: charString.width, + lsb: charString.lsb, + seac: charString.seac + }); + } + + return program; + }, + + extractFontHeader: function Type1Parser_extractFontHeader(properties) { + var token; + while ((token = this.getToken()) !== null) { + if (token !== '/') { + continue; + } + token = this.getToken(); + switch (token) { + case 'FontMatrix': + var matrix = this.readNumberArray(); + properties.fontMatrix = matrix; + break; + case 'Encoding': + var encodingArg = this.getToken(); + var encoding; + if (!/^\d+$/.test(encodingArg)) { + // encoding name is specified + encoding = Encodings[encodingArg]; + } else { + encoding = []; + var size = parseInt(encodingArg, 10) | 0; + this.getToken(); // read in 'array' + + for (var j = 0; j < size; j++) { + token = this.getToken(); + // skipping till first dup or def (e.g. ignoring for statement) + while (token !== 'dup' && token !== 'def') { + token = this.getToken(); + if (token === null) { + return; // invalid header + } + } + if (token === 'def') { + break; // read all array data + } + var index = this.readInt(); + this.getToken(); // read in '/' + var glyph = this.getToken(); + encoding[index] = glyph; + this.getToken(); // read the in 'put' + } + } + properties.builtInEncoding = encoding; + break; + case 'FontBBox': + var fontBBox = this.readNumberArray(); + // adjusting ascent/descent + properties.ascent = fontBBox[3]; + properties.descent = fontBBox[1]; + properties.ascentScaled = true; + break; + } + } + } + }; + + return Type1Parser; +})(); + +/** + * The CFF class takes a Type1 file and wrap it into a + * 'Compact Font Format' which itself embed Type2 charstrings. + */ +var CFFStandardStrings = [ + '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', + 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', + 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', + 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', + 'underscore', 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent', + 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', + 'quotesingle', 'quotedblleft', 'guillemotleft', 'guilsinglleft', + 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl', + 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', + 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', 'questiondown', + 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', + 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash', + 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae', + 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', + 'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', + 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters', + 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', + 'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', + 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave', + 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', + 'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', + 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron', + 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', + 'ccedilla', 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', + 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', + 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', + 'udieresis', 'ugrave', 'yacute', 'ydieresis', 'zcaron', 'exclamsmall', + 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', + 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', + 'onedotenleader', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', + 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', + 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'commasuperior', + 'threequartersemdash', 'periodsuperior', 'questionsmall', 'asuperior', + 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', + 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', + 'tsuperior', 'ff', 'ffi', 'ffl', 'parenleftinferior', 'parenrightinferior', + 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', 'Bsmall', + 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', + 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', + 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', + 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', + 'Tildesmall', 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', + 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall', + 'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior', + 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', 'questiondownsmall', 'oneeighth', + 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', + 'zerosuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', + 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', + 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', + 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior', + 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', + 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall', + 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall', + 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', + 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall', + 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', 'Ocircumflexsmall', + 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', + 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', + 'Thornsmall', 'Ydieresissmall', '001.000', '001.001', '001.002', '001.003', + 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold' +]; + +// Type1Font is also a CIDFontType0. +var Type1Font = function Type1Font(name, file, properties) { + // Some bad generators embed pfb file as is, we have to strip 6-byte headers. + // Also, length1 and length2 might be off by 6 bytes as well. + // http://www.math.ubc.ca/~cass/piscript/type1.pdf + var PFB_HEADER_SIZE = 6; + var headerBlockLength = properties.length1; + var eexecBlockLength = properties.length2; + var pfbHeader = file.peekBytes(PFB_HEADER_SIZE); + var pfbHeaderPresent = pfbHeader[0] === 0x80 && pfbHeader[1] === 0x01; + if (pfbHeaderPresent) { + file.skip(PFB_HEADER_SIZE); + headerBlockLength = (pfbHeader[5] << 24) | (pfbHeader[4] << 16) | + (pfbHeader[3] << 8) | pfbHeader[2]; + } + + // Get the data block containing glyphs and subrs informations + var headerBlock = new Stream(file.getBytes(headerBlockLength)); + var headerBlockParser = new Type1Parser(headerBlock); + headerBlockParser.extractFontHeader(properties); + + if (pfbHeaderPresent) { + pfbHeader = file.getBytes(PFB_HEADER_SIZE); + eexecBlockLength = (pfbHeader[5] << 24) | (pfbHeader[4] << 16) | + (pfbHeader[3] << 8) | pfbHeader[2]; + } + + // Decrypt the data blocks and retrieve it's content + var eexecBlock = new Stream(file.getBytes(eexecBlockLength)); + var eexecBlockParser = new Type1Parser(eexecBlock, true); + var data = eexecBlockParser.extractFontProgram(); + for (var info in data.properties) { + properties[info] = data.properties[info]; + } + + var charstrings = data.charstrings; + var type2Charstrings = this.getType2Charstrings(charstrings); + var subrs = this.getType2Subrs(data.subrs); + + this.charstrings = charstrings; + this.data = this.wrap(name, type2Charstrings, this.charstrings, + subrs, properties); + this.seacs = this.getSeacs(data.charstrings); +}; + +Type1Font.prototype = { + get numGlyphs() { + return this.charstrings.length + 1; + }, + + getCharset: function Type1Font_getCharset() { + var charset = ['.notdef']; + var charstrings = this.charstrings; + for (var glyphId = 0; glyphId < charstrings.length; glyphId++) { + charset.push(charstrings[glyphId].glyphName); + } + return charset; + }, + + getGlyphMapping: function Type1Font_getGlyphMapping(properties) { + var charstrings = this.charstrings; + var glyphNames = ['.notdef'], glyphId; + for (glyphId = 0; glyphId < charstrings.length; glyphId++) { + glyphNames.push(charstrings[glyphId].glyphName); + } + var encoding = properties.builtInEncoding; + if (encoding) { + var builtInEncoding = {}; + for (var charCode in encoding) { + glyphId = glyphNames.indexOf(encoding[charCode]); + if (glyphId >= 0) { + builtInEncoding[charCode] = glyphId; + } + } + } + + return type1FontGlyphMapping(properties, builtInEncoding, glyphNames); + }, + + getSeacs: function Type1Font_getSeacs(charstrings) { + var i, ii; + var seacMap = []; + for (i = 0, ii = charstrings.length; i < ii; i++) { + var charstring = charstrings[i]; + if (charstring.seac) { + // Offset by 1 for .notdef + seacMap[i + 1] = charstring.seac; + } + } + return seacMap; + }, + + getType2Charstrings: function Type1Font_getType2Charstrings( + type1Charstrings) { + var type2Charstrings = []; + for (var i = 0, ii = type1Charstrings.length; i < ii; i++) { + type2Charstrings.push(type1Charstrings[i].charstring); + } + return type2Charstrings; + }, + + getType2Subrs: function Type1Font_getType2Subrs(type1Subrs) { + var bias = 0; + var count = type1Subrs.length; + if (count < 1133) { + bias = 107; + } else if (count < 33769) { + bias = 1131; + } else { + bias = 32768; + } + + // Add a bunch of empty subrs to deal with the Type2 bias + var type2Subrs = []; + var i; + for (i = 0; i < bias; i++) { + type2Subrs.push([0x0B]); + } + + for (i = 0; i < count; i++) { + type2Subrs.push(type1Subrs[i]); + } + + return type2Subrs; + }, + + wrap: function Type1Font_wrap(name, glyphs, charstrings, subrs, properties) { + var cff = new CFF(); + cff.header = new CFFHeader(1, 0, 4, 4); + + cff.names = [name]; + + var topDict = new CFFTopDict(); + // CFF strings IDs 0...390 are predefined names, so refering + // to entries in our own String INDEX starts at SID 391. + topDict.setByName('version', 391); + topDict.setByName('Notice', 392); + topDict.setByName('FullName', 393); + topDict.setByName('FamilyName', 394); + topDict.setByName('Weight', 395); + topDict.setByName('Encoding', null); // placeholder + topDict.setByName('FontMatrix', properties.fontMatrix); + topDict.setByName('FontBBox', properties.bbox); + topDict.setByName('charset', null); // placeholder + topDict.setByName('CharStrings', null); // placeholder + topDict.setByName('Private', null); // placeholder + cff.topDict = topDict; + + var strings = new CFFStrings(); + strings.add('Version 0.11'); // Version + strings.add('See original notice'); // Notice + strings.add(name); // FullName + strings.add(name); // FamilyName + strings.add('Medium'); // Weight + cff.strings = strings; + + cff.globalSubrIndex = new CFFIndex(); + + var count = glyphs.length; + var charsetArray = [0]; + var i, ii; + for (i = 0; i < count; i++) { + var index = CFFStandardStrings.indexOf(charstrings[i].glyphName); + // TODO: Insert the string and correctly map it. Previously it was + // thought mapping names that aren't in the standard strings to .notdef + // was fine, however in issue818 when mapping them all to .notdef the + // adieresis glyph no longer worked. + if (index === -1) { + index = 0; + } + charsetArray.push((index >> 8) & 0xff, index & 0xff); + } + cff.charset = new CFFCharset(false, 0, [], charsetArray); + + var charStringsIndex = new CFFIndex(); + charStringsIndex.add([0x8B, 0x0E]); // .notdef + for (i = 0; i < count; i++) { + charStringsIndex.add(glyphs[i]); + } + cff.charStrings = charStringsIndex; + + var privateDict = new CFFPrivateDict(); + privateDict.setByName('Subrs', null); // placeholder + var fields = [ + 'BlueValues', + 'OtherBlues', + 'FamilyBlues', + 'FamilyOtherBlues', + 'StemSnapH', + 'StemSnapV', + 'BlueShift', + 'BlueFuzz', + 'BlueScale', + 'LanguageGroup', + 'ExpansionFactor', + 'ForceBold', + 'StdHW', + 'StdVW' + ]; + for (i = 0, ii = fields.length; i < ii; i++) { + var field = fields[i]; + if (!properties.privateData.hasOwnProperty(field)) { + continue; + } + var value = properties.privateData[field]; + if (isArray(value)) { + // All of the private dictionary array data in CFF must be stored as + // "delta-encoded" numbers. + for (var j = value.length - 1; j > 0; j--) { + value[j] -= value[j - 1]; // ... difference from previous value + } + } + privateDict.setByName(field, value); + } + cff.topDict.privateDict = privateDict; + + var subrIndex = new CFFIndex(); + for (i = 0, ii = subrs.length; i < ii; i++) { + subrIndex.add(subrs[i]); + } + privateDict.subrsIndex = subrIndex; + + var compiler = new CFFCompiler(cff); + return compiler.compile(); + } +}; + +var CFFFont = (function CFFFontClosure() { + function CFFFont(file, properties) { + this.properties = properties; + + var parser = new CFFParser(file, properties); + this.cff = parser.parse(); + var compiler = new CFFCompiler(this.cff); + this.seacs = this.cff.seacs; + try { + this.data = compiler.compile(); + } catch (e) { + warn('Failed to compile font ' + properties.loadedName); + // There may have just been an issue with the compiler, set the data + // anyway and hope the font loaded. + this.data = file; + } + } + + CFFFont.prototype = { + get numGlyphs() { + return this.cff.charStrings.count; + }, + getCharset: function CFFFont_getCharset() { + return this.cff.charset.charset; + }, + getGlyphMapping: function CFFFont_getGlyphMapping() { + var cff = this.cff; + var properties = this.properties; + var charsets = cff.charset.charset; + var charCodeToGlyphId; + var glyphId; + + if (properties.composite) { + charCodeToGlyphId = Object.create(null); + if (cff.isCIDFont) { + // If the font is actually a CID font then we should use the charset + // to map CIDs to GIDs. + for (glyphId = 0; glyphId < charsets.length; glyphId++) { + var cid = charsets[glyphId]; + var charCode = properties.cMap.charCodeOf(cid); + charCodeToGlyphId[charCode] = glyphId; + } + } else { + // If it is NOT actually a CID font then CIDs should be mapped + // directly to GIDs. + for (glyphId = 0; glyphId < cff.charStrings.count; glyphId++) { + charCodeToGlyphId[glyphId] = glyphId; + } + } + return charCodeToGlyphId; + } + + var encoding = cff.encoding ? cff.encoding.encoding : null; + charCodeToGlyphId = type1FontGlyphMapping(properties, encoding, charsets); + return charCodeToGlyphId; + } + }; + + return CFFFont; +})(); + +var CFFParser = (function CFFParserClosure() { + var CharstringValidationData = [ + null, + { id: 'hstem', min: 2, stackClearing: true, stem: true }, + null, + { id: 'vstem', min: 2, stackClearing: true, stem: true }, + { id: 'vmoveto', min: 1, stackClearing: true }, + { id: 'rlineto', min: 2, resetStack: true }, + { id: 'hlineto', min: 1, resetStack: true }, + { id: 'vlineto', min: 1, resetStack: true }, + { id: 'rrcurveto', min: 6, resetStack: true }, + null, + { id: 'callsubr', min: 1, undefStack: true }, + { id: 'return', min: 0, undefStack: true }, + null, // 12 + null, + { id: 'endchar', min: 0, stackClearing: true }, + null, + null, + null, + { id: 'hstemhm', min: 2, stackClearing: true, stem: true }, + { id: 'hintmask', min: 0, stackClearing: true }, + { id: 'cntrmask', min: 0, stackClearing: true }, + { id: 'rmoveto', min: 2, stackClearing: true }, + { id: 'hmoveto', min: 1, stackClearing: true }, + { id: 'vstemhm', min: 2, stackClearing: true, stem: true }, + { id: 'rcurveline', min: 8, resetStack: true }, + { id: 'rlinecurve', min: 8, resetStack: true }, + { id: 'vvcurveto', min: 4, resetStack: true }, + { id: 'hhcurveto', min: 4, resetStack: true }, + null, // shortint + { id: 'callgsubr', min: 1, undefStack: true }, + { id: 'vhcurveto', min: 4, resetStack: true }, + { id: 'hvcurveto', min: 4, resetStack: true } + ]; + var CharstringValidationData12 = [ + null, + null, + null, + { id: 'and', min: 2, stackDelta: -1 }, + { id: 'or', min: 2, stackDelta: -1 }, + { id: 'not', min: 1, stackDelta: 0 }, + null, + null, + null, + { id: 'abs', min: 1, stackDelta: 0 }, + { id: 'add', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] + stack[index - 1]; + } + }, + { id: 'sub', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] - stack[index - 1]; + } + }, + { id: 'div', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] / stack[index - 1]; + } + }, + null, + { id: 'neg', min: 1, stackDelta: 0, + stackFn: function stack_div(stack, index) { + stack[index - 1] = -stack[index - 1]; + } + }, + { id: 'eq', min: 2, stackDelta: -1 }, + null, + null, + { id: 'drop', min: 1, stackDelta: -1 }, + null, + { id: 'put', min: 2, stackDelta: -2 }, + { id: 'get', min: 1, stackDelta: 0 }, + { id: 'ifelse', min: 4, stackDelta: -3 }, + { id: 'random', min: 0, stackDelta: 1 }, + { id: 'mul', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] * stack[index - 1]; + } + }, + null, + { id: 'sqrt', min: 1, stackDelta: 0 }, + { id: 'dup', min: 1, stackDelta: 1 }, + { id: 'exch', min: 2, stackDelta: 0 }, + { id: 'index', min: 2, stackDelta: 0 }, + { id: 'roll', min: 3, stackDelta: -2 }, + null, + null, + null, + { id: 'hflex', min: 7, resetStack: true }, + { id: 'flex', min: 13, resetStack: true }, + { id: 'hflex1', min: 9, resetStack: true }, + { id: 'flex1', min: 11, resetStack: true } + ]; + + function CFFParser(file, properties) { + this.bytes = file.getBytes(); + this.properties = properties; + } + CFFParser.prototype = { + parse: function CFFParser_parse() { + var properties = this.properties; + var cff = new CFF(); + this.cff = cff; + + // The first five sections must be in order, all the others are reached + // via offsets contained in one of the below. + var header = this.parseHeader(); + var nameIndex = this.parseIndex(header.endPos); + var topDictIndex = this.parseIndex(nameIndex.endPos); + var stringIndex = this.parseIndex(topDictIndex.endPos); + var globalSubrIndex = this.parseIndex(stringIndex.endPos); + + var topDictParsed = this.parseDict(topDictIndex.obj.get(0)); + var topDict = this.createDict(CFFTopDict, topDictParsed, cff.strings); + + cff.header = header.obj; + cff.names = this.parseNameIndex(nameIndex.obj); + cff.strings = this.parseStringIndex(stringIndex.obj); + cff.topDict = topDict; + cff.globalSubrIndex = globalSubrIndex.obj; + + this.parsePrivateDict(cff.topDict); + + cff.isCIDFont = topDict.hasName('ROS'); + + var charStringOffset = topDict.getByName('CharStrings'); + var charStringsAndSeacs = this.parseCharStrings(charStringOffset); + cff.charStrings = charStringsAndSeacs.charStrings; + cff.seacs = charStringsAndSeacs.seacs; + cff.widths = charStringsAndSeacs.widths; + + var fontMatrix = topDict.getByName('FontMatrix'); + if (fontMatrix) { + properties.fontMatrix = fontMatrix; + } + + var fontBBox = topDict.getByName('FontBBox'); + if (fontBBox) { + // adjusting ascent/descent + properties.ascent = fontBBox[3]; + properties.descent = fontBBox[1]; + properties.ascentScaled = true; + } + + var charset, encoding; + if (cff.isCIDFont) { + var fdArrayIndex = this.parseIndex(topDict.getByName('FDArray')).obj; + for (var i = 0, ii = fdArrayIndex.count; i < ii; ++i) { + var dictRaw = fdArrayIndex.get(i); + var fontDict = this.createDict(CFFTopDict, this.parseDict(dictRaw), + cff.strings); + this.parsePrivateDict(fontDict); + cff.fdArray.push(fontDict); + } + // cid fonts don't have an encoding + encoding = null; + charset = this.parseCharsets(topDict.getByName('charset'), + cff.charStrings.count, cff.strings, true); + cff.fdSelect = this.parseFDSelect(topDict.getByName('FDSelect'), + cff.charStrings.count); + } else { + charset = this.parseCharsets(topDict.getByName('charset'), + cff.charStrings.count, cff.strings, false); + encoding = this.parseEncoding(topDict.getByName('Encoding'), + properties, + cff.strings, charset.charset); + } + cff.charset = charset; + cff.encoding = encoding; + + return cff; + }, + parseHeader: function CFFParser_parseHeader() { + var bytes = this.bytes; + var bytesLength = bytes.length; + var offset = 0; + + // Prevent an infinite loop, by checking that the offset is within the + // bounds of the bytes array. Necessary in empty, or invalid, font files. + while (offset < bytesLength && bytes[offset] !== 1) { + ++offset; + } + if (offset >= bytesLength) { + error('Invalid CFF header'); + } else if (offset !== 0) { + info('cff data is shifted'); + bytes = bytes.subarray(offset); + this.bytes = bytes; + } + var major = bytes[0]; + var minor = bytes[1]; + var hdrSize = bytes[2]; + var offSize = bytes[3]; + var header = new CFFHeader(major, minor, hdrSize, offSize); + return { obj: header, endPos: hdrSize }; + }, + parseDict: function CFFParser_parseDict(dict) { + var pos = 0; + + function parseOperand() { + var value = dict[pos++]; + if (value === 30) { + return parseFloatOperand(pos); + } else if (value === 28) { + value = dict[pos++]; + value = ((value << 24) | (dict[pos++] << 16)) >> 16; + return value; + } else if (value === 29) { + value = dict[pos++]; + value = (value << 8) | dict[pos++]; + value = (value << 8) | dict[pos++]; + value = (value << 8) | dict[pos++]; + return value; + } else if (value >= 32 && value <= 246) { + return value - 139; + } else if (value >= 247 && value <= 250) { + return ((value - 247) * 256) + dict[pos++] + 108; + } else if (value >= 251 && value <= 254) { + return -((value - 251) * 256) - dict[pos++] - 108; + } else { + error('255 is not a valid DICT command'); + } + return -1; + } + + function parseFloatOperand() { + var str = ''; + var eof = 15; + var lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8', + '9', '.', 'E', 'E-', null, '-']; + var length = dict.length; + while (pos < length) { + var b = dict[pos++]; + var b1 = b >> 4; + var b2 = b & 15; + + if (b1 === eof) { + break; + } + str += lookup[b1]; + + if (b2 === eof) { + break; + } + str += lookup[b2]; + } + return parseFloat(str); + } + + var operands = []; + var entries = []; + + pos = 0; + var end = dict.length; + while (pos < end) { + var b = dict[pos]; + if (b <= 21) { + if (b === 12) { + b = (b << 8) | dict[++pos]; + } + entries.push([b, operands]); + operands = []; + ++pos; + } else { + operands.push(parseOperand()); + } + } + return entries; + }, + parseIndex: function CFFParser_parseIndex(pos) { + var cffIndex = new CFFIndex(); + var bytes = this.bytes; + var count = (bytes[pos++] << 8) | bytes[pos++]; + var offsets = []; + var end = pos; + var i, ii; + + if (count !== 0) { + var offsetSize = bytes[pos++]; + // add 1 for offset to determine size of last object + var startPos = pos + ((count + 1) * offsetSize) - 1; + + for (i = 0, ii = count + 1; i < ii; ++i) { + var offset = 0; + for (var j = 0; j < offsetSize; ++j) { + offset <<= 8; + offset += bytes[pos++]; + } + offsets.push(startPos + offset); + } + end = offsets[count]; + } + for (i = 0, ii = offsets.length - 1; i < ii; ++i) { + var offsetStart = offsets[i]; + var offsetEnd = offsets[i + 1]; + cffIndex.add(bytes.subarray(offsetStart, offsetEnd)); + } + return {obj: cffIndex, endPos: end}; + }, + parseNameIndex: function CFFParser_parseNameIndex(index) { + var names = []; + for (var i = 0, ii = index.count; i < ii; ++i) { + var name = index.get(i); + // OTS doesn't allow names to be over 127 characters. + var length = Math.min(name.length, 127); + var data = []; + // OTS also only permits certain characters in the name. + for (var j = 0; j < length; ++j) { + var c = name[j]; + if (j === 0 && c === 0) { + data[j] = c; + continue; + } + if ((c < 33 || c > 126) || c === 91 /* [ */ || c === 93 /* ] */ || + c === 40 /* ( */ || c === 41 /* ) */ || c === 123 /* { */ || + c === 125 /* } */ || c === 60 /* < */ || c === 62 /* > */ || + c === 47 /* / */ || c === 37 /* % */ || c === 35 /* # */) { + data[j] = 95; + continue; + } + data[j] = c; + } + names.push(bytesToString(data)); + } + return names; + }, + parseStringIndex: function CFFParser_parseStringIndex(index) { + var strings = new CFFStrings(); + for (var i = 0, ii = index.count; i < ii; ++i) { + var data = index.get(i); + strings.add(bytesToString(data)); + } + return strings; + }, + createDict: function CFFParser_createDict(Type, dict, strings) { + var cffDict = new Type(strings); + for (var i = 0, ii = dict.length; i < ii; ++i) { + var pair = dict[i]; + var key = pair[0]; + var value = pair[1]; + cffDict.setByKey(key, value); + } + return cffDict; + }, + parseCharStrings: function CFFParser_parseCharStrings(charStringOffset) { + var charStrings = this.parseIndex(charStringOffset).obj; + var seacs = []; + var widths = []; + var count = charStrings.count; + for (var i = 0; i < count; i++) { + var charstring = charStrings.get(i); + + var stackSize = 0; + var stack = []; + var undefStack = true; + var hints = 0; + var valid = true; + var data = charstring; + var length = data.length; + var firstStackClearing = true; + for (var j = 0; j < length;) { + var value = data[j++]; + var validationCommand = null; + if (value === 12) { + var q = data[j++]; + if (q === 0) { + // The CFF specification state that the 'dotsection' command + // (12, 0) is deprecated and treated as a no-op, but all Type2 + // charstrings processors should support them. Unfortunately + // the font sanitizer don't. As a workaround the sequence (12, 0) + // is replaced by a useless (0, hmoveto). + data[j - 2] = 139; + data[j - 1] = 22; + stackSize = 0; + } else { + validationCommand = CharstringValidationData12[q]; + } + } else if (value === 28) { // number (16 bit) + stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16)) >> 16; + j += 2; + stackSize++; + } else if (value === 14) { + if (stackSize >= 4) { + stackSize -= 4; + if (SEAC_ANALYSIS_ENABLED) { + seacs[i] = stack.slice(stackSize, stackSize + 4); + valid = false; + } + } + validationCommand = CharstringValidationData[value]; + } else if (value >= 32 && value <= 246) { // number + stack[stackSize] = value - 139; + stackSize++; + } else if (value >= 247 && value <= 254) { // number (+1 bytes) + stack[stackSize] = (value < 251 ? + ((value - 247) << 8) + data[j] + 108 : + -((value - 251) << 8) - data[j] - 108); + j++; + stackSize++; + } else if (value === 255) { // number (32 bit) + stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16) | + (data[j + 2] << 8) | data[j + 3]) / 65536; + j += 4; + stackSize++; + } else if (value === 19 || value === 20) { + hints += stackSize >> 1; + j += (hints + 7) >> 3; // skipping right amount of hints flag data + stackSize %= 2; + validationCommand = CharstringValidationData[value]; + } else { + validationCommand = CharstringValidationData[value]; + } + if (validationCommand) { + if (validationCommand.stem) { + hints += stackSize >> 1; + } + if ('min' in validationCommand) { + if (!undefStack && stackSize < validationCommand.min) { + warn('Not enough parameters for ' + validationCommand.id + + '; actual: ' + stackSize + + ', expected: ' + validationCommand.min); + valid = false; + break; + } + } + if (firstStackClearing && validationCommand.stackClearing) { + firstStackClearing = false; + // the optional character width can be found before the first + // stack-clearing command arguments + stackSize -= validationCommand.min; + if (stackSize >= 2 && validationCommand.stem) { + // there are even amount of arguments for stem commands + stackSize %= 2; + } else if (stackSize > 1) { + warn('Found too many parameters for stack-clearing command'); + } + if (stackSize > 0 && stack[stackSize - 1] >= 0) { + widths[i] = stack[stackSize - 1]; + } + } + if ('stackDelta' in validationCommand) { + if ('stackFn' in validationCommand) { + validationCommand.stackFn(stack, stackSize); + } + stackSize += validationCommand.stackDelta; + } else if (validationCommand.stackClearing) { + stackSize = 0; + } else if (validationCommand.resetStack) { + stackSize = 0; + undefStack = false; + } else if (validationCommand.undefStack) { + stackSize = 0; + undefStack = true; + firstStackClearing = false; + } + } + } + if (!valid) { + // resetting invalid charstring to single 'endchar' + charStrings.set(i, new Uint8Array([14])); + } + } + return { charStrings: charStrings, seacs: seacs, widths: widths }; + }, + emptyPrivateDictionary: + function CFFParser_emptyPrivateDictionary(parentDict) { + var privateDict = this.createDict(CFFPrivateDict, [], + parentDict.strings); + parentDict.setByKey(18, [0, 0]); + parentDict.privateDict = privateDict; + }, + parsePrivateDict: function CFFParser_parsePrivateDict(parentDict) { + // no private dict, do nothing + if (!parentDict.hasName('Private')) { + this.emptyPrivateDictionary(parentDict); + return; + } + var privateOffset = parentDict.getByName('Private'); + // make sure the params are formatted correctly + if (!isArray(privateOffset) || privateOffset.length !== 2) { + parentDict.removeByName('Private'); + return; + } + var size = privateOffset[0]; + var offset = privateOffset[1]; + // remove empty dicts or ones that refer to invalid location + if (size === 0 || offset >= this.bytes.length) { + this.emptyPrivateDictionary(parentDict); + return; + } + + var privateDictEnd = offset + size; + var dictData = this.bytes.subarray(offset, privateDictEnd); + var dict = this.parseDict(dictData); + var privateDict = this.createDict(CFFPrivateDict, dict, + parentDict.strings); + parentDict.privateDict = privateDict; + + // Parse the Subrs index also since it's relative to the private dict. + if (!privateDict.getByName('Subrs')) { + return; + } + var subrsOffset = privateDict.getByName('Subrs'); + var relativeOffset = offset + subrsOffset; + // Validate the offset. + if (subrsOffset === 0 || relativeOffset >= this.bytes.length) { + this.emptyPrivateDictionary(parentDict); + return; + } + var subrsIndex = this.parseIndex(relativeOffset); + privateDict.subrsIndex = subrsIndex.obj; + }, + parseCharsets: function CFFParser_parseCharsets(pos, length, strings, cid) { + if (pos === 0) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.ISO_ADOBE, + ISOAdobeCharset); + } else if (pos === 1) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT, + ExpertCharset); + } else if (pos === 2) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT_SUBSET, + ExpertSubsetCharset); + } + + var bytes = this.bytes; + var start = pos; + var format = bytes[pos++]; + var charset = ['.notdef']; + var id, count, i; + + // subtract 1 for the .notdef glyph + length -= 1; + + switch (format) { + case 0: + for (i = 0; i < length; i++) { + id = (bytes[pos++] << 8) | bytes[pos++]; + charset.push(cid ? id : strings.get(id)); + } + break; + case 1: + while (charset.length <= length) { + id = (bytes[pos++] << 8) | bytes[pos++]; + count = bytes[pos++]; + for (i = 0; i <= count; i++) { + charset.push(cid ? id++ : strings.get(id++)); + } + } + break; + case 2: + while (charset.length <= length) { + id = (bytes[pos++] << 8) | bytes[pos++]; + count = (bytes[pos++] << 8) | bytes[pos++]; + for (i = 0; i <= count; i++) { + charset.push(cid ? id++ : strings.get(id++)); + } + } + break; + default: + error('Unknown charset format'); + } + // Raw won't be needed if we actually compile the charset. + var end = pos; + var raw = bytes.subarray(start, end); + + return new CFFCharset(false, format, charset, raw); + }, + parseEncoding: function CFFParser_parseEncoding(pos, + properties, + strings, + charset) { + var encoding = {}; + var bytes = this.bytes; + var predefined = false; + var hasSupplement = false; + var format, i, ii; + var raw = null; + + function readSupplement() { + var supplementsCount = bytes[pos++]; + for (i = 0; i < supplementsCount; i++) { + var code = bytes[pos++]; + var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff); + encoding[code] = charset.indexOf(strings.get(sid)); + } + } + + if (pos === 0 || pos === 1) { + predefined = true; + format = pos; + var baseEncoding = pos ? Encodings.ExpertEncoding : + Encodings.StandardEncoding; + for (i = 0, ii = charset.length; i < ii; i++) { + var index = baseEncoding.indexOf(charset[i]); + if (index !== -1) { + encoding[index] = i; + } + } + } else { + var dataStart = pos; + format = bytes[pos++]; + switch (format & 0x7f) { + case 0: + var glyphsCount = bytes[pos++]; + for (i = 1; i <= glyphsCount; i++) { + encoding[bytes[pos++]] = i; + } + break; + + case 1: + var rangesCount = bytes[pos++]; + var gid = 1; + for (i = 0; i < rangesCount; i++) { + var start = bytes[pos++]; + var left = bytes[pos++]; + for (var j = start; j <= start + left; j++) { + encoding[j] = gid++; + } + } + break; + + default: + error('Unknow encoding format: ' + format + ' in CFF'); + break; + } + var dataEnd = pos; + if (format & 0x80) { + // The font sanitizer does not support CFF encoding with a + // supplement, since the encoding is not really used to map + // between gid to glyph, let's overwrite what is declared in + // the top dictionary to let the sanitizer think the font use + // StandardEncoding, that's a lie but that's ok. + bytes[dataStart] &= 0x7f; + readSupplement(); + hasSupplement = true; + } + raw = bytes.subarray(dataStart, dataEnd); + } + format = format & 0x7f; + return new CFFEncoding(predefined, format, encoding, raw); + }, + parseFDSelect: function CFFParser_parseFDSelect(pos, length) { + var start = pos; + var bytes = this.bytes; + var format = bytes[pos++]; + var fdSelect = []; + var i; + + switch (format) { + case 0: + for (i = 0; i < length; ++i) { + var id = bytes[pos++]; + fdSelect.push(id); + } + break; + case 3: + var rangesCount = (bytes[pos++] << 8) | bytes[pos++]; + for (i = 0; i < rangesCount; ++i) { + var first = (bytes[pos++] << 8) | bytes[pos++]; + var fdIndex = bytes[pos++]; + var next = (bytes[pos] << 8) | bytes[pos + 1]; + for (var j = first; j < next; ++j) { + fdSelect.push(fdIndex); + } + } + // Advance past the sentinel(next). + pos += 2; + break; + default: + error('Unknown fdselect format ' + format); + break; + } + var end = pos; + return new CFFFDSelect(fdSelect, bytes.subarray(start, end)); + } + }; + return CFFParser; +})(); + +// Compact Font Format +var CFF = (function CFFClosure() { + function CFF() { + this.header = null; + this.names = []; + this.topDict = null; + this.strings = new CFFStrings(); + this.globalSubrIndex = null; + + // The following could really be per font, but since we only have one font + // store them here. + this.encoding = null; + this.charset = null; + this.charStrings = null; + this.fdArray = []; + this.fdSelect = null; + + this.isCIDFont = false; + } + return CFF; +})(); + +var CFFHeader = (function CFFHeaderClosure() { + function CFFHeader(major, minor, hdrSize, offSize) { + this.major = major; + this.minor = minor; + this.hdrSize = hdrSize; + this.offSize = offSize; + } + return CFFHeader; +})(); + +var CFFStrings = (function CFFStringsClosure() { + function CFFStrings() { + this.strings = []; + } + CFFStrings.prototype = { + get: function CFFStrings_get(index) { + if (index >= 0 && index <= 390) { + return CFFStandardStrings[index]; + } + if (index - 391 <= this.strings.length) { + return this.strings[index - 391]; + } + return CFFStandardStrings[0]; + }, + add: function CFFStrings_add(value) { + this.strings.push(value); + }, + get count() { + return this.strings.length; + } + }; + return CFFStrings; +})(); + +var CFFIndex = (function CFFIndexClosure() { + function CFFIndex() { + this.objects = []; + this.length = 0; + } + CFFIndex.prototype = { + add: function CFFIndex_add(data) { + this.length += data.length; + this.objects.push(data); + }, + set: function CFFIndex_set(index, data) { + this.length += data.length - this.objects[index].length; + this.objects[index] = data; + }, + get: function CFFIndex_get(index) { + return this.objects[index]; + }, + get count() { + return this.objects.length; + } + }; + return CFFIndex; +})(); + +var CFFDict = (function CFFDictClosure() { + function CFFDict(tables, strings) { + this.keyToNameMap = tables.keyToNameMap; + this.nameToKeyMap = tables.nameToKeyMap; + this.defaults = tables.defaults; + this.types = tables.types; + this.opcodes = tables.opcodes; + this.order = tables.order; + this.strings = strings; + this.values = {}; + } + CFFDict.prototype = { + // value should always be an array + setByKey: function CFFDict_setByKey(key, value) { + if (!(key in this.keyToNameMap)) { + return false; + } + // ignore empty values + if (value.length === 0) { + return true; + } + var type = this.types[key]; + // remove the array wrapping these types of values + if (type === 'num' || type === 'sid' || type === 'offset') { + value = value[0]; + } + this.values[key] = value; + return true; + }, + setByName: function CFFDict_setByName(name, value) { + if (!(name in this.nameToKeyMap)) { + error('Invalid dictionary name "' + name + '"'); + } + this.values[this.nameToKeyMap[name]] = value; + }, + hasName: function CFFDict_hasName(name) { + return this.nameToKeyMap[name] in this.values; + }, + getByName: function CFFDict_getByName(name) { + if (!(name in this.nameToKeyMap)) { + error('Invalid dictionary name "' + name + '"'); + } + var key = this.nameToKeyMap[name]; + if (!(key in this.values)) { + return this.defaults[key]; + } + return this.values[key]; + }, + removeByName: function CFFDict_removeByName(name) { + delete this.values[this.nameToKeyMap[name]]; + } + }; + CFFDict.createTables = function CFFDict_createTables(layout) { + var tables = { + keyToNameMap: {}, + nameToKeyMap: {}, + defaults: {}, + types: {}, + opcodes: {}, + order: [] + }; + for (var i = 0, ii = layout.length; i < ii; ++i) { + var entry = layout[i]; + var key = isArray(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0]; + tables.keyToNameMap[key] = entry[1]; + tables.nameToKeyMap[entry[1]] = key; + tables.types[key] = entry[2]; + tables.defaults[key] = entry[3]; + tables.opcodes[key] = isArray(entry[0]) ? entry[0] : [entry[0]]; + tables.order.push(key); + } + return tables; + }; + return CFFDict; +})(); + +var CFFTopDict = (function CFFTopDictClosure() { + var layout = [ + [[12, 30], 'ROS', ['sid', 'sid', 'num'], null], + [[12, 20], 'SyntheticBase', 'num', null], + [0, 'version', 'sid', null], + [1, 'Notice', 'sid', null], + [[12, 0], 'Copyright', 'sid', null], + [2, 'FullName', 'sid', null], + [3, 'FamilyName', 'sid', null], + [4, 'Weight', 'sid', null], + [[12, 1], 'isFixedPitch', 'num', 0], + [[12, 2], 'ItalicAngle', 'num', 0], + [[12, 3], 'UnderlinePosition', 'num', -100], + [[12, 4], 'UnderlineThickness', 'num', 50], + [[12, 5], 'PaintType', 'num', 0], + [[12, 6], 'CharstringType', 'num', 2], + [[12, 7], 'FontMatrix', ['num', 'num', 'num', 'num', 'num', 'num'], + [0.001, 0, 0, 0.001, 0, 0]], + [13, 'UniqueID', 'num', null], + [5, 'FontBBox', ['num', 'num', 'num', 'num'], [0, 0, 0, 0]], + [[12, 8], 'StrokeWidth', 'num', 0], + [14, 'XUID', 'array', null], + [15, 'charset', 'offset', 0], + [16, 'Encoding', 'offset', 0], + [17, 'CharStrings', 'offset', 0], + [18, 'Private', ['offset', 'offset'], null], + [[12, 21], 'PostScript', 'sid', null], + [[12, 22], 'BaseFontName', 'sid', null], + [[12, 23], 'BaseFontBlend', 'delta', null], + [[12, 31], 'CIDFontVersion', 'num', 0], + [[12, 32], 'CIDFontRevision', 'num', 0], + [[12, 33], 'CIDFontType', 'num', 0], + [[12, 34], 'CIDCount', 'num', 8720], + [[12, 35], 'UIDBase', 'num', null], + // XXX: CID Fonts on DirectWrite 6.1 only seem to work if FDSelect comes + // before FDArray. + [[12, 37], 'FDSelect', 'offset', null], + [[12, 36], 'FDArray', 'offset', null], + [[12, 38], 'FontName', 'sid', null] + ]; + var tables = null; + function CFFTopDict(strings) { + if (tables === null) { + tables = CFFDict.createTables(layout); + } + CFFDict.call(this, tables, strings); + this.privateDict = null; + } + CFFTopDict.prototype = Object.create(CFFDict.prototype); + return CFFTopDict; +})(); + +var CFFPrivateDict = (function CFFPrivateDictClosure() { + var layout = [ + [6, 'BlueValues', 'delta', null], + [7, 'OtherBlues', 'delta', null], + [8, 'FamilyBlues', 'delta', null], + [9, 'FamilyOtherBlues', 'delta', null], + [[12, 9], 'BlueScale', 'num', 0.039625], + [[12, 10], 'BlueShift', 'num', 7], + [[12, 11], 'BlueFuzz', 'num', 1], + [10, 'StdHW', 'num', null], + [11, 'StdVW', 'num', null], + [[12, 12], 'StemSnapH', 'delta', null], + [[12, 13], 'StemSnapV', 'delta', null], + [[12, 14], 'ForceBold', 'num', 0], + [[12, 17], 'LanguageGroup', 'num', 0], + [[12, 18], 'ExpansionFactor', 'num', 0.06], + [[12, 19], 'initialRandomSeed', 'num', 0], + [20, 'defaultWidthX', 'num', 0], + [21, 'nominalWidthX', 'num', 0], + [19, 'Subrs', 'offset', null] + ]; + var tables = null; + function CFFPrivateDict(strings) { + if (tables === null) { + tables = CFFDict.createTables(layout); + } + CFFDict.call(this, tables, strings); + this.subrsIndex = null; + } + CFFPrivateDict.prototype = Object.create(CFFDict.prototype); + return CFFPrivateDict; +})(); + +var CFFCharsetPredefinedTypes = { + ISO_ADOBE: 0, + EXPERT: 1, + EXPERT_SUBSET: 2 +}; +var CFFCharset = (function CFFCharsetClosure() { + function CFFCharset(predefined, format, charset, raw) { + this.predefined = predefined; + this.format = format; + this.charset = charset; + this.raw = raw; + } + return CFFCharset; +})(); + +var CFFEncoding = (function CFFEncodingClosure() { + function CFFEncoding(predefined, format, encoding, raw) { + this.predefined = predefined; + this.format = format; + this.encoding = encoding; + this.raw = raw; + } + return CFFEncoding; +})(); + +var CFFFDSelect = (function CFFFDSelectClosure() { + function CFFFDSelect(fdSelect, raw) { + this.fdSelect = fdSelect; + this.raw = raw; + } + return CFFFDSelect; +})(); + +// Helper class to keep track of where an offset is within the data and helps +// filling in that offset once it's known. +var CFFOffsetTracker = (function CFFOffsetTrackerClosure() { + function CFFOffsetTracker() { + this.offsets = {}; + } + CFFOffsetTracker.prototype = { + isTracking: function CFFOffsetTracker_isTracking(key) { + return key in this.offsets; + }, + track: function CFFOffsetTracker_track(key, location) { + if (key in this.offsets) { + error('Already tracking location of ' + key); + } + this.offsets[key] = location; + }, + offset: function CFFOffsetTracker_offset(value) { + for (var key in this.offsets) { + this.offsets[key] += value; + } + }, + setEntryLocation: function CFFOffsetTracker_setEntryLocation(key, + values, + output) { + if (!(key in this.offsets)) { + error('Not tracking location of ' + key); + } + var data = output.data; + var dataOffset = this.offsets[key]; + var size = 5; + for (var i = 0, ii = values.length; i < ii; ++i) { + var offset0 = i * size + dataOffset; + var offset1 = offset0 + 1; + var offset2 = offset0 + 2; + var offset3 = offset0 + 3; + var offset4 = offset0 + 4; + // It's easy to screw up offsets so perform this sanity check. + if (data[offset0] !== 0x1d || data[offset1] !== 0 || + data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) { + error('writing to an offset that is not empty'); + } + var value = values[i]; + data[offset0] = 0x1d; + data[offset1] = (value >> 24) & 0xFF; + data[offset2] = (value >> 16) & 0xFF; + data[offset3] = (value >> 8) & 0xFF; + data[offset4] = value & 0xFF; + } + } + }; + return CFFOffsetTracker; +})(); + +// Takes a CFF and converts it to the binary representation. +var CFFCompiler = (function CFFCompilerClosure() { + function CFFCompiler(cff) { + this.cff = cff; + } + CFFCompiler.prototype = { + compile: function CFFCompiler_compile() { + var cff = this.cff; + var output = { + data: [], + length: 0, + add: function CFFCompiler_add(data) { + this.data = this.data.concat(data); + this.length = this.data.length; + } + }; + + // Compile the five entries that must be in order. + var header = this.compileHeader(cff.header); + output.add(header); + + var nameIndex = this.compileNameIndex(cff.names); + output.add(nameIndex); + + if (cff.isCIDFont) { + // The spec is unclear on how font matrices should relate to each other + // when there is one in the main top dict and the sub top dicts. + // Windows handles this differently than linux and osx so we have to + // normalize to work on all. + // Rules based off of some mailing list discussions: + // - If main font has a matrix and subfont doesn't, use the main matrix. + // - If no main font matrix and there is a subfont matrix, use the + // subfont matrix. + // - If both have matrices, concat together. + // - If neither have matrices, use default. + // To make this work on all platforms we move the top matrix into each + // sub top dict and concat if necessary. + if (cff.topDict.hasName('FontMatrix')) { + var base = cff.topDict.getByName('FontMatrix'); + cff.topDict.removeByName('FontMatrix'); + for (var i = 0, ii = cff.fdArray.length; i < ii; i++) { + var subDict = cff.fdArray[i]; + var matrix = base.slice(0); + if (subDict.hasName('FontMatrix')) { + matrix = Util.transform(matrix, subDict.getByName('FontMatrix')); + } + subDict.setByName('FontMatrix', matrix); + } + } + } + + var compiled = this.compileTopDicts([cff.topDict], + output.length, + cff.isCIDFont); + output.add(compiled.output); + var topDictTracker = compiled.trackers[0]; + + var stringIndex = this.compileStringIndex(cff.strings.strings); + output.add(stringIndex); + + var globalSubrIndex = this.compileIndex(cff.globalSubrIndex); + output.add(globalSubrIndex); + + // Now start on the other entries that have no specfic order. + if (cff.encoding && cff.topDict.hasName('Encoding')) { + if (cff.encoding.predefined) { + topDictTracker.setEntryLocation('Encoding', [cff.encoding.format], + output); + } else { + var encoding = this.compileEncoding(cff.encoding); + topDictTracker.setEntryLocation('Encoding', [output.length], output); + output.add(encoding); + } + } + + if (cff.charset && cff.topDict.hasName('charset')) { + if (cff.charset.predefined) { + topDictTracker.setEntryLocation('charset', [cff.charset.format], + output); + } else { + var charset = this.compileCharset(cff.charset); + topDictTracker.setEntryLocation('charset', [output.length], output); + output.add(charset); + } + } + + var charStrings = this.compileCharStrings(cff.charStrings); + topDictTracker.setEntryLocation('CharStrings', [output.length], output); + output.add(charStrings); + + if (cff.isCIDFont) { + // For some reason FDSelect must be in front of FDArray on windows. OSX + // and linux don't seem to care. + topDictTracker.setEntryLocation('FDSelect', [output.length], output); + var fdSelect = this.compileFDSelect(cff.fdSelect.raw); + output.add(fdSelect); + // It is unclear if the sub font dictionary can have CID related + // dictionary keys, but the sanitizer doesn't like them so remove them. + compiled = this.compileTopDicts(cff.fdArray, output.length, true); + topDictTracker.setEntryLocation('FDArray', [output.length], output); + output.add(compiled.output); + var fontDictTrackers = compiled.trackers; + + this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output); + } + + this.compilePrivateDicts([cff.topDict], [topDictTracker], output); + + // If the font data ends with INDEX whose object data is zero-length, + // the sanitizer will bail out. Add a dummy byte to avoid that. + output.add([0]); + + return output.data; + }, + encodeNumber: function CFFCompiler_encodeNumber(value) { + if (parseFloat(value) === parseInt(value, 10) && !isNaN(value)) { // isInt + return this.encodeInteger(value); + } else { + return this.encodeFloat(value); + } + }, + encodeFloat: function CFFCompiler_encodeFloat(num) { + var value = num.toString(); + + // rounding inaccurate doubles + var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value); + if (m) { + var epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length)); + value = (Math.round(num * epsilon) / epsilon).toString(); + } + + var nibbles = ''; + var i, ii; + for (i = 0, ii = value.length; i < ii; ++i) { + var a = value[i]; + if (a === 'e') { + nibbles += value[++i] === '-' ? 'c' : 'b'; + } else if (a === '.') { + nibbles += 'a'; + } else if (a === '-') { + nibbles += 'e'; + } else { + nibbles += a; + } + } + nibbles += (nibbles.length & 1) ? 'f' : 'ff'; + var out = [30]; + for (i = 0, ii = nibbles.length; i < ii; i += 2) { + out.push(parseInt(nibbles.substr(i, 2), 16)); + } + return out; + }, + encodeInteger: function CFFCompiler_encodeInteger(value) { + var code; + if (value >= -107 && value <= 107) { + code = [value + 139]; + } else if (value >= 108 && value <= 1131) { + value = [value - 108]; + code = [(value >> 8) + 247, value & 0xFF]; + } else if (value >= -1131 && value <= -108) { + value = -value - 108; + code = [(value >> 8) + 251, value & 0xFF]; + } else if (value >= -32768 && value <= 32767) { + code = [0x1c, (value >> 8) & 0xFF, value & 0xFF]; + } else { + code = [0x1d, + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF]; + } + return code; + }, + compileHeader: function CFFCompiler_compileHeader(header) { + return [ + header.major, + header.minor, + header.hdrSize, + header.offSize + ]; + }, + compileNameIndex: function CFFCompiler_compileNameIndex(names) { + var nameIndex = new CFFIndex(); + for (var i = 0, ii = names.length; i < ii; ++i) { + nameIndex.add(stringToBytes(names[i])); + } + return this.compileIndex(nameIndex); + }, + compileTopDicts: function CFFCompiler_compileTopDicts(dicts, + length, + removeCidKeys) { + var fontDictTrackers = []; + var fdArrayIndex = new CFFIndex(); + for (var i = 0, ii = dicts.length; i < ii; ++i) { + var fontDict = dicts[i]; + if (removeCidKeys) { + fontDict.removeByName('CIDFontVersion'); + fontDict.removeByName('CIDFontRevision'); + fontDict.removeByName('CIDFontType'); + fontDict.removeByName('CIDCount'); + fontDict.removeByName('UIDBase'); + } + var fontDictTracker = new CFFOffsetTracker(); + var fontDictData = this.compileDict(fontDict, fontDictTracker); + fontDictTrackers.push(fontDictTracker); + fdArrayIndex.add(fontDictData); + fontDictTracker.offset(length); + } + fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers); + return { + trackers: fontDictTrackers, + output: fdArrayIndex + }; + }, + compilePrivateDicts: function CFFCompiler_compilePrivateDicts(dicts, + trackers, + output) { + for (var i = 0, ii = dicts.length; i < ii; ++i) { + var fontDict = dicts[i]; + assert(fontDict.privateDict && fontDict.hasName('Private'), + 'There must be an private dictionary.'); + var privateDict = fontDict.privateDict; + var privateDictTracker = new CFFOffsetTracker(); + var privateDictData = this.compileDict(privateDict, privateDictTracker); + + var outputLength = output.length; + privateDictTracker.offset(outputLength); + if (!privateDictData.length) { + // The private dictionary was empty, set the output length to zero to + // ensure the offset length isn't out of bounds in the eyes of the + // sanitizer. + outputLength = 0; + } + + trackers[i].setEntryLocation('Private', + [privateDictData.length, outputLength], + output); + output.add(privateDictData); + + if (privateDict.subrsIndex && privateDict.hasName('Subrs')) { + var subrs = this.compileIndex(privateDict.subrsIndex); + privateDictTracker.setEntryLocation('Subrs', [privateDictData.length], + output); + output.add(subrs); + } + } + }, + compileDict: function CFFCompiler_compileDict(dict, offsetTracker) { + var out = []; + // The dictionary keys must be in a certain order. + var order = dict.order; + for (var i = 0; i < order.length; ++i) { + var key = order[i]; + if (!(key in dict.values)) { + continue; + } + var values = dict.values[key]; + var types = dict.types[key]; + if (!isArray(types)) { + types = [types]; + } + if (!isArray(values)) { + values = [values]; + } + + // Remove any empty dict values. + if (values.length === 0) { + continue; + } + + for (var j = 0, jj = types.length; j < jj; ++j) { + var type = types[j]; + var value = values[j]; + switch (type) { + case 'num': + case 'sid': + out = out.concat(this.encodeNumber(value)); + break; + case 'offset': + // For offsets we just insert a 32bit integer so we don't have to + // deal with figuring out the length of the offset when it gets + // replaced later on by the compiler. + var name = dict.keyToNameMap[key]; + // Some offsets have the offset and the length, so just record the + // position of the first one. + if (!offsetTracker.isTracking(name)) { + offsetTracker.track(name, out.length); + } + out = out.concat([0x1d, 0, 0, 0, 0]); + break; + case 'array': + case 'delta': + out = out.concat(this.encodeNumber(value)); + for (var k = 1, kk = values.length; k < kk; ++k) { + out = out.concat(this.encodeNumber(values[k])); + } + break; + default: + error('Unknown data type of ' + type); + break; + } + } + out = out.concat(dict.opcodes[key]); + } + return out; + }, + compileStringIndex: function CFFCompiler_compileStringIndex(strings) { + var stringIndex = new CFFIndex(); + for (var i = 0, ii = strings.length; i < ii; ++i) { + stringIndex.add(stringToBytes(strings[i])); + } + return this.compileIndex(stringIndex); + }, + compileGlobalSubrIndex: function CFFCompiler_compileGlobalSubrIndex() { + var globalSubrIndex = this.cff.globalSubrIndex; + this.out.writeByteArray(this.compileIndex(globalSubrIndex)); + }, + compileCharStrings: function CFFCompiler_compileCharStrings(charStrings) { + return this.compileIndex(charStrings); + }, + compileCharset: function CFFCompiler_compileCharset(charset) { + return this.compileTypedArray(charset.raw); + }, + compileEncoding: function CFFCompiler_compileEncoding(encoding) { + return this.compileTypedArray(encoding.raw); + }, + compileFDSelect: function CFFCompiler_compileFDSelect(fdSelect) { + return this.compileTypedArray(fdSelect); + }, + compileTypedArray: function CFFCompiler_compileTypedArray(data) { + var out = []; + for (var i = 0, ii = data.length; i < ii; ++i) { + out[i] = data[i]; + } + return out; + }, + compileIndex: function CFFCompiler_compileIndex(index, trackers) { + trackers = trackers || []; + var objects = index.objects; + // First 2 bytes contains the number of objects contained into this index + var count = objects.length; + + // If there is no object, just create an index. This technically + // should just be [0, 0] but OTS has an issue with that. + if (count === 0) { + return [0, 0, 0]; + } + + var data = [(count >> 8) & 0xFF, count & 0xff]; + + var lastOffset = 1, i; + for (i = 0; i < count; ++i) { + lastOffset += objects[i].length; + } + + var offsetSize; + if (lastOffset < 0x100) { + offsetSize = 1; + } else if (lastOffset < 0x10000) { + offsetSize = 2; + } else if (lastOffset < 0x1000000) { + offsetSize = 3; + } else { + offsetSize = 4; + } + + // Next byte contains the offset size use to reference object in the file + data.push(offsetSize); + + // Add another offset after this one because we need a new offset + var relativeOffset = 1; + for (i = 0; i < count + 1; i++) { + if (offsetSize === 1) { + data.push(relativeOffset & 0xFF); + } else if (offsetSize === 2) { + data.push((relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } else if (offsetSize === 3) { + data.push((relativeOffset >> 16) & 0xFF, + (relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } else { + data.push((relativeOffset >>> 24) & 0xFF, + (relativeOffset >> 16) & 0xFF, + (relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } + + if (objects[i]) { + relativeOffset += objects[i].length; + } + } + + for (i = 0; i < count; i++) { + // Notify the tracker where the object will be offset in the data. + if (trackers[i]) { + trackers[i].offset(data.length); + } + for (var j = 0, jj = objects[i].length; j < jj; j++) { + data.push(objects[i][j]); + } + } + return data; + } + }; + return CFFCompiler; +})(); + +// Workaround for seac on Windows. +(function checkSeacSupport() { + if (/Windows/.test(navigator.userAgent)) { + SEAC_ANALYSIS_ENABLED = true; + } +})(); + +// Workaround for Private Use Area characters in Chrome on Windows +// http://code.google.com/p/chromium/issues/detail?id=122465 +// https://github.com/mozilla/pdf.js/issues/1689 +(function checkChromeWindows() { + if (/Windows.*Chrome/.test(navigator.userAgent)) { + SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = true; + } +})(); + + +var FontRendererFactory = (function FontRendererFactoryClosure() { + function getLong(data, offset) { + return (data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]; + } + + function getUshort(data, offset) { + return (data[offset] << 8) | data[offset + 1]; + } + + function parseCmap(data, start, end) { + var offset = (getUshort(data, start + 2) === 1 ? + getLong(data, start + 8) : getLong(data, start + 16)); + var format = getUshort(data, start + offset); + var length, ranges, p, i; + if (format === 4) { + length = getUshort(data, start + offset + 2); + var segCount = getUshort(data, start + offset + 6) >> 1; + p = start + offset + 14; + ranges = []; + for (i = 0; i < segCount; i++, p += 2) { + ranges[i] = {end: getUshort(data, p)}; + } + p += 2; + for (i = 0; i < segCount; i++, p += 2) { + ranges[i].start = getUshort(data, p); + } + for (i = 0; i < segCount; i++, p += 2) { + ranges[i].idDelta = getUshort(data, p); + } + for (i = 0; i < segCount; i++, p += 2) { + var idOffset = getUshort(data, p); + if (idOffset === 0) { + continue; + } + ranges[i].ids = []; + for (var j = 0, jj = ranges[i].end - ranges[i].start + 1; j < jj; j++) { + ranges[i].ids[j] = getUshort(data, p + idOffset); + idOffset += 2; + } + } + return ranges; + } else if (format === 12) { + length = getLong(data, start + offset + 4); + var groups = getLong(data, start + offset + 12); + p = start + offset + 16; + ranges = []; + for (i = 0; i < groups; i++) { + ranges.push({ + start: getLong(data, p), + end: getLong(data, p + 4), + idDelta: getLong(data, p + 8) - getLong(data, p) + }); + p += 12; + } + return ranges; + } + error('not supported cmap: ' + format); + } + + function parseCff(data, start, end) { + var properties = {}; + var parser = new CFFParser(new Stream(data, start, end - start), + properties); + var cff = parser.parse(); + return { + glyphs: cff.charStrings.objects, + subrs: (cff.topDict.privateDict && cff.topDict.privateDict.subrsIndex && + cff.topDict.privateDict.subrsIndex.objects), + gsubrs: cff.globalSubrIndex && cff.globalSubrIndex.objects + }; + } + + function parseGlyfTable(glyf, loca, isGlyphLocationsLong) { + var itemSize, itemDecode; + if (isGlyphLocationsLong) { + itemSize = 4; + itemDecode = function fontItemDecodeLong(data, offset) { + return (data[offset] << 24) | (data[offset + 1] << 16) | + (data[offset + 2] << 8) | data[offset + 3]; + }; + } else { + itemSize = 2; + itemDecode = function fontItemDecode(data, offset) { + return (data[offset] << 9) | (data[offset + 1] << 1); + }; + } + var glyphs = []; + var startOffset = itemDecode(loca, 0); + for (var j = itemSize; j < loca.length; j += itemSize) { + var endOffset = itemDecode(loca, j); + glyphs.push(glyf.subarray(startOffset, endOffset)); + startOffset = endOffset; + } + return glyphs; + } + + function lookupCmap(ranges, unicode) { + var code = unicode.charCodeAt(0); + var l = 0, r = ranges.length - 1; + while (l < r) { + var c = (l + r + 1) >> 1; + if (code < ranges[c].start) { + r = c - 1; + } else { + l = c; + } + } + if (ranges[l].start <= code && code <= ranges[l].end) { + return (ranges[l].idDelta + (ranges[l].ids ? + ranges[l].ids[code - ranges[l].start] : code)) & 0xFFFF; + } + return 0; + } + + function compileGlyf(code, js, font) { + function moveTo(x, y) { + js.push('c.moveTo(' + x + ',' + y + ');'); + } + function lineTo(x, y) { + js.push('c.lineTo(' + x + ',' + y + ');'); + } + function quadraticCurveTo(xa, ya, x, y) { + js.push('c.quadraticCurveTo(' + xa + ',' + ya + ',' + + x + ',' + y + ');'); + } + + var i = 0; + var numberOfContours = ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + var flags; + var x = 0, y = 0; + i += 10; + if (numberOfContours < 0) { + // composite glyph + do { + flags = (code[i] << 8) | code[i + 1]; + var glyphIndex = (code[i + 2] << 8) | code[i + 3]; + i += 4; + var arg1, arg2; + if ((flags & 0x01)) { + arg1 = ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + arg2 = ((code[i + 2] << 24) | (code[i + 3] << 16)) >> 16; + i += 4; + } else { + arg1 = code[i++]; arg2 = code[i++]; + } + if ((flags & 0x02)) { + x = arg1; + y = arg2; + } else { + x = 0; y = 0; // TODO "they are points" ? + } + var scaleX = 1, scaleY = 1, scale01 = 0, scale10 = 0; + if ((flags & 0x08)) { + scaleX = + scaleY = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824; + i += 2; + } else if ((flags & 0x40)) { + scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824; + scaleY = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824; + i += 4; + } else if ((flags & 0x80)) { + scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824; + scale01 = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824; + scale10 = ((code[i + 4] << 24) | (code[i + 5] << 16)) / 1073741824; + scaleY = ((code[i + 6] << 24) | (code[i + 7] << 16)) / 1073741824; + i += 8; + } + var subglyph = font.glyphs[glyphIndex]; + if (subglyph) { + js.push('c.save();'); + js.push('c.transform(' + scaleX + ',' + scale01 + ',' + + scale10 + ',' + scaleY + ',' + x + ',' + y + ');'); + compileGlyf(subglyph, js, font); + js.push('c.restore();'); + } + } while ((flags & 0x20)); + } else { + // simple glyph + var endPtsOfContours = []; + var j, jj; + for (j = 0; j < numberOfContours; j++) { + endPtsOfContours.push((code[i] << 8) | code[i + 1]); + i += 2; + } + var instructionLength = (code[i] << 8) | code[i + 1]; + i += 2 + instructionLength; // skipping the instructions + var numberOfPoints = endPtsOfContours[endPtsOfContours.length - 1] + 1; + var points = []; + while (points.length < numberOfPoints) { + flags = code[i++]; + var repeat = 1; + if ((flags & 0x08)) { + repeat += code[i++]; + } + while (repeat-- > 0) { + points.push({flags: flags}); + } + } + for (j = 0; j < numberOfPoints; j++) { + switch (points[j].flags & 0x12) { + case 0x00: + x += ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + i += 2; + break; + case 0x02: + x -= code[i++]; + break; + case 0x12: + x += code[i++]; + break; + } + points[j].x = x; + } + for (j = 0; j < numberOfPoints; j++) { + switch (points[j].flags & 0x24) { + case 0x00: + y += ((code[i] << 24) | (code[i + 1] << 16)) >> 16; + i += 2; + break; + case 0x04: + y -= code[i++]; + break; + case 0x24: + y += code[i++]; + break; + } + points[j].y = y; + } + + var startPoint = 0; + for (i = 0; i < numberOfContours; i++) { + var endPoint = endPtsOfContours[i]; + // contours might have implicit points, which is located in the middle + // between two neighboring off-curve points + var contour = points.slice(startPoint, endPoint + 1); + if ((contour[0].flags & 1)) { + contour.push(contour[0]); // using start point at the contour end + } else if ((contour[contour.length - 1].flags & 1)) { + // first is off-curve point, trying to use one from the end + contour.unshift(contour[contour.length - 1]); + } else { + // start and end are off-curve points, creating implicit one + var p = { + flags: 1, + x: (contour[0].x + contour[contour.length - 1].x) / 2, + y: (contour[0].y + contour[contour.length - 1].y) / 2 + }; + contour.unshift(p); + contour.push(p); + } + moveTo(contour[0].x, contour[0].y); + for (j = 1, jj = contour.length; j < jj; j++) { + if ((contour[j].flags & 1)) { + lineTo(contour[j].x, contour[j].y); + } else if ((contour[j + 1].flags & 1)){ + quadraticCurveTo(contour[j].x, contour[j].y, + contour[j + 1].x, contour[j + 1].y); + j++; + } else { + quadraticCurveTo(contour[j].x, contour[j].y, + (contour[j].x + contour[j + 1].x) / 2, + (contour[j].y + contour[j + 1].y) / 2); + } + } + startPoint = endPoint + 1; + } + } + } + + function compileCharString(code, js, font) { + var stack = []; + var x = 0, y = 0; + var stems = 0; + + function moveTo(x, y) { + js.push('c.moveTo(' + x + ',' + y + ');'); + } + function lineTo(x, y) { + js.push('c.lineTo(' + x + ',' + y + ');'); + } + function bezierCurveTo(x1, y1, x2, y2, x, y) { + js.push('c.bezierCurveTo(' + x1 + ',' + y1 + ',' + x2 + ',' + y2 + ',' + + x + ',' + y + ');'); + } + + function parse(code) { + var i = 0; + while (i < code.length) { + var stackClean = false; + var v = code[i++]; + var xa, xb, ya, yb, y1, y2, y3, n, subrCode; + switch (v) { + case 1: // hstem + stems += stack.length >> 1; + stackClean = true; + break; + case 3: // vstem + stems += stack.length >> 1; + stackClean = true; + break; + case 4: // vmoveto + y += stack.pop(); + moveTo(x, y); + stackClean = true; + break; + case 5: // rlineto + while (stack.length > 0) { + x += stack.shift(); + y += stack.shift(); + lineTo(x, y); + } + break; + case 6: // hlineto + while (stack.length > 0) { + x += stack.shift(); + lineTo(x, y); + if (stack.length === 0) { + break; + } + y += stack.shift(); + lineTo(x, y); + } + break; + case 7: // vlineto + while (stack.length > 0) { + y += stack.shift(); + lineTo(x, y); + if (stack.length === 0) { + break; + } + x += stack.shift(); + lineTo(x, y); + } + break; + case 8: // rrcurveto + while (stack.length > 0) { + xa = x + stack.shift(); ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 10: // callsubr + n = stack.pop() + font.subrsBias; + subrCode = font.subrs[n]; + if (subrCode) { + parse(subrCode); + } + break; + case 11: // return + return; + case 12: + v = code[i++]; + switch (v) { + case 34: // flex + xa = x + stack.shift(); + xb = xa + stack.shift(); y1 = y + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y, xb, y1, x, y1); + xa = x + stack.shift(); + xb = xa + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y1, xb, y, x, y); + break; + case 35: // flex + xa = x + stack.shift(); ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + xa = x + stack.shift(); ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + stack.pop(); // fd + break; + case 36: // hflex1 + xa = x + stack.shift(); y1 = y + stack.shift(); + xb = xa + stack.shift(); y2 = y1 + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y1, xb, y2, x, y2); + xa = x + stack.shift(); + xb = xa + stack.shift(); y3 = y2 + stack.shift(); + x = xb + stack.shift(); + bezierCurveTo(xa, y2, xb, y3, x, y); + break; + case 37: // flex1 + var x0 = x, y0 = y; + xa = x + stack.shift(); ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + xa = x + stack.shift(); ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb; y = yb; + if (Math.abs(x - x0) > Math.abs(y - y0)) { + x += stack.shift(); + } else { + y += stack.shift(); + } + bezierCurveTo(xa, ya, xb, yb, x, y); + break; + default: + error('unknown operator: 12 ' + v); + } + break; + case 14: // endchar + if (stack.length >= 4) { + var achar = stack.pop(); + var bchar = stack.pop(); + y = stack.pop(); + x = stack.pop(); + js.push('c.save();'); + js.push('c.translate('+ x + ',' + y + ');'); + var gid = lookupCmap(font.cmap, String.fromCharCode( + font.glyphNameMap[Encodings.StandardEncoding[achar]])); + compileCharString(font.glyphs[gid], js, font); + js.push('c.restore();'); + + gid = lookupCmap(font.cmap, String.fromCharCode( + font.glyphNameMap[Encodings.StandardEncoding[bchar]])); + compileCharString(font.glyphs[gid], js, font); + } + return; + case 18: // hstemhm + stems += stack.length >> 1; + stackClean = true; + break; + case 19: // hintmask + stems += stack.length >> 1; + i += (stems + 7) >> 3; + stackClean = true; + break; + case 20: // cntrmask + stems += stack.length >> 1; + i += (stems + 7) >> 3; + stackClean = true; + break; + case 21: // rmoveto + y += stack.pop(); + x += stack.pop(); + moveTo(x, y); + stackClean = true; + break; + case 22: // hmoveto + x += stack.pop(); + moveTo(x, y); + stackClean = true; + break; + case 23: // vstemhm + stems += stack.length >> 1; + stackClean = true; + break; + case 24: // rcurveline + while (stack.length > 2) { + xa = x + stack.shift(); ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + x += stack.shift(); + y += stack.shift(); + lineTo(x, y); + break; + case 25: // rlinecurve + while (stack.length > 6) { + x += stack.shift(); + y += stack.shift(); + lineTo(x, y); + } + xa = x + stack.shift(); ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + break; + case 26: // vvcurveto + if (stack.length % 2) { + x += stack.shift(); + } + while (stack.length > 0) { + xa = x; ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb; y = yb + stack.shift(); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 27: // hhcurveto + if (stack.length % 2) { + y += stack.shift(); + } + while (stack.length > 0) { + xa = x + stack.shift(); ya = y; + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); y = yb; + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 28: + stack.push(((code[i] << 24) | (code[i + 1] << 16)) >> 16); + i += 2; + break; + case 29: // callgsubr + n = stack.pop() + font.gsubrsBias; + subrCode = font.gsubrs[n]; + if (subrCode) { + parse(subrCode); + } + break; + case 30: // vhcurveto + while (stack.length > 0) { + xa = x; ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); + y = yb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + if (stack.length === 0) { + break; + } + + xa = x + stack.shift(); ya = y; + xb = xa + stack.shift(); yb = ya + stack.shift(); + y = yb + stack.shift(); + x = xb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + case 31: // hvcurveto + while (stack.length > 0) { + xa = x + stack.shift(); ya = y; + xb = xa + stack.shift(); yb = ya + stack.shift(); + y = yb + stack.shift(); + x = xb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + if (stack.length === 0) { + break; + } + + xa = x; ya = y + stack.shift(); + xb = xa + stack.shift(); yb = ya + stack.shift(); + x = xb + stack.shift(); + y = yb + (stack.length === 1 ? stack.shift() : 0); + bezierCurveTo(xa, ya, xb, yb, x, y); + } + break; + default: + if (v < 32) { + error('unknown operator: ' + v); + } + if (v < 247) { + stack.push(v - 139); + } else if (v < 251) { + stack.push((v - 247) * 256 + code[i++] + 108); + } else if (v < 255) { + stack.push(-(v - 251) * 256 - code[i++] - 108); + } else { + stack.push(((code[i] << 24) | (code[i + 1] << 16) | + (code[i + 2] << 8) | code[i + 3]) / 65536); + i += 4; + } + break; + } + if (stackClean) { + stack.length = 0; + } + } + } + parse(code); + } + + var noop = ''; + + function CompiledFont(fontMatrix) { + this.compiledGlyphs = {}; + this.fontMatrix = fontMatrix; + } + CompiledFont.prototype = { + getPathJs: function (unicode) { + var gid = lookupCmap(this.cmap, unicode); + var fn = this.compiledGlyphs[gid]; + if (!fn) { + this.compiledGlyphs[gid] = fn = this.compileGlyph(this.glyphs[gid]); + } + return fn; + }, + + compileGlyph: function (code) { + if (!code || code.length === 0 || code[0] === 14) { + return noop; + } + + var js = []; + js.push('c.save();'); + js.push('c.transform(' + this.fontMatrix.join(',') + ');'); + js.push('c.scale(size, -size);'); + + this.compileGlyphImpl(code, js); + + js.push('c.restore();'); + + return js.join('\n'); + }, + + compileGlyphImpl: function () { + error('Children classes should implement this.'); + }, + + hasBuiltPath: function (unicode) { + var gid = lookupCmap(this.cmap, unicode); + return gid in this.compiledGlyphs; + } + }; + + function TrueTypeCompiled(glyphs, cmap, fontMatrix) { + fontMatrix = fontMatrix || [0.000488, 0, 0, 0.000488, 0, 0]; + CompiledFont.call(this, fontMatrix); + + this.glyphs = glyphs; + this.cmap = cmap; + + this.compiledGlyphs = []; + } + + Util.inherit(TrueTypeCompiled, CompiledFont, { + compileGlyphImpl: function (code, js) { + compileGlyf(code, js, this); + } + }); + + function Type2Compiled(cffInfo, cmap, fontMatrix, glyphNameMap) { + fontMatrix = fontMatrix || [0.001, 0, 0, 0.001, 0, 0]; + CompiledFont.call(this, fontMatrix); + this.glyphs = cffInfo.glyphs; + this.gsubrs = cffInfo.gsubrs || []; + this.subrs = cffInfo.subrs || []; + this.cmap = cmap; + this.glyphNameMap = glyphNameMap || GlyphsUnicode; + + this.compiledGlyphs = []; + this.gsubrsBias = (this.gsubrs.length < 1240 ? + 107 : (this.gsubrs.length < 33900 ? 1131 : 32768)); + this.subrsBias = (this.subrs.length < 1240 ? + 107 : (this.subrs.length < 33900 ? 1131 : 32768)); + } + + Util.inherit(Type2Compiled, CompiledFont, { + compileGlyphImpl: function (code, js) { + compileCharString(code, js, this); + } + }); + + + return { + create: function FontRendererFactory_create(font) { + var data = new Uint8Array(font.data); + var cmap, glyf, loca, cff, indexToLocFormat, unitsPerEm; + var numTables = getUshort(data, 4); + for (var i = 0, p = 12; i < numTables; i++, p += 16) { + var tag = bytesToString(data.subarray(p, p + 4)); + var offset = getLong(data, p + 8); + var length = getLong(data, p + 12); + switch (tag) { + case 'cmap': + cmap = parseCmap(data, offset, offset + length); + break; + case 'glyf': + glyf = data.subarray(offset, offset + length); + break; + case 'loca': + loca = data.subarray(offset, offset + length); + break; + case 'head': + unitsPerEm = getUshort(data, offset + 18); + indexToLocFormat = getUshort(data, offset + 50); + break; + case 'CFF ': + cff = parseCff(data, offset, offset + length); + break; + } + } + + if (glyf) { + var fontMatrix = (!unitsPerEm ? font.fontMatrix : + [1 / unitsPerEm, 0, 0, 1 / unitsPerEm, 0, 0]); + return new TrueTypeCompiled( + parseGlyfTable(glyf, loca, indexToLocFormat), cmap, fontMatrix); + } else { + return new Type2Compiled(cff, cmap, font.fontMatrix, font.glyphNameMap); + } + } + }; +})(); + + +var GlyphsUnicode = { + A: 0x0041, + AE: 0x00C6, + AEacute: 0x01FC, + AEmacron: 0x01E2, + AEsmall: 0xF7E6, + Aacute: 0x00C1, + Aacutesmall: 0xF7E1, + Abreve: 0x0102, + Abreveacute: 0x1EAE, + Abrevecyrillic: 0x04D0, + Abrevedotbelow: 0x1EB6, + Abrevegrave: 0x1EB0, + Abrevehookabove: 0x1EB2, + Abrevetilde: 0x1EB4, + Acaron: 0x01CD, + Acircle: 0x24B6, + Acircumflex: 0x00C2, + Acircumflexacute: 0x1EA4, + Acircumflexdotbelow: 0x1EAC, + Acircumflexgrave: 0x1EA6, + Acircumflexhookabove: 0x1EA8, + Acircumflexsmall: 0xF7E2, + Acircumflextilde: 0x1EAA, + Acute: 0xF6C9, + Acutesmall: 0xF7B4, + Acyrillic: 0x0410, + Adblgrave: 0x0200, + Adieresis: 0x00C4, + Adieresiscyrillic: 0x04D2, + Adieresismacron: 0x01DE, + Adieresissmall: 0xF7E4, + Adotbelow: 0x1EA0, + Adotmacron: 0x01E0, + Agrave: 0x00C0, + Agravesmall: 0xF7E0, + Ahookabove: 0x1EA2, + Aiecyrillic: 0x04D4, + Ainvertedbreve: 0x0202, + Alpha: 0x0391, + Alphatonos: 0x0386, + Amacron: 0x0100, + Amonospace: 0xFF21, + Aogonek: 0x0104, + Aring: 0x00C5, + Aringacute: 0x01FA, + Aringbelow: 0x1E00, + Aringsmall: 0xF7E5, + Asmall: 0xF761, + Atilde: 0x00C3, + Atildesmall: 0xF7E3, + Aybarmenian: 0x0531, + B: 0x0042, + Bcircle: 0x24B7, + Bdotaccent: 0x1E02, + Bdotbelow: 0x1E04, + Becyrillic: 0x0411, + Benarmenian: 0x0532, + Beta: 0x0392, + Bhook: 0x0181, + Blinebelow: 0x1E06, + Bmonospace: 0xFF22, + Brevesmall: 0xF6F4, + Bsmall: 0xF762, + Btopbar: 0x0182, + C: 0x0043, + Caarmenian: 0x053E, + Cacute: 0x0106, + Caron: 0xF6CA, + Caronsmall: 0xF6F5, + Ccaron: 0x010C, + Ccedilla: 0x00C7, + Ccedillaacute: 0x1E08, + Ccedillasmall: 0xF7E7, + Ccircle: 0x24B8, + Ccircumflex: 0x0108, + Cdot: 0x010A, + Cdotaccent: 0x010A, + Cedillasmall: 0xF7B8, + Chaarmenian: 0x0549, + Cheabkhasiancyrillic: 0x04BC, + Checyrillic: 0x0427, + Chedescenderabkhasiancyrillic: 0x04BE, + Chedescendercyrillic: 0x04B6, + Chedieresiscyrillic: 0x04F4, + Cheharmenian: 0x0543, + Chekhakassiancyrillic: 0x04CB, + Cheverticalstrokecyrillic: 0x04B8, + Chi: 0x03A7, + Chook: 0x0187, + Circumflexsmall: 0xF6F6, + Cmonospace: 0xFF23, + Coarmenian: 0x0551, + Csmall: 0xF763, + D: 0x0044, + DZ: 0x01F1, + DZcaron: 0x01C4, + Daarmenian: 0x0534, + Dafrican: 0x0189, + Dcaron: 0x010E, + Dcedilla: 0x1E10, + Dcircle: 0x24B9, + Dcircumflexbelow: 0x1E12, + Dcroat: 0x0110, + Ddotaccent: 0x1E0A, + Ddotbelow: 0x1E0C, + Decyrillic: 0x0414, + Deicoptic: 0x03EE, + Delta: 0x2206, + Deltagreek: 0x0394, + Dhook: 0x018A, + Dieresis: 0xF6CB, + DieresisAcute: 0xF6CC, + DieresisGrave: 0xF6CD, + Dieresissmall: 0xF7A8, + Digammagreek: 0x03DC, + Djecyrillic: 0x0402, + Dlinebelow: 0x1E0E, + Dmonospace: 0xFF24, + Dotaccentsmall: 0xF6F7, + Dslash: 0x0110, + Dsmall: 0xF764, + Dtopbar: 0x018B, + Dz: 0x01F2, + Dzcaron: 0x01C5, + Dzeabkhasiancyrillic: 0x04E0, + Dzecyrillic: 0x0405, + Dzhecyrillic: 0x040F, + E: 0x0045, + Eacute: 0x00C9, + Eacutesmall: 0xF7E9, + Ebreve: 0x0114, + Ecaron: 0x011A, + Ecedillabreve: 0x1E1C, + Echarmenian: 0x0535, + Ecircle: 0x24BA, + Ecircumflex: 0x00CA, + Ecircumflexacute: 0x1EBE, + Ecircumflexbelow: 0x1E18, + Ecircumflexdotbelow: 0x1EC6, + Ecircumflexgrave: 0x1EC0, + Ecircumflexhookabove: 0x1EC2, + Ecircumflexsmall: 0xF7EA, + Ecircumflextilde: 0x1EC4, + Ecyrillic: 0x0404, + Edblgrave: 0x0204, + Edieresis: 0x00CB, + Edieresissmall: 0xF7EB, + Edot: 0x0116, + Edotaccent: 0x0116, + Edotbelow: 0x1EB8, + Efcyrillic: 0x0424, + Egrave: 0x00C8, + Egravesmall: 0xF7E8, + Eharmenian: 0x0537, + Ehookabove: 0x1EBA, + Eightroman: 0x2167, + Einvertedbreve: 0x0206, + Eiotifiedcyrillic: 0x0464, + Elcyrillic: 0x041B, + Elevenroman: 0x216A, + Emacron: 0x0112, + Emacronacute: 0x1E16, + Emacrongrave: 0x1E14, + Emcyrillic: 0x041C, + Emonospace: 0xFF25, + Encyrillic: 0x041D, + Endescendercyrillic: 0x04A2, + Eng: 0x014A, + Enghecyrillic: 0x04A4, + Enhookcyrillic: 0x04C7, + Eogonek: 0x0118, + Eopen: 0x0190, + Epsilon: 0x0395, + Epsilontonos: 0x0388, + Ercyrillic: 0x0420, + Ereversed: 0x018E, + Ereversedcyrillic: 0x042D, + Escyrillic: 0x0421, + Esdescendercyrillic: 0x04AA, + Esh: 0x01A9, + Esmall: 0xF765, + Eta: 0x0397, + Etarmenian: 0x0538, + Etatonos: 0x0389, + Eth: 0x00D0, + Ethsmall: 0xF7F0, + Etilde: 0x1EBC, + Etildebelow: 0x1E1A, + Euro: 0x20AC, + Ezh: 0x01B7, + Ezhcaron: 0x01EE, + Ezhreversed: 0x01B8, + F: 0x0046, + Fcircle: 0x24BB, + Fdotaccent: 0x1E1E, + Feharmenian: 0x0556, + Feicoptic: 0x03E4, + Fhook: 0x0191, + Fitacyrillic: 0x0472, + Fiveroman: 0x2164, + Fmonospace: 0xFF26, + Fourroman: 0x2163, + Fsmall: 0xF766, + G: 0x0047, + GBsquare: 0x3387, + Gacute: 0x01F4, + Gamma: 0x0393, + Gammaafrican: 0x0194, + Gangiacoptic: 0x03EA, + Gbreve: 0x011E, + Gcaron: 0x01E6, + Gcedilla: 0x0122, + Gcircle: 0x24BC, + Gcircumflex: 0x011C, + Gcommaaccent: 0x0122, + Gdot: 0x0120, + Gdotaccent: 0x0120, + Gecyrillic: 0x0413, + Ghadarmenian: 0x0542, + Ghemiddlehookcyrillic: 0x0494, + Ghestrokecyrillic: 0x0492, + Gheupturncyrillic: 0x0490, + Ghook: 0x0193, + Gimarmenian: 0x0533, + Gjecyrillic: 0x0403, + Gmacron: 0x1E20, + Gmonospace: 0xFF27, + Grave: 0xF6CE, + Gravesmall: 0xF760, + Gsmall: 0xF767, + Gsmallhook: 0x029B, + Gstroke: 0x01E4, + H: 0x0048, + H18533: 0x25CF, + H18543: 0x25AA, + H18551: 0x25AB, + H22073: 0x25A1, + HPsquare: 0x33CB, + Haabkhasiancyrillic: 0x04A8, + Hadescendercyrillic: 0x04B2, + Hardsigncyrillic: 0x042A, + Hbar: 0x0126, + Hbrevebelow: 0x1E2A, + Hcedilla: 0x1E28, + Hcircle: 0x24BD, + Hcircumflex: 0x0124, + Hdieresis: 0x1E26, + Hdotaccent: 0x1E22, + Hdotbelow: 0x1E24, + Hmonospace: 0xFF28, + Hoarmenian: 0x0540, + Horicoptic: 0x03E8, + Hsmall: 0xF768, + Hungarumlaut: 0xF6CF, + Hungarumlautsmall: 0xF6F8, + Hzsquare: 0x3390, + I: 0x0049, + IAcyrillic: 0x042F, + IJ: 0x0132, + IUcyrillic: 0x042E, + Iacute: 0x00CD, + Iacutesmall: 0xF7ED, + Ibreve: 0x012C, + Icaron: 0x01CF, + Icircle: 0x24BE, + Icircumflex: 0x00CE, + Icircumflexsmall: 0xF7EE, + Icyrillic: 0x0406, + Idblgrave: 0x0208, + Idieresis: 0x00CF, + Idieresisacute: 0x1E2E, + Idieresiscyrillic: 0x04E4, + Idieresissmall: 0xF7EF, + Idot: 0x0130, + Idotaccent: 0x0130, + Idotbelow: 0x1ECA, + Iebrevecyrillic: 0x04D6, + Iecyrillic: 0x0415, + Ifraktur: 0x2111, + Igrave: 0x00CC, + Igravesmall: 0xF7EC, + Ihookabove: 0x1EC8, + Iicyrillic: 0x0418, + Iinvertedbreve: 0x020A, + Iishortcyrillic: 0x0419, + Imacron: 0x012A, + Imacroncyrillic: 0x04E2, + Imonospace: 0xFF29, + Iniarmenian: 0x053B, + Iocyrillic: 0x0401, + Iogonek: 0x012E, + Iota: 0x0399, + Iotaafrican: 0x0196, + Iotadieresis: 0x03AA, + Iotatonos: 0x038A, + Ismall: 0xF769, + Istroke: 0x0197, + Itilde: 0x0128, + Itildebelow: 0x1E2C, + Izhitsacyrillic: 0x0474, + Izhitsadblgravecyrillic: 0x0476, + J: 0x004A, + Jaarmenian: 0x0541, + Jcircle: 0x24BF, + Jcircumflex: 0x0134, + Jecyrillic: 0x0408, + Jheharmenian: 0x054B, + Jmonospace: 0xFF2A, + Jsmall: 0xF76A, + K: 0x004B, + KBsquare: 0x3385, + KKsquare: 0x33CD, + Kabashkircyrillic: 0x04A0, + Kacute: 0x1E30, + Kacyrillic: 0x041A, + Kadescendercyrillic: 0x049A, + Kahookcyrillic: 0x04C3, + Kappa: 0x039A, + Kastrokecyrillic: 0x049E, + Kaverticalstrokecyrillic: 0x049C, + Kcaron: 0x01E8, + Kcedilla: 0x0136, + Kcircle: 0x24C0, + Kcommaaccent: 0x0136, + Kdotbelow: 0x1E32, + Keharmenian: 0x0554, + Kenarmenian: 0x053F, + Khacyrillic: 0x0425, + Kheicoptic: 0x03E6, + Khook: 0x0198, + Kjecyrillic: 0x040C, + Klinebelow: 0x1E34, + Kmonospace: 0xFF2B, + Koppacyrillic: 0x0480, + Koppagreek: 0x03DE, + Ksicyrillic: 0x046E, + Ksmall: 0xF76B, + L: 0x004C, + LJ: 0x01C7, + LL: 0xF6BF, + Lacute: 0x0139, + Lambda: 0x039B, + Lcaron: 0x013D, + Lcedilla: 0x013B, + Lcircle: 0x24C1, + Lcircumflexbelow: 0x1E3C, + Lcommaaccent: 0x013B, + Ldot: 0x013F, + Ldotaccent: 0x013F, + Ldotbelow: 0x1E36, + Ldotbelowmacron: 0x1E38, + Liwnarmenian: 0x053C, + Lj: 0x01C8, + Ljecyrillic: 0x0409, + Llinebelow: 0x1E3A, + Lmonospace: 0xFF2C, + Lslash: 0x0141, + Lslashsmall: 0xF6F9, + Lsmall: 0xF76C, + M: 0x004D, + MBsquare: 0x3386, + Macron: 0xF6D0, + Macronsmall: 0xF7AF, + Macute: 0x1E3E, + Mcircle: 0x24C2, + Mdotaccent: 0x1E40, + Mdotbelow: 0x1E42, + Menarmenian: 0x0544, + Mmonospace: 0xFF2D, + Msmall: 0xF76D, + Mturned: 0x019C, + Mu: 0x039C, + N: 0x004E, + NJ: 0x01CA, + Nacute: 0x0143, + Ncaron: 0x0147, + Ncedilla: 0x0145, + Ncircle: 0x24C3, + Ncircumflexbelow: 0x1E4A, + Ncommaaccent: 0x0145, + Ndotaccent: 0x1E44, + Ndotbelow: 0x1E46, + Nhookleft: 0x019D, + Nineroman: 0x2168, + Nj: 0x01CB, + Njecyrillic: 0x040A, + Nlinebelow: 0x1E48, + Nmonospace: 0xFF2E, + Nowarmenian: 0x0546, + Nsmall: 0xF76E, + Ntilde: 0x00D1, + Ntildesmall: 0xF7F1, + Nu: 0x039D, + O: 0x004F, + OE: 0x0152, + OEsmall: 0xF6FA, + Oacute: 0x00D3, + Oacutesmall: 0xF7F3, + Obarredcyrillic: 0x04E8, + Obarreddieresiscyrillic: 0x04EA, + Obreve: 0x014E, + Ocaron: 0x01D1, + Ocenteredtilde: 0x019F, + Ocircle: 0x24C4, + Ocircumflex: 0x00D4, + Ocircumflexacute: 0x1ED0, + Ocircumflexdotbelow: 0x1ED8, + Ocircumflexgrave: 0x1ED2, + Ocircumflexhookabove: 0x1ED4, + Ocircumflexsmall: 0xF7F4, + Ocircumflextilde: 0x1ED6, + Ocyrillic: 0x041E, + Odblacute: 0x0150, + Odblgrave: 0x020C, + Odieresis: 0x00D6, + Odieresiscyrillic: 0x04E6, + Odieresissmall: 0xF7F6, + Odotbelow: 0x1ECC, + Ogoneksmall: 0xF6FB, + Ograve: 0x00D2, + Ogravesmall: 0xF7F2, + Oharmenian: 0x0555, + Ohm: 0x2126, + Ohookabove: 0x1ECE, + Ohorn: 0x01A0, + Ohornacute: 0x1EDA, + Ohorndotbelow: 0x1EE2, + Ohorngrave: 0x1EDC, + Ohornhookabove: 0x1EDE, + Ohorntilde: 0x1EE0, + Ohungarumlaut: 0x0150, + Oi: 0x01A2, + Oinvertedbreve: 0x020E, + Omacron: 0x014C, + Omacronacute: 0x1E52, + Omacrongrave: 0x1E50, + Omega: 0x2126, + Omegacyrillic: 0x0460, + Omegagreek: 0x03A9, + Omegaroundcyrillic: 0x047A, + Omegatitlocyrillic: 0x047C, + Omegatonos: 0x038F, + Omicron: 0x039F, + Omicrontonos: 0x038C, + Omonospace: 0xFF2F, + Oneroman: 0x2160, + Oogonek: 0x01EA, + Oogonekmacron: 0x01EC, + Oopen: 0x0186, + Oslash: 0x00D8, + Oslashacute: 0x01FE, + Oslashsmall: 0xF7F8, + Osmall: 0xF76F, + Ostrokeacute: 0x01FE, + Otcyrillic: 0x047E, + Otilde: 0x00D5, + Otildeacute: 0x1E4C, + Otildedieresis: 0x1E4E, + Otildesmall: 0xF7F5, + P: 0x0050, + Pacute: 0x1E54, + Pcircle: 0x24C5, + Pdotaccent: 0x1E56, + Pecyrillic: 0x041F, + Peharmenian: 0x054A, + Pemiddlehookcyrillic: 0x04A6, + Phi: 0x03A6, + Phook: 0x01A4, + Pi: 0x03A0, + Piwrarmenian: 0x0553, + Pmonospace: 0xFF30, + Psi: 0x03A8, + Psicyrillic: 0x0470, + Psmall: 0xF770, + Q: 0x0051, + Qcircle: 0x24C6, + Qmonospace: 0xFF31, + Qsmall: 0xF771, + R: 0x0052, + Raarmenian: 0x054C, + Racute: 0x0154, + Rcaron: 0x0158, + Rcedilla: 0x0156, + Rcircle: 0x24C7, + Rcommaaccent: 0x0156, + Rdblgrave: 0x0210, + Rdotaccent: 0x1E58, + Rdotbelow: 0x1E5A, + Rdotbelowmacron: 0x1E5C, + Reharmenian: 0x0550, + Rfraktur: 0x211C, + Rho: 0x03A1, + Ringsmall: 0xF6FC, + Rinvertedbreve: 0x0212, + Rlinebelow: 0x1E5E, + Rmonospace: 0xFF32, + Rsmall: 0xF772, + Rsmallinverted: 0x0281, + Rsmallinvertedsuperior: 0x02B6, + S: 0x0053, + SF010000: 0x250C, + SF020000: 0x2514, + SF030000: 0x2510, + SF040000: 0x2518, + SF050000: 0x253C, + SF060000: 0x252C, + SF070000: 0x2534, + SF080000: 0x251C, + SF090000: 0x2524, + SF100000: 0x2500, + SF110000: 0x2502, + SF190000: 0x2561, + SF200000: 0x2562, + SF210000: 0x2556, + SF220000: 0x2555, + SF230000: 0x2563, + SF240000: 0x2551, + SF250000: 0x2557, + SF260000: 0x255D, + SF270000: 0x255C, + SF280000: 0x255B, + SF360000: 0x255E, + SF370000: 0x255F, + SF380000: 0x255A, + SF390000: 0x2554, + SF400000: 0x2569, + SF410000: 0x2566, + SF420000: 0x2560, + SF430000: 0x2550, + SF440000: 0x256C, + SF450000: 0x2567, + SF460000: 0x2568, + SF470000: 0x2564, + SF480000: 0x2565, + SF490000: 0x2559, + SF500000: 0x2558, + SF510000: 0x2552, + SF520000: 0x2553, + SF530000: 0x256B, + SF540000: 0x256A, + Sacute: 0x015A, + Sacutedotaccent: 0x1E64, + Sampigreek: 0x03E0, + Scaron: 0x0160, + Scarondotaccent: 0x1E66, + Scaronsmall: 0xF6FD, + Scedilla: 0x015E, + Schwa: 0x018F, + Schwacyrillic: 0x04D8, + Schwadieresiscyrillic: 0x04DA, + Scircle: 0x24C8, + Scircumflex: 0x015C, + Scommaaccent: 0x0218, + Sdotaccent: 0x1E60, + Sdotbelow: 0x1E62, + Sdotbelowdotaccent: 0x1E68, + Seharmenian: 0x054D, + Sevenroman: 0x2166, + Shaarmenian: 0x0547, + Shacyrillic: 0x0428, + Shchacyrillic: 0x0429, + Sheicoptic: 0x03E2, + Shhacyrillic: 0x04BA, + Shimacoptic: 0x03EC, + Sigma: 0x03A3, + Sixroman: 0x2165, + Smonospace: 0xFF33, + Softsigncyrillic: 0x042C, + Ssmall: 0xF773, + Stigmagreek: 0x03DA, + T: 0x0054, + Tau: 0x03A4, + Tbar: 0x0166, + Tcaron: 0x0164, + Tcedilla: 0x0162, + Tcircle: 0x24C9, + Tcircumflexbelow: 0x1E70, + Tcommaaccent: 0x0162, + Tdotaccent: 0x1E6A, + Tdotbelow: 0x1E6C, + Tecyrillic: 0x0422, + Tedescendercyrillic: 0x04AC, + Tenroman: 0x2169, + Tetsecyrillic: 0x04B4, + Theta: 0x0398, + Thook: 0x01AC, + Thorn: 0x00DE, + Thornsmall: 0xF7FE, + Threeroman: 0x2162, + Tildesmall: 0xF6FE, + Tiwnarmenian: 0x054F, + Tlinebelow: 0x1E6E, + Tmonospace: 0xFF34, + Toarmenian: 0x0539, + Tonefive: 0x01BC, + Tonesix: 0x0184, + Tonetwo: 0x01A7, + Tretroflexhook: 0x01AE, + Tsecyrillic: 0x0426, + Tshecyrillic: 0x040B, + Tsmall: 0xF774, + Twelveroman: 0x216B, + Tworoman: 0x2161, + U: 0x0055, + Uacute: 0x00DA, + Uacutesmall: 0xF7FA, + Ubreve: 0x016C, + Ucaron: 0x01D3, + Ucircle: 0x24CA, + Ucircumflex: 0x00DB, + Ucircumflexbelow: 0x1E76, + Ucircumflexsmall: 0xF7FB, + Ucyrillic: 0x0423, + Udblacute: 0x0170, + Udblgrave: 0x0214, + Udieresis: 0x00DC, + Udieresisacute: 0x01D7, + Udieresisbelow: 0x1E72, + Udieresiscaron: 0x01D9, + Udieresiscyrillic: 0x04F0, + Udieresisgrave: 0x01DB, + Udieresismacron: 0x01D5, + Udieresissmall: 0xF7FC, + Udotbelow: 0x1EE4, + Ugrave: 0x00D9, + Ugravesmall: 0xF7F9, + Uhookabove: 0x1EE6, + Uhorn: 0x01AF, + Uhornacute: 0x1EE8, + Uhorndotbelow: 0x1EF0, + Uhorngrave: 0x1EEA, + Uhornhookabove: 0x1EEC, + Uhorntilde: 0x1EEE, + Uhungarumlaut: 0x0170, + Uhungarumlautcyrillic: 0x04F2, + Uinvertedbreve: 0x0216, + Ukcyrillic: 0x0478, + Umacron: 0x016A, + Umacroncyrillic: 0x04EE, + Umacrondieresis: 0x1E7A, + Umonospace: 0xFF35, + Uogonek: 0x0172, + Upsilon: 0x03A5, + Upsilon1: 0x03D2, + Upsilonacutehooksymbolgreek: 0x03D3, + Upsilonafrican: 0x01B1, + Upsilondieresis: 0x03AB, + Upsilondieresishooksymbolgreek: 0x03D4, + Upsilonhooksymbol: 0x03D2, + Upsilontonos: 0x038E, + Uring: 0x016E, + Ushortcyrillic: 0x040E, + Usmall: 0xF775, + Ustraightcyrillic: 0x04AE, + Ustraightstrokecyrillic: 0x04B0, + Utilde: 0x0168, + Utildeacute: 0x1E78, + Utildebelow: 0x1E74, + V: 0x0056, + Vcircle: 0x24CB, + Vdotbelow: 0x1E7E, + Vecyrillic: 0x0412, + Vewarmenian: 0x054E, + Vhook: 0x01B2, + Vmonospace: 0xFF36, + Voarmenian: 0x0548, + Vsmall: 0xF776, + Vtilde: 0x1E7C, + W: 0x0057, + Wacute: 0x1E82, + Wcircle: 0x24CC, + Wcircumflex: 0x0174, + Wdieresis: 0x1E84, + Wdotaccent: 0x1E86, + Wdotbelow: 0x1E88, + Wgrave: 0x1E80, + Wmonospace: 0xFF37, + Wsmall: 0xF777, + X: 0x0058, + Xcircle: 0x24CD, + Xdieresis: 0x1E8C, + Xdotaccent: 0x1E8A, + Xeharmenian: 0x053D, + Xi: 0x039E, + Xmonospace: 0xFF38, + Xsmall: 0xF778, + Y: 0x0059, + Yacute: 0x00DD, + Yacutesmall: 0xF7FD, + Yatcyrillic: 0x0462, + Ycircle: 0x24CE, + Ycircumflex: 0x0176, + Ydieresis: 0x0178, + Ydieresissmall: 0xF7FF, + Ydotaccent: 0x1E8E, + Ydotbelow: 0x1EF4, + Yericyrillic: 0x042B, + Yerudieresiscyrillic: 0x04F8, + Ygrave: 0x1EF2, + Yhook: 0x01B3, + Yhookabove: 0x1EF6, + Yiarmenian: 0x0545, + Yicyrillic: 0x0407, + Yiwnarmenian: 0x0552, + Ymonospace: 0xFF39, + Ysmall: 0xF779, + Ytilde: 0x1EF8, + Yusbigcyrillic: 0x046A, + Yusbigiotifiedcyrillic: 0x046C, + Yuslittlecyrillic: 0x0466, + Yuslittleiotifiedcyrillic: 0x0468, + Z: 0x005A, + Zaarmenian: 0x0536, + Zacute: 0x0179, + Zcaron: 0x017D, + Zcaronsmall: 0xF6FF, + Zcircle: 0x24CF, + Zcircumflex: 0x1E90, + Zdot: 0x017B, + Zdotaccent: 0x017B, + Zdotbelow: 0x1E92, + Zecyrillic: 0x0417, + Zedescendercyrillic: 0x0498, + Zedieresiscyrillic: 0x04DE, + Zeta: 0x0396, + Zhearmenian: 0x053A, + Zhebrevecyrillic: 0x04C1, + Zhecyrillic: 0x0416, + Zhedescendercyrillic: 0x0496, + Zhedieresiscyrillic: 0x04DC, + Zlinebelow: 0x1E94, + Zmonospace: 0xFF3A, + Zsmall: 0xF77A, + Zstroke: 0x01B5, + a: 0x0061, + aabengali: 0x0986, + aacute: 0x00E1, + aadeva: 0x0906, + aagujarati: 0x0A86, + aagurmukhi: 0x0A06, + aamatragurmukhi: 0x0A3E, + aarusquare: 0x3303, + aavowelsignbengali: 0x09BE, + aavowelsigndeva: 0x093E, + aavowelsigngujarati: 0x0ABE, + abbreviationmarkarmenian: 0x055F, + abbreviationsigndeva: 0x0970, + abengali: 0x0985, + abopomofo: 0x311A, + abreve: 0x0103, + abreveacute: 0x1EAF, + abrevecyrillic: 0x04D1, + abrevedotbelow: 0x1EB7, + abrevegrave: 0x1EB1, + abrevehookabove: 0x1EB3, + abrevetilde: 0x1EB5, + acaron: 0x01CE, + acircle: 0x24D0, + acircumflex: 0x00E2, + acircumflexacute: 0x1EA5, + acircumflexdotbelow: 0x1EAD, + acircumflexgrave: 0x1EA7, + acircumflexhookabove: 0x1EA9, + acircumflextilde: 0x1EAB, + acute: 0x00B4, + acutebelowcmb: 0x0317, + acutecmb: 0x0301, + acutecomb: 0x0301, + acutedeva: 0x0954, + acutelowmod: 0x02CF, + acutetonecmb: 0x0341, + acyrillic: 0x0430, + adblgrave: 0x0201, + addakgurmukhi: 0x0A71, + adeva: 0x0905, + adieresis: 0x00E4, + adieresiscyrillic: 0x04D3, + adieresismacron: 0x01DF, + adotbelow: 0x1EA1, + adotmacron: 0x01E1, + ae: 0x00E6, + aeacute: 0x01FD, + aekorean: 0x3150, + aemacron: 0x01E3, + afii00208: 0x2015, + afii08941: 0x20A4, + afii10017: 0x0410, + afii10018: 0x0411, + afii10019: 0x0412, + afii10020: 0x0413, + afii10021: 0x0414, + afii10022: 0x0415, + afii10023: 0x0401, + afii10024: 0x0416, + afii10025: 0x0417, + afii10026: 0x0418, + afii10027: 0x0419, + afii10028: 0x041A, + afii10029: 0x041B, + afii10030: 0x041C, + afii10031: 0x041D, + afii10032: 0x041E, + afii10033: 0x041F, + afii10034: 0x0420, + afii10035: 0x0421, + afii10036: 0x0422, + afii10037: 0x0423, + afii10038: 0x0424, + afii10039: 0x0425, + afii10040: 0x0426, + afii10041: 0x0427, + afii10042: 0x0428, + afii10043: 0x0429, + afii10044: 0x042A, + afii10045: 0x042B, + afii10046: 0x042C, + afii10047: 0x042D, + afii10048: 0x042E, + afii10049: 0x042F, + afii10050: 0x0490, + afii10051: 0x0402, + afii10052: 0x0403, + afii10053: 0x0404, + afii10054: 0x0405, + afii10055: 0x0406, + afii10056: 0x0407, + afii10057: 0x0408, + afii10058: 0x0409, + afii10059: 0x040A, + afii10060: 0x040B, + afii10061: 0x040C, + afii10062: 0x040E, + afii10063: 0xF6C4, + afii10064: 0xF6C5, + afii10065: 0x0430, + afii10066: 0x0431, + afii10067: 0x0432, + afii10068: 0x0433, + afii10069: 0x0434, + afii10070: 0x0435, + afii10071: 0x0451, + afii10072: 0x0436, + afii10073: 0x0437, + afii10074: 0x0438, + afii10075: 0x0439, + afii10076: 0x043A, + afii10077: 0x043B, + afii10078: 0x043C, + afii10079: 0x043D, + afii10080: 0x043E, + afii10081: 0x043F, + afii10082: 0x0440, + afii10083: 0x0441, + afii10084: 0x0442, + afii10085: 0x0443, + afii10086: 0x0444, + afii10087: 0x0445, + afii10088: 0x0446, + afii10089: 0x0447, + afii10090: 0x0448, + afii10091: 0x0449, + afii10092: 0x044A, + afii10093: 0x044B, + afii10094: 0x044C, + afii10095: 0x044D, + afii10096: 0x044E, + afii10097: 0x044F, + afii10098: 0x0491, + afii10099: 0x0452, + afii10100: 0x0453, + afii10101: 0x0454, + afii10102: 0x0455, + afii10103: 0x0456, + afii10104: 0x0457, + afii10105: 0x0458, + afii10106: 0x0459, + afii10107: 0x045A, + afii10108: 0x045B, + afii10109: 0x045C, + afii10110: 0x045E, + afii10145: 0x040F, + afii10146: 0x0462, + afii10147: 0x0472, + afii10148: 0x0474, + afii10192: 0xF6C6, + afii10193: 0x045F, + afii10194: 0x0463, + afii10195: 0x0473, + afii10196: 0x0475, + afii10831: 0xF6C7, + afii10832: 0xF6C8, + afii10846: 0x04D9, + afii299: 0x200E, + afii300: 0x200F, + afii301: 0x200D, + afii57381: 0x066A, + afii57388: 0x060C, + afii57392: 0x0660, + afii57393: 0x0661, + afii57394: 0x0662, + afii57395: 0x0663, + afii57396: 0x0664, + afii57397: 0x0665, + afii57398: 0x0666, + afii57399: 0x0667, + afii57400: 0x0668, + afii57401: 0x0669, + afii57403: 0x061B, + afii57407: 0x061F, + afii57409: 0x0621, + afii57410: 0x0622, + afii57411: 0x0623, + afii57412: 0x0624, + afii57413: 0x0625, + afii57414: 0x0626, + afii57415: 0x0627, + afii57416: 0x0628, + afii57417: 0x0629, + afii57418: 0x062A, + afii57419: 0x062B, + afii57420: 0x062C, + afii57421: 0x062D, + afii57422: 0x062E, + afii57423: 0x062F, + afii57424: 0x0630, + afii57425: 0x0631, + afii57426: 0x0632, + afii57427: 0x0633, + afii57428: 0x0634, + afii57429: 0x0635, + afii57430: 0x0636, + afii57431: 0x0637, + afii57432: 0x0638, + afii57433: 0x0639, + afii57434: 0x063A, + afii57440: 0x0640, + afii57441: 0x0641, + afii57442: 0x0642, + afii57443: 0x0643, + afii57444: 0x0644, + afii57445: 0x0645, + afii57446: 0x0646, + afii57448: 0x0648, + afii57449: 0x0649, + afii57450: 0x064A, + afii57451: 0x064B, + afii57452: 0x064C, + afii57453: 0x064D, + afii57454: 0x064E, + afii57455: 0x064F, + afii57456: 0x0650, + afii57457: 0x0651, + afii57458: 0x0652, + afii57470: 0x0647, + afii57505: 0x06A4, + afii57506: 0x067E, + afii57507: 0x0686, + afii57508: 0x0698, + afii57509: 0x06AF, + afii57511: 0x0679, + afii57512: 0x0688, + afii57513: 0x0691, + afii57514: 0x06BA, + afii57519: 0x06D2, + afii57534: 0x06D5, + afii57636: 0x20AA, + afii57645: 0x05BE, + afii57658: 0x05C3, + afii57664: 0x05D0, + afii57665: 0x05D1, + afii57666: 0x05D2, + afii57667: 0x05D3, + afii57668: 0x05D4, + afii57669: 0x05D5, + afii57670: 0x05D6, + afii57671: 0x05D7, + afii57672: 0x05D8, + afii57673: 0x05D9, + afii57674: 0x05DA, + afii57675: 0x05DB, + afii57676: 0x05DC, + afii57677: 0x05DD, + afii57678: 0x05DE, + afii57679: 0x05DF, + afii57680: 0x05E0, + afii57681: 0x05E1, + afii57682: 0x05E2, + afii57683: 0x05E3, + afii57684: 0x05E4, + afii57685: 0x05E5, + afii57686: 0x05E6, + afii57687: 0x05E7, + afii57688: 0x05E8, + afii57689: 0x05E9, + afii57690: 0x05EA, + afii57694: 0xFB2A, + afii57695: 0xFB2B, + afii57700: 0xFB4B, + afii57705: 0xFB1F, + afii57716: 0x05F0, + afii57717: 0x05F1, + afii57718: 0x05F2, + afii57723: 0xFB35, + afii57793: 0x05B4, + afii57794: 0x05B5, + afii57795: 0x05B6, + afii57796: 0x05BB, + afii57797: 0x05B8, + afii57798: 0x05B7, + afii57799: 0x05B0, + afii57800: 0x05B2, + afii57801: 0x05B1, + afii57802: 0x05B3, + afii57803: 0x05C2, + afii57804: 0x05C1, + afii57806: 0x05B9, + afii57807: 0x05BC, + afii57839: 0x05BD, + afii57841: 0x05BF, + afii57842: 0x05C0, + afii57929: 0x02BC, + afii61248: 0x2105, + afii61289: 0x2113, + afii61352: 0x2116, + afii61573: 0x202C, + afii61574: 0x202D, + afii61575: 0x202E, + afii61664: 0x200C, + afii63167: 0x066D, + afii64937: 0x02BD, + agrave: 0x00E0, + agujarati: 0x0A85, + agurmukhi: 0x0A05, + ahiragana: 0x3042, + ahookabove: 0x1EA3, + aibengali: 0x0990, + aibopomofo: 0x311E, + aideva: 0x0910, + aiecyrillic: 0x04D5, + aigujarati: 0x0A90, + aigurmukhi: 0x0A10, + aimatragurmukhi: 0x0A48, + ainarabic: 0x0639, + ainfinalarabic: 0xFECA, + aininitialarabic: 0xFECB, + ainmedialarabic: 0xFECC, + ainvertedbreve: 0x0203, + aivowelsignbengali: 0x09C8, + aivowelsigndeva: 0x0948, + aivowelsigngujarati: 0x0AC8, + akatakana: 0x30A2, + akatakanahalfwidth: 0xFF71, + akorean: 0x314F, + alef: 0x05D0, + alefarabic: 0x0627, + alefdageshhebrew: 0xFB30, + aleffinalarabic: 0xFE8E, + alefhamzaabovearabic: 0x0623, + alefhamzaabovefinalarabic: 0xFE84, + alefhamzabelowarabic: 0x0625, + alefhamzabelowfinalarabic: 0xFE88, + alefhebrew: 0x05D0, + aleflamedhebrew: 0xFB4F, + alefmaddaabovearabic: 0x0622, + alefmaddaabovefinalarabic: 0xFE82, + alefmaksuraarabic: 0x0649, + alefmaksurafinalarabic: 0xFEF0, + alefmaksurainitialarabic: 0xFEF3, + alefmaksuramedialarabic: 0xFEF4, + alefpatahhebrew: 0xFB2E, + alefqamatshebrew: 0xFB2F, + aleph: 0x2135, + allequal: 0x224C, + alpha: 0x03B1, + alphatonos: 0x03AC, + amacron: 0x0101, + amonospace: 0xFF41, + ampersand: 0x0026, + ampersandmonospace: 0xFF06, + ampersandsmall: 0xF726, + amsquare: 0x33C2, + anbopomofo: 0x3122, + angbopomofo: 0x3124, + angbracketleft: 0x3008, // This glyph is missing from Adobe's original list. + angbracketright: 0x3009, // This glyph is missing from Adobe's original list. + angkhankhuthai: 0x0E5A, + angle: 0x2220, + anglebracketleft: 0x3008, + anglebracketleftvertical: 0xFE3F, + anglebracketright: 0x3009, + anglebracketrightvertical: 0xFE40, + angleleft: 0x2329, + angleright: 0x232A, + angstrom: 0x212B, + anoteleia: 0x0387, + anudattadeva: 0x0952, + anusvarabengali: 0x0982, + anusvaradeva: 0x0902, + anusvaragujarati: 0x0A82, + aogonek: 0x0105, + apaatosquare: 0x3300, + aparen: 0x249C, + apostrophearmenian: 0x055A, + apostrophemod: 0x02BC, + apple: 0xF8FF, + approaches: 0x2250, + approxequal: 0x2248, + approxequalorimage: 0x2252, + approximatelyequal: 0x2245, + araeaekorean: 0x318E, + araeakorean: 0x318D, + arc: 0x2312, + arighthalfring: 0x1E9A, + aring: 0x00E5, + aringacute: 0x01FB, + aringbelow: 0x1E01, + arrowboth: 0x2194, + arrowdashdown: 0x21E3, + arrowdashleft: 0x21E0, + arrowdashright: 0x21E2, + arrowdashup: 0x21E1, + arrowdblboth: 0x21D4, + arrowdbldown: 0x21D3, + arrowdblleft: 0x21D0, + arrowdblright: 0x21D2, + arrowdblup: 0x21D1, + arrowdown: 0x2193, + arrowdownleft: 0x2199, + arrowdownright: 0x2198, + arrowdownwhite: 0x21E9, + arrowheaddownmod: 0x02C5, + arrowheadleftmod: 0x02C2, + arrowheadrightmod: 0x02C3, + arrowheadupmod: 0x02C4, + arrowhorizex: 0xF8E7, + arrowleft: 0x2190, + arrowleftdbl: 0x21D0, + arrowleftdblstroke: 0x21CD, + arrowleftoverright: 0x21C6, + arrowleftwhite: 0x21E6, + arrowright: 0x2192, + arrowrightdblstroke: 0x21CF, + arrowrightheavy: 0x279E, + arrowrightoverleft: 0x21C4, + arrowrightwhite: 0x21E8, + arrowtableft: 0x21E4, + arrowtabright: 0x21E5, + arrowup: 0x2191, + arrowupdn: 0x2195, + arrowupdnbse: 0x21A8, + arrowupdownbase: 0x21A8, + arrowupleft: 0x2196, + arrowupleftofdown: 0x21C5, + arrowupright: 0x2197, + arrowupwhite: 0x21E7, + arrowvertex: 0xF8E6, + asciicircum: 0x005E, + asciicircummonospace: 0xFF3E, + asciitilde: 0x007E, + asciitildemonospace: 0xFF5E, + ascript: 0x0251, + ascriptturned: 0x0252, + asmallhiragana: 0x3041, + asmallkatakana: 0x30A1, + asmallkatakanahalfwidth: 0xFF67, + asterisk: 0x002A, + asteriskaltonearabic: 0x066D, + asteriskarabic: 0x066D, + asteriskmath: 0x2217, + asteriskmonospace: 0xFF0A, + asterisksmall: 0xFE61, + asterism: 0x2042, + asuperior: 0xF6E9, + asymptoticallyequal: 0x2243, + at: 0x0040, + atilde: 0x00E3, + atmonospace: 0xFF20, + atsmall: 0xFE6B, + aturned: 0x0250, + aubengali: 0x0994, + aubopomofo: 0x3120, + audeva: 0x0914, + augujarati: 0x0A94, + augurmukhi: 0x0A14, + aulengthmarkbengali: 0x09D7, + aumatragurmukhi: 0x0A4C, + auvowelsignbengali: 0x09CC, + auvowelsigndeva: 0x094C, + auvowelsigngujarati: 0x0ACC, + avagrahadeva: 0x093D, + aybarmenian: 0x0561, + ayin: 0x05E2, + ayinaltonehebrew: 0xFB20, + ayinhebrew: 0x05E2, + b: 0x0062, + babengali: 0x09AC, + backslash: 0x005C, + backslashmonospace: 0xFF3C, + badeva: 0x092C, + bagujarati: 0x0AAC, + bagurmukhi: 0x0A2C, + bahiragana: 0x3070, + bahtthai: 0x0E3F, + bakatakana: 0x30D0, + bar: 0x007C, + barmonospace: 0xFF5C, + bbopomofo: 0x3105, + bcircle: 0x24D1, + bdotaccent: 0x1E03, + bdotbelow: 0x1E05, + beamedsixteenthnotes: 0x266C, + because: 0x2235, + becyrillic: 0x0431, + beharabic: 0x0628, + behfinalarabic: 0xFE90, + behinitialarabic: 0xFE91, + behiragana: 0x3079, + behmedialarabic: 0xFE92, + behmeeminitialarabic: 0xFC9F, + behmeemisolatedarabic: 0xFC08, + behnoonfinalarabic: 0xFC6D, + bekatakana: 0x30D9, + benarmenian: 0x0562, + bet: 0x05D1, + beta: 0x03B2, + betasymbolgreek: 0x03D0, + betdagesh: 0xFB31, + betdageshhebrew: 0xFB31, + bethebrew: 0x05D1, + betrafehebrew: 0xFB4C, + bhabengali: 0x09AD, + bhadeva: 0x092D, + bhagujarati: 0x0AAD, + bhagurmukhi: 0x0A2D, + bhook: 0x0253, + bihiragana: 0x3073, + bikatakana: 0x30D3, + bilabialclick: 0x0298, + bindigurmukhi: 0x0A02, + birusquare: 0x3331, + blackcircle: 0x25CF, + blackdiamond: 0x25C6, + blackdownpointingtriangle: 0x25BC, + blackleftpointingpointer: 0x25C4, + blackleftpointingtriangle: 0x25C0, + blacklenticularbracketleft: 0x3010, + blacklenticularbracketleftvertical: 0xFE3B, + blacklenticularbracketright: 0x3011, + blacklenticularbracketrightvertical: 0xFE3C, + blacklowerlefttriangle: 0x25E3, + blacklowerrighttriangle: 0x25E2, + blackrectangle: 0x25AC, + blackrightpointingpointer: 0x25BA, + blackrightpointingtriangle: 0x25B6, + blacksmallsquare: 0x25AA, + blacksmilingface: 0x263B, + blacksquare: 0x25A0, + blackstar: 0x2605, + blackupperlefttriangle: 0x25E4, + blackupperrighttriangle: 0x25E5, + blackuppointingsmalltriangle: 0x25B4, + blackuppointingtriangle: 0x25B2, + blank: 0x2423, + blinebelow: 0x1E07, + block: 0x2588, + bmonospace: 0xFF42, + bobaimaithai: 0x0E1A, + bohiragana: 0x307C, + bokatakana: 0x30DC, + bparen: 0x249D, + bqsquare: 0x33C3, + braceex: 0xF8F4, + braceleft: 0x007B, + braceleftbt: 0xF8F3, + braceleftmid: 0xF8F2, + braceleftmonospace: 0xFF5B, + braceleftsmall: 0xFE5B, + bracelefttp: 0xF8F1, + braceleftvertical: 0xFE37, + braceright: 0x007D, + bracerightbt: 0xF8FE, + bracerightmid: 0xF8FD, + bracerightmonospace: 0xFF5D, + bracerightsmall: 0xFE5C, + bracerighttp: 0xF8FC, + bracerightvertical: 0xFE38, + bracketleft: 0x005B, + bracketleftbt: 0xF8F0, + bracketleftex: 0xF8EF, + bracketleftmonospace: 0xFF3B, + bracketlefttp: 0xF8EE, + bracketright: 0x005D, + bracketrightbt: 0xF8FB, + bracketrightex: 0xF8FA, + bracketrightmonospace: 0xFF3D, + bracketrighttp: 0xF8F9, + breve: 0x02D8, + brevebelowcmb: 0x032E, + brevecmb: 0x0306, + breveinvertedbelowcmb: 0x032F, + breveinvertedcmb: 0x0311, + breveinverteddoublecmb: 0x0361, + bridgebelowcmb: 0x032A, + bridgeinvertedbelowcmb: 0x033A, + brokenbar: 0x00A6, + bstroke: 0x0180, + bsuperior: 0xF6EA, + btopbar: 0x0183, + buhiragana: 0x3076, + bukatakana: 0x30D6, + bullet: 0x2022, + bulletinverse: 0x25D8, + bulletoperator: 0x2219, + bullseye: 0x25CE, + c: 0x0063, + caarmenian: 0x056E, + cabengali: 0x099A, + cacute: 0x0107, + cadeva: 0x091A, + cagujarati: 0x0A9A, + cagurmukhi: 0x0A1A, + calsquare: 0x3388, + candrabindubengali: 0x0981, + candrabinducmb: 0x0310, + candrabindudeva: 0x0901, + candrabindugujarati: 0x0A81, + capslock: 0x21EA, + careof: 0x2105, + caron: 0x02C7, + caronbelowcmb: 0x032C, + caroncmb: 0x030C, + carriagereturn: 0x21B5, + cbopomofo: 0x3118, + ccaron: 0x010D, + ccedilla: 0x00E7, + ccedillaacute: 0x1E09, + ccircle: 0x24D2, + ccircumflex: 0x0109, + ccurl: 0x0255, + cdot: 0x010B, + cdotaccent: 0x010B, + cdsquare: 0x33C5, + cedilla: 0x00B8, + cedillacmb: 0x0327, + cent: 0x00A2, + centigrade: 0x2103, + centinferior: 0xF6DF, + centmonospace: 0xFFE0, + centoldstyle: 0xF7A2, + centsuperior: 0xF6E0, + chaarmenian: 0x0579, + chabengali: 0x099B, + chadeva: 0x091B, + chagujarati: 0x0A9B, + chagurmukhi: 0x0A1B, + chbopomofo: 0x3114, + cheabkhasiancyrillic: 0x04BD, + checkmark: 0x2713, + checyrillic: 0x0447, + chedescenderabkhasiancyrillic: 0x04BF, + chedescendercyrillic: 0x04B7, + chedieresiscyrillic: 0x04F5, + cheharmenian: 0x0573, + chekhakassiancyrillic: 0x04CC, + cheverticalstrokecyrillic: 0x04B9, + chi: 0x03C7, + chieuchacirclekorean: 0x3277, + chieuchaparenkorean: 0x3217, + chieuchcirclekorean: 0x3269, + chieuchkorean: 0x314A, + chieuchparenkorean: 0x3209, + chochangthai: 0x0E0A, + chochanthai: 0x0E08, + chochingthai: 0x0E09, + chochoethai: 0x0E0C, + chook: 0x0188, + cieucacirclekorean: 0x3276, + cieucaparenkorean: 0x3216, + cieuccirclekorean: 0x3268, + cieuckorean: 0x3148, + cieucparenkorean: 0x3208, + cieucuparenkorean: 0x321C, + circle: 0x25CB, + circlecopyrt: 0x00A9, // This glyph is missing from Adobe's original list. + circlemultiply: 0x2297, + circleot: 0x2299, + circleplus: 0x2295, + circlepostalmark: 0x3036, + circlewithlefthalfblack: 0x25D0, + circlewithrighthalfblack: 0x25D1, + circumflex: 0x02C6, + circumflexbelowcmb: 0x032D, + circumflexcmb: 0x0302, + clear: 0x2327, + clickalveolar: 0x01C2, + clickdental: 0x01C0, + clicklateral: 0x01C1, + clickretroflex: 0x01C3, + club: 0x2663, + clubsuitblack: 0x2663, + clubsuitwhite: 0x2667, + cmcubedsquare: 0x33A4, + cmonospace: 0xFF43, + cmsquaredsquare: 0x33A0, + coarmenian: 0x0581, + colon: 0x003A, + colonmonetary: 0x20A1, + colonmonospace: 0xFF1A, + colonsign: 0x20A1, + colonsmall: 0xFE55, + colontriangularhalfmod: 0x02D1, + colontriangularmod: 0x02D0, + comma: 0x002C, + commaabovecmb: 0x0313, + commaaboverightcmb: 0x0315, + commaaccent: 0xF6C3, + commaarabic: 0x060C, + commaarmenian: 0x055D, + commainferior: 0xF6E1, + commamonospace: 0xFF0C, + commareversedabovecmb: 0x0314, + commareversedmod: 0x02BD, + commasmall: 0xFE50, + commasuperior: 0xF6E2, + commaturnedabovecmb: 0x0312, + commaturnedmod: 0x02BB, + compass: 0x263C, + congruent: 0x2245, + contourintegral: 0x222E, + control: 0x2303, + controlACK: 0x0006, + controlBEL: 0x0007, + controlBS: 0x0008, + controlCAN: 0x0018, + controlCR: 0x000D, + controlDC1: 0x0011, + controlDC2: 0x0012, + controlDC3: 0x0013, + controlDC4: 0x0014, + controlDEL: 0x007F, + controlDLE: 0x0010, + controlEM: 0x0019, + controlENQ: 0x0005, + controlEOT: 0x0004, + controlESC: 0x001B, + controlETB: 0x0017, + controlETX: 0x0003, + controlFF: 0x000C, + controlFS: 0x001C, + controlGS: 0x001D, + controlHT: 0x0009, + controlLF: 0x000A, + controlNAK: 0x0015, + controlRS: 0x001E, + controlSI: 0x000F, + controlSO: 0x000E, + controlSOT: 0x0002, + controlSTX: 0x0001, + controlSUB: 0x001A, + controlSYN: 0x0016, + controlUS: 0x001F, + controlVT: 0x000B, + copyright: 0x00A9, + copyrightsans: 0xF8E9, + copyrightserif: 0xF6D9, + cornerbracketleft: 0x300C, + cornerbracketlefthalfwidth: 0xFF62, + cornerbracketleftvertical: 0xFE41, + cornerbracketright: 0x300D, + cornerbracketrighthalfwidth: 0xFF63, + cornerbracketrightvertical: 0xFE42, + corporationsquare: 0x337F, + cosquare: 0x33C7, + coverkgsquare: 0x33C6, + cparen: 0x249E, + cruzeiro: 0x20A2, + cstretched: 0x0297, + curlyand: 0x22CF, + curlyor: 0x22CE, + currency: 0x00A4, + cyrBreve: 0xF6D1, + cyrFlex: 0xF6D2, + cyrbreve: 0xF6D4, + cyrflex: 0xF6D5, + d: 0x0064, + daarmenian: 0x0564, + dabengali: 0x09A6, + dadarabic: 0x0636, + dadeva: 0x0926, + dadfinalarabic: 0xFEBE, + dadinitialarabic: 0xFEBF, + dadmedialarabic: 0xFEC0, + dagesh: 0x05BC, + dageshhebrew: 0x05BC, + dagger: 0x2020, + daggerdbl: 0x2021, + dagujarati: 0x0AA6, + dagurmukhi: 0x0A26, + dahiragana: 0x3060, + dakatakana: 0x30C0, + dalarabic: 0x062F, + dalet: 0x05D3, + daletdagesh: 0xFB33, + daletdageshhebrew: 0xFB33, + dalethebrew: 0x05D3, + dalfinalarabic: 0xFEAA, + dammaarabic: 0x064F, + dammalowarabic: 0x064F, + dammatanaltonearabic: 0x064C, + dammatanarabic: 0x064C, + danda: 0x0964, + dargahebrew: 0x05A7, + dargalefthebrew: 0x05A7, + dasiapneumatacyrilliccmb: 0x0485, + dblGrave: 0xF6D3, + dblanglebracketleft: 0x300A, + dblanglebracketleftvertical: 0xFE3D, + dblanglebracketright: 0x300B, + dblanglebracketrightvertical: 0xFE3E, + dblarchinvertedbelowcmb: 0x032B, + dblarrowleft: 0x21D4, + dblarrowright: 0x21D2, + dbldanda: 0x0965, + dblgrave: 0xF6D6, + dblgravecmb: 0x030F, + dblintegral: 0x222C, + dbllowline: 0x2017, + dbllowlinecmb: 0x0333, + dbloverlinecmb: 0x033F, + dblprimemod: 0x02BA, + dblverticalbar: 0x2016, + dblverticallineabovecmb: 0x030E, + dbopomofo: 0x3109, + dbsquare: 0x33C8, + dcaron: 0x010F, + dcedilla: 0x1E11, + dcircle: 0x24D3, + dcircumflexbelow: 0x1E13, + dcroat: 0x0111, + ddabengali: 0x09A1, + ddadeva: 0x0921, + ddagujarati: 0x0AA1, + ddagurmukhi: 0x0A21, + ddalarabic: 0x0688, + ddalfinalarabic: 0xFB89, + dddhadeva: 0x095C, + ddhabengali: 0x09A2, + ddhadeva: 0x0922, + ddhagujarati: 0x0AA2, + ddhagurmukhi: 0x0A22, + ddotaccent: 0x1E0B, + ddotbelow: 0x1E0D, + decimalseparatorarabic: 0x066B, + decimalseparatorpersian: 0x066B, + decyrillic: 0x0434, + degree: 0x00B0, + dehihebrew: 0x05AD, + dehiragana: 0x3067, + deicoptic: 0x03EF, + dekatakana: 0x30C7, + deleteleft: 0x232B, + deleteright: 0x2326, + delta: 0x03B4, + deltaturned: 0x018D, + denominatorminusonenumeratorbengali: 0x09F8, + dezh: 0x02A4, + dhabengali: 0x09A7, + dhadeva: 0x0927, + dhagujarati: 0x0AA7, + dhagurmukhi: 0x0A27, + dhook: 0x0257, + dialytikatonos: 0x0385, + dialytikatonoscmb: 0x0344, + diamond: 0x2666, + diamondsuitwhite: 0x2662, + dieresis: 0x00A8, + dieresisacute: 0xF6D7, + dieresisbelowcmb: 0x0324, + dieresiscmb: 0x0308, + dieresisgrave: 0xF6D8, + dieresistonos: 0x0385, + dihiragana: 0x3062, + dikatakana: 0x30C2, + dittomark: 0x3003, + divide: 0x00F7, + divides: 0x2223, + divisionslash: 0x2215, + djecyrillic: 0x0452, + dkshade: 0x2593, + dlinebelow: 0x1E0F, + dlsquare: 0x3397, + dmacron: 0x0111, + dmonospace: 0xFF44, + dnblock: 0x2584, + dochadathai: 0x0E0E, + dodekthai: 0x0E14, + dohiragana: 0x3069, + dokatakana: 0x30C9, + dollar: 0x0024, + dollarinferior: 0xF6E3, + dollarmonospace: 0xFF04, + dollaroldstyle: 0xF724, + dollarsmall: 0xFE69, + dollarsuperior: 0xF6E4, + dong: 0x20AB, + dorusquare: 0x3326, + dotaccent: 0x02D9, + dotaccentcmb: 0x0307, + dotbelowcmb: 0x0323, + dotbelowcomb: 0x0323, + dotkatakana: 0x30FB, + dotlessi: 0x0131, + dotlessj: 0xF6BE, + dotlessjstrokehook: 0x0284, + dotmath: 0x22C5, + dottedcircle: 0x25CC, + doubleyodpatah: 0xFB1F, + doubleyodpatahhebrew: 0xFB1F, + downtackbelowcmb: 0x031E, + downtackmod: 0x02D5, + dparen: 0x249F, + dsuperior: 0xF6EB, + dtail: 0x0256, + dtopbar: 0x018C, + duhiragana: 0x3065, + dukatakana: 0x30C5, + dz: 0x01F3, + dzaltone: 0x02A3, + dzcaron: 0x01C6, + dzcurl: 0x02A5, + dzeabkhasiancyrillic: 0x04E1, + dzecyrillic: 0x0455, + dzhecyrillic: 0x045F, + e: 0x0065, + eacute: 0x00E9, + earth: 0x2641, + ebengali: 0x098F, + ebopomofo: 0x311C, + ebreve: 0x0115, + ecandradeva: 0x090D, + ecandragujarati: 0x0A8D, + ecandravowelsigndeva: 0x0945, + ecandravowelsigngujarati: 0x0AC5, + ecaron: 0x011B, + ecedillabreve: 0x1E1D, + echarmenian: 0x0565, + echyiwnarmenian: 0x0587, + ecircle: 0x24D4, + ecircumflex: 0x00EA, + ecircumflexacute: 0x1EBF, + ecircumflexbelow: 0x1E19, + ecircumflexdotbelow: 0x1EC7, + ecircumflexgrave: 0x1EC1, + ecircumflexhookabove: 0x1EC3, + ecircumflextilde: 0x1EC5, + ecyrillic: 0x0454, + edblgrave: 0x0205, + edeva: 0x090F, + edieresis: 0x00EB, + edot: 0x0117, + edotaccent: 0x0117, + edotbelow: 0x1EB9, + eegurmukhi: 0x0A0F, + eematragurmukhi: 0x0A47, + efcyrillic: 0x0444, + egrave: 0x00E8, + egujarati: 0x0A8F, + eharmenian: 0x0567, + ehbopomofo: 0x311D, + ehiragana: 0x3048, + ehookabove: 0x1EBB, + eibopomofo: 0x311F, + eight: 0x0038, + eightarabic: 0x0668, + eightbengali: 0x09EE, + eightcircle: 0x2467, + eightcircleinversesansserif: 0x2791, + eightdeva: 0x096E, + eighteencircle: 0x2471, + eighteenparen: 0x2485, + eighteenperiod: 0x2499, + eightgujarati: 0x0AEE, + eightgurmukhi: 0x0A6E, + eighthackarabic: 0x0668, + eighthangzhou: 0x3028, + eighthnotebeamed: 0x266B, + eightideographicparen: 0x3227, + eightinferior: 0x2088, + eightmonospace: 0xFF18, + eightoldstyle: 0xF738, + eightparen: 0x247B, + eightperiod: 0x248F, + eightpersian: 0x06F8, + eightroman: 0x2177, + eightsuperior: 0x2078, + eightthai: 0x0E58, + einvertedbreve: 0x0207, + eiotifiedcyrillic: 0x0465, + ekatakana: 0x30A8, + ekatakanahalfwidth: 0xFF74, + ekonkargurmukhi: 0x0A74, + ekorean: 0x3154, + elcyrillic: 0x043B, + element: 0x2208, + elevencircle: 0x246A, + elevenparen: 0x247E, + elevenperiod: 0x2492, + elevenroman: 0x217A, + ellipsis: 0x2026, + ellipsisvertical: 0x22EE, + emacron: 0x0113, + emacronacute: 0x1E17, + emacrongrave: 0x1E15, + emcyrillic: 0x043C, + emdash: 0x2014, + emdashvertical: 0xFE31, + emonospace: 0xFF45, + emphasismarkarmenian: 0x055B, + emptyset: 0x2205, + enbopomofo: 0x3123, + encyrillic: 0x043D, + endash: 0x2013, + endashvertical: 0xFE32, + endescendercyrillic: 0x04A3, + eng: 0x014B, + engbopomofo: 0x3125, + enghecyrillic: 0x04A5, + enhookcyrillic: 0x04C8, + enspace: 0x2002, + eogonek: 0x0119, + eokorean: 0x3153, + eopen: 0x025B, + eopenclosed: 0x029A, + eopenreversed: 0x025C, + eopenreversedclosed: 0x025E, + eopenreversedhook: 0x025D, + eparen: 0x24A0, + epsilon: 0x03B5, + epsilontonos: 0x03AD, + equal: 0x003D, + equalmonospace: 0xFF1D, + equalsmall: 0xFE66, + equalsuperior: 0x207C, + equivalence: 0x2261, + erbopomofo: 0x3126, + ercyrillic: 0x0440, + ereversed: 0x0258, + ereversedcyrillic: 0x044D, + escyrillic: 0x0441, + esdescendercyrillic: 0x04AB, + esh: 0x0283, + eshcurl: 0x0286, + eshortdeva: 0x090E, + eshortvowelsigndeva: 0x0946, + eshreversedloop: 0x01AA, + eshsquatreversed: 0x0285, + esmallhiragana: 0x3047, + esmallkatakana: 0x30A7, + esmallkatakanahalfwidth: 0xFF6A, + estimated: 0x212E, + esuperior: 0xF6EC, + eta: 0x03B7, + etarmenian: 0x0568, + etatonos: 0x03AE, + eth: 0x00F0, + etilde: 0x1EBD, + etildebelow: 0x1E1B, + etnahtafoukhhebrew: 0x0591, + etnahtafoukhlefthebrew: 0x0591, + etnahtahebrew: 0x0591, + etnahtalefthebrew: 0x0591, + eturned: 0x01DD, + eukorean: 0x3161, + euro: 0x20AC, + evowelsignbengali: 0x09C7, + evowelsigndeva: 0x0947, + evowelsigngujarati: 0x0AC7, + exclam: 0x0021, + exclamarmenian: 0x055C, + exclamdbl: 0x203C, + exclamdown: 0x00A1, + exclamdownsmall: 0xF7A1, + exclammonospace: 0xFF01, + exclamsmall: 0xF721, + existential: 0x2203, + ezh: 0x0292, + ezhcaron: 0x01EF, + ezhcurl: 0x0293, + ezhreversed: 0x01B9, + ezhtail: 0x01BA, + f: 0x0066, + fadeva: 0x095E, + fagurmukhi: 0x0A5E, + fahrenheit: 0x2109, + fathaarabic: 0x064E, + fathalowarabic: 0x064E, + fathatanarabic: 0x064B, + fbopomofo: 0x3108, + fcircle: 0x24D5, + fdotaccent: 0x1E1F, + feharabic: 0x0641, + feharmenian: 0x0586, + fehfinalarabic: 0xFED2, + fehinitialarabic: 0xFED3, + fehmedialarabic: 0xFED4, + feicoptic: 0x03E5, + female: 0x2640, + ff: 0xFB00, + ffi: 0xFB03, + ffl: 0xFB04, + fi: 0xFB01, + fifteencircle: 0x246E, + fifteenparen: 0x2482, + fifteenperiod: 0x2496, + figuredash: 0x2012, + filledbox: 0x25A0, + filledrect: 0x25AC, + finalkaf: 0x05DA, + finalkafdagesh: 0xFB3A, + finalkafdageshhebrew: 0xFB3A, + finalkafhebrew: 0x05DA, + finalmem: 0x05DD, + finalmemhebrew: 0x05DD, + finalnun: 0x05DF, + finalnunhebrew: 0x05DF, + finalpe: 0x05E3, + finalpehebrew: 0x05E3, + finaltsadi: 0x05E5, + finaltsadihebrew: 0x05E5, + firsttonechinese: 0x02C9, + fisheye: 0x25C9, + fitacyrillic: 0x0473, + five: 0x0035, + fivearabic: 0x0665, + fivebengali: 0x09EB, + fivecircle: 0x2464, + fivecircleinversesansserif: 0x278E, + fivedeva: 0x096B, + fiveeighths: 0x215D, + fivegujarati: 0x0AEB, + fivegurmukhi: 0x0A6B, + fivehackarabic: 0x0665, + fivehangzhou: 0x3025, + fiveideographicparen: 0x3224, + fiveinferior: 0x2085, + fivemonospace: 0xFF15, + fiveoldstyle: 0xF735, + fiveparen: 0x2478, + fiveperiod: 0x248C, + fivepersian: 0x06F5, + fiveroman: 0x2174, + fivesuperior: 0x2075, + fivethai: 0x0E55, + fl: 0xFB02, + florin: 0x0192, + fmonospace: 0xFF46, + fmsquare: 0x3399, + fofanthai: 0x0E1F, + fofathai: 0x0E1D, + fongmanthai: 0x0E4F, + forall: 0x2200, + four: 0x0034, + fourarabic: 0x0664, + fourbengali: 0x09EA, + fourcircle: 0x2463, + fourcircleinversesansserif: 0x278D, + fourdeva: 0x096A, + fourgujarati: 0x0AEA, + fourgurmukhi: 0x0A6A, + fourhackarabic: 0x0664, + fourhangzhou: 0x3024, + fourideographicparen: 0x3223, + fourinferior: 0x2084, + fourmonospace: 0xFF14, + fournumeratorbengali: 0x09F7, + fouroldstyle: 0xF734, + fourparen: 0x2477, + fourperiod: 0x248B, + fourpersian: 0x06F4, + fourroman: 0x2173, + foursuperior: 0x2074, + fourteencircle: 0x246D, + fourteenparen: 0x2481, + fourteenperiod: 0x2495, + fourthai: 0x0E54, + fourthtonechinese: 0x02CB, + fparen: 0x24A1, + fraction: 0x2044, + franc: 0x20A3, + g: 0x0067, + gabengali: 0x0997, + gacute: 0x01F5, + gadeva: 0x0917, + gafarabic: 0x06AF, + gaffinalarabic: 0xFB93, + gafinitialarabic: 0xFB94, + gafmedialarabic: 0xFB95, + gagujarati: 0x0A97, + gagurmukhi: 0x0A17, + gahiragana: 0x304C, + gakatakana: 0x30AC, + gamma: 0x03B3, + gammalatinsmall: 0x0263, + gammasuperior: 0x02E0, + gangiacoptic: 0x03EB, + gbopomofo: 0x310D, + gbreve: 0x011F, + gcaron: 0x01E7, + gcedilla: 0x0123, + gcircle: 0x24D6, + gcircumflex: 0x011D, + gcommaaccent: 0x0123, + gdot: 0x0121, + gdotaccent: 0x0121, + gecyrillic: 0x0433, + gehiragana: 0x3052, + gekatakana: 0x30B2, + geometricallyequal: 0x2251, + gereshaccenthebrew: 0x059C, + gereshhebrew: 0x05F3, + gereshmuqdamhebrew: 0x059D, + germandbls: 0x00DF, + gershayimaccenthebrew: 0x059E, + gershayimhebrew: 0x05F4, + getamark: 0x3013, + ghabengali: 0x0998, + ghadarmenian: 0x0572, + ghadeva: 0x0918, + ghagujarati: 0x0A98, + ghagurmukhi: 0x0A18, + ghainarabic: 0x063A, + ghainfinalarabic: 0xFECE, + ghaininitialarabic: 0xFECF, + ghainmedialarabic: 0xFED0, + ghemiddlehookcyrillic: 0x0495, + ghestrokecyrillic: 0x0493, + gheupturncyrillic: 0x0491, + ghhadeva: 0x095A, + ghhagurmukhi: 0x0A5A, + ghook: 0x0260, + ghzsquare: 0x3393, + gihiragana: 0x304E, + gikatakana: 0x30AE, + gimarmenian: 0x0563, + gimel: 0x05D2, + gimeldagesh: 0xFB32, + gimeldageshhebrew: 0xFB32, + gimelhebrew: 0x05D2, + gjecyrillic: 0x0453, + glottalinvertedstroke: 0x01BE, + glottalstop: 0x0294, + glottalstopinverted: 0x0296, + glottalstopmod: 0x02C0, + glottalstopreversed: 0x0295, + glottalstopreversedmod: 0x02C1, + glottalstopreversedsuperior: 0x02E4, + glottalstopstroke: 0x02A1, + glottalstopstrokereversed: 0x02A2, + gmacron: 0x1E21, + gmonospace: 0xFF47, + gohiragana: 0x3054, + gokatakana: 0x30B4, + gparen: 0x24A2, + gpasquare: 0x33AC, + gradient: 0x2207, + grave: 0x0060, + gravebelowcmb: 0x0316, + gravecmb: 0x0300, + gravecomb: 0x0300, + gravedeva: 0x0953, + gravelowmod: 0x02CE, + gravemonospace: 0xFF40, + gravetonecmb: 0x0340, + greater: 0x003E, + greaterequal: 0x2265, + greaterequalorless: 0x22DB, + greatermonospace: 0xFF1E, + greaterorequivalent: 0x2273, + greaterorless: 0x2277, + greateroverequal: 0x2267, + greatersmall: 0xFE65, + gscript: 0x0261, + gstroke: 0x01E5, + guhiragana: 0x3050, + guillemotleft: 0x00AB, + guillemotright: 0x00BB, + guilsinglleft: 0x2039, + guilsinglright: 0x203A, + gukatakana: 0x30B0, + guramusquare: 0x3318, + gysquare: 0x33C9, + h: 0x0068, + haabkhasiancyrillic: 0x04A9, + haaltonearabic: 0x06C1, + habengali: 0x09B9, + hadescendercyrillic: 0x04B3, + hadeva: 0x0939, + hagujarati: 0x0AB9, + hagurmukhi: 0x0A39, + haharabic: 0x062D, + hahfinalarabic: 0xFEA2, + hahinitialarabic: 0xFEA3, + hahiragana: 0x306F, + hahmedialarabic: 0xFEA4, + haitusquare: 0x332A, + hakatakana: 0x30CF, + hakatakanahalfwidth: 0xFF8A, + halantgurmukhi: 0x0A4D, + hamzaarabic: 0x0621, + hamzalowarabic: 0x0621, + hangulfiller: 0x3164, + hardsigncyrillic: 0x044A, + harpoonleftbarbup: 0x21BC, + harpoonrightbarbup: 0x21C0, + hasquare: 0x33CA, + hatafpatah: 0x05B2, + hatafpatah16: 0x05B2, + hatafpatah23: 0x05B2, + hatafpatah2f: 0x05B2, + hatafpatahhebrew: 0x05B2, + hatafpatahnarrowhebrew: 0x05B2, + hatafpatahquarterhebrew: 0x05B2, + hatafpatahwidehebrew: 0x05B2, + hatafqamats: 0x05B3, + hatafqamats1b: 0x05B3, + hatafqamats28: 0x05B3, + hatafqamats34: 0x05B3, + hatafqamatshebrew: 0x05B3, + hatafqamatsnarrowhebrew: 0x05B3, + hatafqamatsquarterhebrew: 0x05B3, + hatafqamatswidehebrew: 0x05B3, + hatafsegol: 0x05B1, + hatafsegol17: 0x05B1, + hatafsegol24: 0x05B1, + hatafsegol30: 0x05B1, + hatafsegolhebrew: 0x05B1, + hatafsegolnarrowhebrew: 0x05B1, + hatafsegolquarterhebrew: 0x05B1, + hatafsegolwidehebrew: 0x05B1, + hbar: 0x0127, + hbopomofo: 0x310F, + hbrevebelow: 0x1E2B, + hcedilla: 0x1E29, + hcircle: 0x24D7, + hcircumflex: 0x0125, + hdieresis: 0x1E27, + hdotaccent: 0x1E23, + hdotbelow: 0x1E25, + he: 0x05D4, + heart: 0x2665, + heartsuitblack: 0x2665, + heartsuitwhite: 0x2661, + hedagesh: 0xFB34, + hedageshhebrew: 0xFB34, + hehaltonearabic: 0x06C1, + heharabic: 0x0647, + hehebrew: 0x05D4, + hehfinalaltonearabic: 0xFBA7, + hehfinalalttwoarabic: 0xFEEA, + hehfinalarabic: 0xFEEA, + hehhamzaabovefinalarabic: 0xFBA5, + hehhamzaaboveisolatedarabic: 0xFBA4, + hehinitialaltonearabic: 0xFBA8, + hehinitialarabic: 0xFEEB, + hehiragana: 0x3078, + hehmedialaltonearabic: 0xFBA9, + hehmedialarabic: 0xFEEC, + heiseierasquare: 0x337B, + hekatakana: 0x30D8, + hekatakanahalfwidth: 0xFF8D, + hekutaarusquare: 0x3336, + henghook: 0x0267, + herutusquare: 0x3339, + het: 0x05D7, + hethebrew: 0x05D7, + hhook: 0x0266, + hhooksuperior: 0x02B1, + hieuhacirclekorean: 0x327B, + hieuhaparenkorean: 0x321B, + hieuhcirclekorean: 0x326D, + hieuhkorean: 0x314E, + hieuhparenkorean: 0x320D, + hihiragana: 0x3072, + hikatakana: 0x30D2, + hikatakanahalfwidth: 0xFF8B, + hiriq: 0x05B4, + hiriq14: 0x05B4, + hiriq21: 0x05B4, + hiriq2d: 0x05B4, + hiriqhebrew: 0x05B4, + hiriqnarrowhebrew: 0x05B4, + hiriqquarterhebrew: 0x05B4, + hiriqwidehebrew: 0x05B4, + hlinebelow: 0x1E96, + hmonospace: 0xFF48, + hoarmenian: 0x0570, + hohipthai: 0x0E2B, + hohiragana: 0x307B, + hokatakana: 0x30DB, + hokatakanahalfwidth: 0xFF8E, + holam: 0x05B9, + holam19: 0x05B9, + holam26: 0x05B9, + holam32: 0x05B9, + holamhebrew: 0x05B9, + holamnarrowhebrew: 0x05B9, + holamquarterhebrew: 0x05B9, + holamwidehebrew: 0x05B9, + honokhukthai: 0x0E2E, + hookabovecomb: 0x0309, + hookcmb: 0x0309, + hookpalatalizedbelowcmb: 0x0321, + hookretroflexbelowcmb: 0x0322, + hoonsquare: 0x3342, + horicoptic: 0x03E9, + horizontalbar: 0x2015, + horncmb: 0x031B, + hotsprings: 0x2668, + house: 0x2302, + hparen: 0x24A3, + hsuperior: 0x02B0, + hturned: 0x0265, + huhiragana: 0x3075, + huiitosquare: 0x3333, + hukatakana: 0x30D5, + hukatakanahalfwidth: 0xFF8C, + hungarumlaut: 0x02DD, + hungarumlautcmb: 0x030B, + hv: 0x0195, + hyphen: 0x002D, + hypheninferior: 0xF6E5, + hyphenmonospace: 0xFF0D, + hyphensmall: 0xFE63, + hyphensuperior: 0xF6E6, + hyphentwo: 0x2010, + i: 0x0069, + iacute: 0x00ED, + iacyrillic: 0x044F, + ibengali: 0x0987, + ibopomofo: 0x3127, + ibreve: 0x012D, + icaron: 0x01D0, + icircle: 0x24D8, + icircumflex: 0x00EE, + icyrillic: 0x0456, + idblgrave: 0x0209, + ideographearthcircle: 0x328F, + ideographfirecircle: 0x328B, + ideographicallianceparen: 0x323F, + ideographiccallparen: 0x323A, + ideographiccentrecircle: 0x32A5, + ideographicclose: 0x3006, + ideographiccomma: 0x3001, + ideographiccommaleft: 0xFF64, + ideographiccongratulationparen: 0x3237, + ideographiccorrectcircle: 0x32A3, + ideographicearthparen: 0x322F, + ideographicenterpriseparen: 0x323D, + ideographicexcellentcircle: 0x329D, + ideographicfestivalparen: 0x3240, + ideographicfinancialcircle: 0x3296, + ideographicfinancialparen: 0x3236, + ideographicfireparen: 0x322B, + ideographichaveparen: 0x3232, + ideographichighcircle: 0x32A4, + ideographiciterationmark: 0x3005, + ideographiclaborcircle: 0x3298, + ideographiclaborparen: 0x3238, + ideographicleftcircle: 0x32A7, + ideographiclowcircle: 0x32A6, + ideographicmedicinecircle: 0x32A9, + ideographicmetalparen: 0x322E, + ideographicmoonparen: 0x322A, + ideographicnameparen: 0x3234, + ideographicperiod: 0x3002, + ideographicprintcircle: 0x329E, + ideographicreachparen: 0x3243, + ideographicrepresentparen: 0x3239, + ideographicresourceparen: 0x323E, + ideographicrightcircle: 0x32A8, + ideographicsecretcircle: 0x3299, + ideographicselfparen: 0x3242, + ideographicsocietyparen: 0x3233, + ideographicspace: 0x3000, + ideographicspecialparen: 0x3235, + ideographicstockparen: 0x3231, + ideographicstudyparen: 0x323B, + ideographicsunparen: 0x3230, + ideographicsuperviseparen: 0x323C, + ideographicwaterparen: 0x322C, + ideographicwoodparen: 0x322D, + ideographiczero: 0x3007, + ideographmetalcircle: 0x328E, + ideographmooncircle: 0x328A, + ideographnamecircle: 0x3294, + ideographsuncircle: 0x3290, + ideographwatercircle: 0x328C, + ideographwoodcircle: 0x328D, + ideva: 0x0907, + idieresis: 0x00EF, + idieresisacute: 0x1E2F, + idieresiscyrillic: 0x04E5, + idotbelow: 0x1ECB, + iebrevecyrillic: 0x04D7, + iecyrillic: 0x0435, + ieungacirclekorean: 0x3275, + ieungaparenkorean: 0x3215, + ieungcirclekorean: 0x3267, + ieungkorean: 0x3147, + ieungparenkorean: 0x3207, + igrave: 0x00EC, + igujarati: 0x0A87, + igurmukhi: 0x0A07, + ihiragana: 0x3044, + ihookabove: 0x1EC9, + iibengali: 0x0988, + iicyrillic: 0x0438, + iideva: 0x0908, + iigujarati: 0x0A88, + iigurmukhi: 0x0A08, + iimatragurmukhi: 0x0A40, + iinvertedbreve: 0x020B, + iishortcyrillic: 0x0439, + iivowelsignbengali: 0x09C0, + iivowelsigndeva: 0x0940, + iivowelsigngujarati: 0x0AC0, + ij: 0x0133, + ikatakana: 0x30A4, + ikatakanahalfwidth: 0xFF72, + ikorean: 0x3163, + ilde: 0x02DC, + iluyhebrew: 0x05AC, + imacron: 0x012B, + imacroncyrillic: 0x04E3, + imageorapproximatelyequal: 0x2253, + imatragurmukhi: 0x0A3F, + imonospace: 0xFF49, + increment: 0x2206, + infinity: 0x221E, + iniarmenian: 0x056B, + integral: 0x222B, + integralbottom: 0x2321, + integralbt: 0x2321, + integralex: 0xF8F5, + integraltop: 0x2320, + integraltp: 0x2320, + intersection: 0x2229, + intisquare: 0x3305, + invbullet: 0x25D8, + invcircle: 0x25D9, + invsmileface: 0x263B, + iocyrillic: 0x0451, + iogonek: 0x012F, + iota: 0x03B9, + iotadieresis: 0x03CA, + iotadieresistonos: 0x0390, + iotalatin: 0x0269, + iotatonos: 0x03AF, + iparen: 0x24A4, + irigurmukhi: 0x0A72, + ismallhiragana: 0x3043, + ismallkatakana: 0x30A3, + ismallkatakanahalfwidth: 0xFF68, + issharbengali: 0x09FA, + istroke: 0x0268, + isuperior: 0xF6ED, + iterationhiragana: 0x309D, + iterationkatakana: 0x30FD, + itilde: 0x0129, + itildebelow: 0x1E2D, + iubopomofo: 0x3129, + iucyrillic: 0x044E, + ivowelsignbengali: 0x09BF, + ivowelsigndeva: 0x093F, + ivowelsigngujarati: 0x0ABF, + izhitsacyrillic: 0x0475, + izhitsadblgravecyrillic: 0x0477, + j: 0x006A, + jaarmenian: 0x0571, + jabengali: 0x099C, + jadeva: 0x091C, + jagujarati: 0x0A9C, + jagurmukhi: 0x0A1C, + jbopomofo: 0x3110, + jcaron: 0x01F0, + jcircle: 0x24D9, + jcircumflex: 0x0135, + jcrossedtail: 0x029D, + jdotlessstroke: 0x025F, + jecyrillic: 0x0458, + jeemarabic: 0x062C, + jeemfinalarabic: 0xFE9E, + jeeminitialarabic: 0xFE9F, + jeemmedialarabic: 0xFEA0, + jeharabic: 0x0698, + jehfinalarabic: 0xFB8B, + jhabengali: 0x099D, + jhadeva: 0x091D, + jhagujarati: 0x0A9D, + jhagurmukhi: 0x0A1D, + jheharmenian: 0x057B, + jis: 0x3004, + jmonospace: 0xFF4A, + jparen: 0x24A5, + jsuperior: 0x02B2, + k: 0x006B, + kabashkircyrillic: 0x04A1, + kabengali: 0x0995, + kacute: 0x1E31, + kacyrillic: 0x043A, + kadescendercyrillic: 0x049B, + kadeva: 0x0915, + kaf: 0x05DB, + kafarabic: 0x0643, + kafdagesh: 0xFB3B, + kafdageshhebrew: 0xFB3B, + kaffinalarabic: 0xFEDA, + kafhebrew: 0x05DB, + kafinitialarabic: 0xFEDB, + kafmedialarabic: 0xFEDC, + kafrafehebrew: 0xFB4D, + kagujarati: 0x0A95, + kagurmukhi: 0x0A15, + kahiragana: 0x304B, + kahookcyrillic: 0x04C4, + kakatakana: 0x30AB, + kakatakanahalfwidth: 0xFF76, + kappa: 0x03BA, + kappasymbolgreek: 0x03F0, + kapyeounmieumkorean: 0x3171, + kapyeounphieuphkorean: 0x3184, + kapyeounpieupkorean: 0x3178, + kapyeounssangpieupkorean: 0x3179, + karoriisquare: 0x330D, + kashidaautoarabic: 0x0640, + kashidaautonosidebearingarabic: 0x0640, + kasmallkatakana: 0x30F5, + kasquare: 0x3384, + kasraarabic: 0x0650, + kasratanarabic: 0x064D, + kastrokecyrillic: 0x049F, + katahiraprolongmarkhalfwidth: 0xFF70, + kaverticalstrokecyrillic: 0x049D, + kbopomofo: 0x310E, + kcalsquare: 0x3389, + kcaron: 0x01E9, + kcedilla: 0x0137, + kcircle: 0x24DA, + kcommaaccent: 0x0137, + kdotbelow: 0x1E33, + keharmenian: 0x0584, + kehiragana: 0x3051, + kekatakana: 0x30B1, + kekatakanahalfwidth: 0xFF79, + kenarmenian: 0x056F, + kesmallkatakana: 0x30F6, + kgreenlandic: 0x0138, + khabengali: 0x0996, + khacyrillic: 0x0445, + khadeva: 0x0916, + khagujarati: 0x0A96, + khagurmukhi: 0x0A16, + khaharabic: 0x062E, + khahfinalarabic: 0xFEA6, + khahinitialarabic: 0xFEA7, + khahmedialarabic: 0xFEA8, + kheicoptic: 0x03E7, + khhadeva: 0x0959, + khhagurmukhi: 0x0A59, + khieukhacirclekorean: 0x3278, + khieukhaparenkorean: 0x3218, + khieukhcirclekorean: 0x326A, + khieukhkorean: 0x314B, + khieukhparenkorean: 0x320A, + khokhaithai: 0x0E02, + khokhonthai: 0x0E05, + khokhuatthai: 0x0E03, + khokhwaithai: 0x0E04, + khomutthai: 0x0E5B, + khook: 0x0199, + khorakhangthai: 0x0E06, + khzsquare: 0x3391, + kihiragana: 0x304D, + kikatakana: 0x30AD, + kikatakanahalfwidth: 0xFF77, + kiroguramusquare: 0x3315, + kiromeetorusquare: 0x3316, + kirosquare: 0x3314, + kiyeokacirclekorean: 0x326E, + kiyeokaparenkorean: 0x320E, + kiyeokcirclekorean: 0x3260, + kiyeokkorean: 0x3131, + kiyeokparenkorean: 0x3200, + kiyeoksioskorean: 0x3133, + kjecyrillic: 0x045C, + klinebelow: 0x1E35, + klsquare: 0x3398, + kmcubedsquare: 0x33A6, + kmonospace: 0xFF4B, + kmsquaredsquare: 0x33A2, + kohiragana: 0x3053, + kohmsquare: 0x33C0, + kokaithai: 0x0E01, + kokatakana: 0x30B3, + kokatakanahalfwidth: 0xFF7A, + kooposquare: 0x331E, + koppacyrillic: 0x0481, + koreanstandardsymbol: 0x327F, + koroniscmb: 0x0343, + kparen: 0x24A6, + kpasquare: 0x33AA, + ksicyrillic: 0x046F, + ktsquare: 0x33CF, + kturned: 0x029E, + kuhiragana: 0x304F, + kukatakana: 0x30AF, + kukatakanahalfwidth: 0xFF78, + kvsquare: 0x33B8, + kwsquare: 0x33BE, + l: 0x006C, + labengali: 0x09B2, + lacute: 0x013A, + ladeva: 0x0932, + lagujarati: 0x0AB2, + lagurmukhi: 0x0A32, + lakkhangyaothai: 0x0E45, + lamaleffinalarabic: 0xFEFC, + lamalefhamzaabovefinalarabic: 0xFEF8, + lamalefhamzaaboveisolatedarabic: 0xFEF7, + lamalefhamzabelowfinalarabic: 0xFEFA, + lamalefhamzabelowisolatedarabic: 0xFEF9, + lamalefisolatedarabic: 0xFEFB, + lamalefmaddaabovefinalarabic: 0xFEF6, + lamalefmaddaaboveisolatedarabic: 0xFEF5, + lamarabic: 0x0644, + lambda: 0x03BB, + lambdastroke: 0x019B, + lamed: 0x05DC, + lameddagesh: 0xFB3C, + lameddageshhebrew: 0xFB3C, + lamedhebrew: 0x05DC, + lamfinalarabic: 0xFEDE, + lamhahinitialarabic: 0xFCCA, + laminitialarabic: 0xFEDF, + lamjeeminitialarabic: 0xFCC9, + lamkhahinitialarabic: 0xFCCB, + lamlamhehisolatedarabic: 0xFDF2, + lammedialarabic: 0xFEE0, + lammeemhahinitialarabic: 0xFD88, + lammeeminitialarabic: 0xFCCC, + largecircle: 0x25EF, + lbar: 0x019A, + lbelt: 0x026C, + lbopomofo: 0x310C, + lcaron: 0x013E, + lcedilla: 0x013C, + lcircle: 0x24DB, + lcircumflexbelow: 0x1E3D, + lcommaaccent: 0x013C, + ldot: 0x0140, + ldotaccent: 0x0140, + ldotbelow: 0x1E37, + ldotbelowmacron: 0x1E39, + leftangleabovecmb: 0x031A, + lefttackbelowcmb: 0x0318, + less: 0x003C, + lessequal: 0x2264, + lessequalorgreater: 0x22DA, + lessmonospace: 0xFF1C, + lessorequivalent: 0x2272, + lessorgreater: 0x2276, + lessoverequal: 0x2266, + lesssmall: 0xFE64, + lezh: 0x026E, + lfblock: 0x258C, + lhookretroflex: 0x026D, + lira: 0x20A4, + liwnarmenian: 0x056C, + lj: 0x01C9, + ljecyrillic: 0x0459, + ll: 0xF6C0, + lladeva: 0x0933, + llagujarati: 0x0AB3, + llinebelow: 0x1E3B, + llladeva: 0x0934, + llvocalicbengali: 0x09E1, + llvocalicdeva: 0x0961, + llvocalicvowelsignbengali: 0x09E3, + llvocalicvowelsigndeva: 0x0963, + lmiddletilde: 0x026B, + lmonospace: 0xFF4C, + lmsquare: 0x33D0, + lochulathai: 0x0E2C, + logicaland: 0x2227, + logicalnot: 0x00AC, + logicalnotreversed: 0x2310, + logicalor: 0x2228, + lolingthai: 0x0E25, + longs: 0x017F, + lowlinecenterline: 0xFE4E, + lowlinecmb: 0x0332, + lowlinedashed: 0xFE4D, + lozenge: 0x25CA, + lparen: 0x24A7, + lslash: 0x0142, + lsquare: 0x2113, + lsuperior: 0xF6EE, + ltshade: 0x2591, + luthai: 0x0E26, + lvocalicbengali: 0x098C, + lvocalicdeva: 0x090C, + lvocalicvowelsignbengali: 0x09E2, + lvocalicvowelsigndeva: 0x0962, + lxsquare: 0x33D3, + m: 0x006D, + mabengali: 0x09AE, + macron: 0x00AF, + macronbelowcmb: 0x0331, + macroncmb: 0x0304, + macronlowmod: 0x02CD, + macronmonospace: 0xFFE3, + macute: 0x1E3F, + madeva: 0x092E, + magujarati: 0x0AAE, + magurmukhi: 0x0A2E, + mahapakhhebrew: 0x05A4, + mahapakhlefthebrew: 0x05A4, + mahiragana: 0x307E, + maichattawalowleftthai: 0xF895, + maichattawalowrightthai: 0xF894, + maichattawathai: 0x0E4B, + maichattawaupperleftthai: 0xF893, + maieklowleftthai: 0xF88C, + maieklowrightthai: 0xF88B, + maiekthai: 0x0E48, + maiekupperleftthai: 0xF88A, + maihanakatleftthai: 0xF884, + maihanakatthai: 0x0E31, + maitaikhuleftthai: 0xF889, + maitaikhuthai: 0x0E47, + maitholowleftthai: 0xF88F, + maitholowrightthai: 0xF88E, + maithothai: 0x0E49, + maithoupperleftthai: 0xF88D, + maitrilowleftthai: 0xF892, + maitrilowrightthai: 0xF891, + maitrithai: 0x0E4A, + maitriupperleftthai: 0xF890, + maiyamokthai: 0x0E46, + makatakana: 0x30DE, + makatakanahalfwidth: 0xFF8F, + male: 0x2642, + mansyonsquare: 0x3347, + maqafhebrew: 0x05BE, + mars: 0x2642, + masoracirclehebrew: 0x05AF, + masquare: 0x3383, + mbopomofo: 0x3107, + mbsquare: 0x33D4, + mcircle: 0x24DC, + mcubedsquare: 0x33A5, + mdotaccent: 0x1E41, + mdotbelow: 0x1E43, + meemarabic: 0x0645, + meemfinalarabic: 0xFEE2, + meeminitialarabic: 0xFEE3, + meemmedialarabic: 0xFEE4, + meemmeeminitialarabic: 0xFCD1, + meemmeemisolatedarabic: 0xFC48, + meetorusquare: 0x334D, + mehiragana: 0x3081, + meizierasquare: 0x337E, + mekatakana: 0x30E1, + mekatakanahalfwidth: 0xFF92, + mem: 0x05DE, + memdagesh: 0xFB3E, + memdageshhebrew: 0xFB3E, + memhebrew: 0x05DE, + menarmenian: 0x0574, + merkhahebrew: 0x05A5, + merkhakefulahebrew: 0x05A6, + merkhakefulalefthebrew: 0x05A6, + merkhalefthebrew: 0x05A5, + mhook: 0x0271, + mhzsquare: 0x3392, + middledotkatakanahalfwidth: 0xFF65, + middot: 0x00B7, + mieumacirclekorean: 0x3272, + mieumaparenkorean: 0x3212, + mieumcirclekorean: 0x3264, + mieumkorean: 0x3141, + mieumpansioskorean: 0x3170, + mieumparenkorean: 0x3204, + mieumpieupkorean: 0x316E, + mieumsioskorean: 0x316F, + mihiragana: 0x307F, + mikatakana: 0x30DF, + mikatakanahalfwidth: 0xFF90, + minus: 0x2212, + minusbelowcmb: 0x0320, + minuscircle: 0x2296, + minusmod: 0x02D7, + minusplus: 0x2213, + minute: 0x2032, + miribaarusquare: 0x334A, + mirisquare: 0x3349, + mlonglegturned: 0x0270, + mlsquare: 0x3396, + mmcubedsquare: 0x33A3, + mmonospace: 0xFF4D, + mmsquaredsquare: 0x339F, + mohiragana: 0x3082, + mohmsquare: 0x33C1, + mokatakana: 0x30E2, + mokatakanahalfwidth: 0xFF93, + molsquare: 0x33D6, + momathai: 0x0E21, + moverssquare: 0x33A7, + moverssquaredsquare: 0x33A8, + mparen: 0x24A8, + mpasquare: 0x33AB, + mssquare: 0x33B3, + msuperior: 0xF6EF, + mturned: 0x026F, + mu: 0x00B5, + mu1: 0x00B5, + muasquare: 0x3382, + muchgreater: 0x226B, + muchless: 0x226A, + mufsquare: 0x338C, + mugreek: 0x03BC, + mugsquare: 0x338D, + muhiragana: 0x3080, + mukatakana: 0x30E0, + mukatakanahalfwidth: 0xFF91, + mulsquare: 0x3395, + multiply: 0x00D7, + mumsquare: 0x339B, + munahhebrew: 0x05A3, + munahlefthebrew: 0x05A3, + musicalnote: 0x266A, + musicalnotedbl: 0x266B, + musicflatsign: 0x266D, + musicsharpsign: 0x266F, + mussquare: 0x33B2, + muvsquare: 0x33B6, + muwsquare: 0x33BC, + mvmegasquare: 0x33B9, + mvsquare: 0x33B7, + mwmegasquare: 0x33BF, + mwsquare: 0x33BD, + n: 0x006E, + nabengali: 0x09A8, + nabla: 0x2207, + nacute: 0x0144, + nadeva: 0x0928, + nagujarati: 0x0AA8, + nagurmukhi: 0x0A28, + nahiragana: 0x306A, + nakatakana: 0x30CA, + nakatakanahalfwidth: 0xFF85, + napostrophe: 0x0149, + nasquare: 0x3381, + nbopomofo: 0x310B, + nbspace: 0x00A0, + ncaron: 0x0148, + ncedilla: 0x0146, + ncircle: 0x24DD, + ncircumflexbelow: 0x1E4B, + ncommaaccent: 0x0146, + ndotaccent: 0x1E45, + ndotbelow: 0x1E47, + nehiragana: 0x306D, + nekatakana: 0x30CD, + nekatakanahalfwidth: 0xFF88, + newsheqelsign: 0x20AA, + nfsquare: 0x338B, + ngabengali: 0x0999, + ngadeva: 0x0919, + ngagujarati: 0x0A99, + ngagurmukhi: 0x0A19, + ngonguthai: 0x0E07, + nhiragana: 0x3093, + nhookleft: 0x0272, + nhookretroflex: 0x0273, + nieunacirclekorean: 0x326F, + nieunaparenkorean: 0x320F, + nieuncieuckorean: 0x3135, + nieuncirclekorean: 0x3261, + nieunhieuhkorean: 0x3136, + nieunkorean: 0x3134, + nieunpansioskorean: 0x3168, + nieunparenkorean: 0x3201, + nieunsioskorean: 0x3167, + nieuntikeutkorean: 0x3166, + nihiragana: 0x306B, + nikatakana: 0x30CB, + nikatakanahalfwidth: 0xFF86, + nikhahitleftthai: 0xF899, + nikhahitthai: 0x0E4D, + nine: 0x0039, + ninearabic: 0x0669, + ninebengali: 0x09EF, + ninecircle: 0x2468, + ninecircleinversesansserif: 0x2792, + ninedeva: 0x096F, + ninegujarati: 0x0AEF, + ninegurmukhi: 0x0A6F, + ninehackarabic: 0x0669, + ninehangzhou: 0x3029, + nineideographicparen: 0x3228, + nineinferior: 0x2089, + ninemonospace: 0xFF19, + nineoldstyle: 0xF739, + nineparen: 0x247C, + nineperiod: 0x2490, + ninepersian: 0x06F9, + nineroman: 0x2178, + ninesuperior: 0x2079, + nineteencircle: 0x2472, + nineteenparen: 0x2486, + nineteenperiod: 0x249A, + ninethai: 0x0E59, + nj: 0x01CC, + njecyrillic: 0x045A, + nkatakana: 0x30F3, + nkatakanahalfwidth: 0xFF9D, + nlegrightlong: 0x019E, + nlinebelow: 0x1E49, + nmonospace: 0xFF4E, + nmsquare: 0x339A, + nnabengali: 0x09A3, + nnadeva: 0x0923, + nnagujarati: 0x0AA3, + nnagurmukhi: 0x0A23, + nnnadeva: 0x0929, + nohiragana: 0x306E, + nokatakana: 0x30CE, + nokatakanahalfwidth: 0xFF89, + nonbreakingspace: 0x00A0, + nonenthai: 0x0E13, + nonuthai: 0x0E19, + noonarabic: 0x0646, + noonfinalarabic: 0xFEE6, + noonghunnaarabic: 0x06BA, + noonghunnafinalarabic: 0xFB9F, + nooninitialarabic: 0xFEE7, + noonjeeminitialarabic: 0xFCD2, + noonjeemisolatedarabic: 0xFC4B, + noonmedialarabic: 0xFEE8, + noonmeeminitialarabic: 0xFCD5, + noonmeemisolatedarabic: 0xFC4E, + noonnoonfinalarabic: 0xFC8D, + notcontains: 0x220C, + notelement: 0x2209, + notelementof: 0x2209, + notequal: 0x2260, + notgreater: 0x226F, + notgreaternorequal: 0x2271, + notgreaternorless: 0x2279, + notidentical: 0x2262, + notless: 0x226E, + notlessnorequal: 0x2270, + notparallel: 0x2226, + notprecedes: 0x2280, + notsubset: 0x2284, + notsucceeds: 0x2281, + notsuperset: 0x2285, + nowarmenian: 0x0576, + nparen: 0x24A9, + nssquare: 0x33B1, + nsuperior: 0x207F, + ntilde: 0x00F1, + nu: 0x03BD, + nuhiragana: 0x306C, + nukatakana: 0x30CC, + nukatakanahalfwidth: 0xFF87, + nuktabengali: 0x09BC, + nuktadeva: 0x093C, + nuktagujarati: 0x0ABC, + nuktagurmukhi: 0x0A3C, + numbersign: 0x0023, + numbersignmonospace: 0xFF03, + numbersignsmall: 0xFE5F, + numeralsigngreek: 0x0374, + numeralsignlowergreek: 0x0375, + numero: 0x2116, + nun: 0x05E0, + nundagesh: 0xFB40, + nundageshhebrew: 0xFB40, + nunhebrew: 0x05E0, + nvsquare: 0x33B5, + nwsquare: 0x33BB, + nyabengali: 0x099E, + nyadeva: 0x091E, + nyagujarati: 0x0A9E, + nyagurmukhi: 0x0A1E, + o: 0x006F, + oacute: 0x00F3, + oangthai: 0x0E2D, + obarred: 0x0275, + obarredcyrillic: 0x04E9, + obarreddieresiscyrillic: 0x04EB, + obengali: 0x0993, + obopomofo: 0x311B, + obreve: 0x014F, + ocandradeva: 0x0911, + ocandragujarati: 0x0A91, + ocandravowelsigndeva: 0x0949, + ocandravowelsigngujarati: 0x0AC9, + ocaron: 0x01D2, + ocircle: 0x24DE, + ocircumflex: 0x00F4, + ocircumflexacute: 0x1ED1, + ocircumflexdotbelow: 0x1ED9, + ocircumflexgrave: 0x1ED3, + ocircumflexhookabove: 0x1ED5, + ocircumflextilde: 0x1ED7, + ocyrillic: 0x043E, + odblacute: 0x0151, + odblgrave: 0x020D, + odeva: 0x0913, + odieresis: 0x00F6, + odieresiscyrillic: 0x04E7, + odotbelow: 0x1ECD, + oe: 0x0153, + oekorean: 0x315A, + ogonek: 0x02DB, + ogonekcmb: 0x0328, + ograve: 0x00F2, + ogujarati: 0x0A93, + oharmenian: 0x0585, + ohiragana: 0x304A, + ohookabove: 0x1ECF, + ohorn: 0x01A1, + ohornacute: 0x1EDB, + ohorndotbelow: 0x1EE3, + ohorngrave: 0x1EDD, + ohornhookabove: 0x1EDF, + ohorntilde: 0x1EE1, + ohungarumlaut: 0x0151, + oi: 0x01A3, + oinvertedbreve: 0x020F, + okatakana: 0x30AA, + okatakanahalfwidth: 0xFF75, + okorean: 0x3157, + olehebrew: 0x05AB, + omacron: 0x014D, + omacronacute: 0x1E53, + omacrongrave: 0x1E51, + omdeva: 0x0950, + omega: 0x03C9, + omega1: 0x03D6, + omegacyrillic: 0x0461, + omegalatinclosed: 0x0277, + omegaroundcyrillic: 0x047B, + omegatitlocyrillic: 0x047D, + omegatonos: 0x03CE, + omgujarati: 0x0AD0, + omicron: 0x03BF, + omicrontonos: 0x03CC, + omonospace: 0xFF4F, + one: 0x0031, + onearabic: 0x0661, + onebengali: 0x09E7, + onecircle: 0x2460, + onecircleinversesansserif: 0x278A, + onedeva: 0x0967, + onedotenleader: 0x2024, + oneeighth: 0x215B, + onefitted: 0xF6DC, + onegujarati: 0x0AE7, + onegurmukhi: 0x0A67, + onehackarabic: 0x0661, + onehalf: 0x00BD, + onehangzhou: 0x3021, + oneideographicparen: 0x3220, + oneinferior: 0x2081, + onemonospace: 0xFF11, + onenumeratorbengali: 0x09F4, + oneoldstyle: 0xF731, + oneparen: 0x2474, + oneperiod: 0x2488, + onepersian: 0x06F1, + onequarter: 0x00BC, + oneroman: 0x2170, + onesuperior: 0x00B9, + onethai: 0x0E51, + onethird: 0x2153, + oogonek: 0x01EB, + oogonekmacron: 0x01ED, + oogurmukhi: 0x0A13, + oomatragurmukhi: 0x0A4B, + oopen: 0x0254, + oparen: 0x24AA, + openbullet: 0x25E6, + option: 0x2325, + ordfeminine: 0x00AA, + ordmasculine: 0x00BA, + orthogonal: 0x221F, + oshortdeva: 0x0912, + oshortvowelsigndeva: 0x094A, + oslash: 0x00F8, + oslashacute: 0x01FF, + osmallhiragana: 0x3049, + osmallkatakana: 0x30A9, + osmallkatakanahalfwidth: 0xFF6B, + ostrokeacute: 0x01FF, + osuperior: 0xF6F0, + otcyrillic: 0x047F, + otilde: 0x00F5, + otildeacute: 0x1E4D, + otildedieresis: 0x1E4F, + oubopomofo: 0x3121, + overline: 0x203E, + overlinecenterline: 0xFE4A, + overlinecmb: 0x0305, + overlinedashed: 0xFE49, + overlinedblwavy: 0xFE4C, + overlinewavy: 0xFE4B, + overscore: 0x00AF, + ovowelsignbengali: 0x09CB, + ovowelsigndeva: 0x094B, + ovowelsigngujarati: 0x0ACB, + p: 0x0070, + paampssquare: 0x3380, + paasentosquare: 0x332B, + pabengali: 0x09AA, + pacute: 0x1E55, + padeva: 0x092A, + pagedown: 0x21DF, + pageup: 0x21DE, + pagujarati: 0x0AAA, + pagurmukhi: 0x0A2A, + pahiragana: 0x3071, + paiyannoithai: 0x0E2F, + pakatakana: 0x30D1, + palatalizationcyrilliccmb: 0x0484, + palochkacyrillic: 0x04C0, + pansioskorean: 0x317F, + paragraph: 0x00B6, + parallel: 0x2225, + parenleft: 0x0028, + parenleftaltonearabic: 0xFD3E, + parenleftbt: 0xF8ED, + parenleftex: 0xF8EC, + parenleftinferior: 0x208D, + parenleftmonospace: 0xFF08, + parenleftsmall: 0xFE59, + parenleftsuperior: 0x207D, + parenlefttp: 0xF8EB, + parenleftvertical: 0xFE35, + parenright: 0x0029, + parenrightaltonearabic: 0xFD3F, + parenrightbt: 0xF8F8, + parenrightex: 0xF8F7, + parenrightinferior: 0x208E, + parenrightmonospace: 0xFF09, + parenrightsmall: 0xFE5A, + parenrightsuperior: 0x207E, + parenrighttp: 0xF8F6, + parenrightvertical: 0xFE36, + partialdiff: 0x2202, + paseqhebrew: 0x05C0, + pashtahebrew: 0x0599, + pasquare: 0x33A9, + patah: 0x05B7, + patah11: 0x05B7, + patah1d: 0x05B7, + patah2a: 0x05B7, + patahhebrew: 0x05B7, + patahnarrowhebrew: 0x05B7, + patahquarterhebrew: 0x05B7, + patahwidehebrew: 0x05B7, + pazerhebrew: 0x05A1, + pbopomofo: 0x3106, + pcircle: 0x24DF, + pdotaccent: 0x1E57, + pe: 0x05E4, + pecyrillic: 0x043F, + pedagesh: 0xFB44, + pedageshhebrew: 0xFB44, + peezisquare: 0x333B, + pefinaldageshhebrew: 0xFB43, + peharabic: 0x067E, + peharmenian: 0x057A, + pehebrew: 0x05E4, + pehfinalarabic: 0xFB57, + pehinitialarabic: 0xFB58, + pehiragana: 0x307A, + pehmedialarabic: 0xFB59, + pekatakana: 0x30DA, + pemiddlehookcyrillic: 0x04A7, + perafehebrew: 0xFB4E, + percent: 0x0025, + percentarabic: 0x066A, + percentmonospace: 0xFF05, + percentsmall: 0xFE6A, + period: 0x002E, + periodarmenian: 0x0589, + periodcentered: 0x00B7, + periodhalfwidth: 0xFF61, + periodinferior: 0xF6E7, + periodmonospace: 0xFF0E, + periodsmall: 0xFE52, + periodsuperior: 0xF6E8, + perispomenigreekcmb: 0x0342, + perpendicular: 0x22A5, + perthousand: 0x2030, + peseta: 0x20A7, + pfsquare: 0x338A, + phabengali: 0x09AB, + phadeva: 0x092B, + phagujarati: 0x0AAB, + phagurmukhi: 0x0A2B, + phi: 0x03C6, + phi1: 0x03D5, + phieuphacirclekorean: 0x327A, + phieuphaparenkorean: 0x321A, + phieuphcirclekorean: 0x326C, + phieuphkorean: 0x314D, + phieuphparenkorean: 0x320C, + philatin: 0x0278, + phinthuthai: 0x0E3A, + phisymbolgreek: 0x03D5, + phook: 0x01A5, + phophanthai: 0x0E1E, + phophungthai: 0x0E1C, + phosamphaothai: 0x0E20, + pi: 0x03C0, + pieupacirclekorean: 0x3273, + pieupaparenkorean: 0x3213, + pieupcieuckorean: 0x3176, + pieupcirclekorean: 0x3265, + pieupkiyeokkorean: 0x3172, + pieupkorean: 0x3142, + pieupparenkorean: 0x3205, + pieupsioskiyeokkorean: 0x3174, + pieupsioskorean: 0x3144, + pieupsiostikeutkorean: 0x3175, + pieupthieuthkorean: 0x3177, + pieuptikeutkorean: 0x3173, + pihiragana: 0x3074, + pikatakana: 0x30D4, + pisymbolgreek: 0x03D6, + piwrarmenian: 0x0583, + plus: 0x002B, + plusbelowcmb: 0x031F, + pluscircle: 0x2295, + plusminus: 0x00B1, + plusmod: 0x02D6, + plusmonospace: 0xFF0B, + plussmall: 0xFE62, + plussuperior: 0x207A, + pmonospace: 0xFF50, + pmsquare: 0x33D8, + pohiragana: 0x307D, + pointingindexdownwhite: 0x261F, + pointingindexleftwhite: 0x261C, + pointingindexrightwhite: 0x261E, + pointingindexupwhite: 0x261D, + pokatakana: 0x30DD, + poplathai: 0x0E1B, + postalmark: 0x3012, + postalmarkface: 0x3020, + pparen: 0x24AB, + precedes: 0x227A, + prescription: 0x211E, + primemod: 0x02B9, + primereversed: 0x2035, + product: 0x220F, + projective: 0x2305, + prolongedkana: 0x30FC, + propellor: 0x2318, + propersubset: 0x2282, + propersuperset: 0x2283, + proportion: 0x2237, + proportional: 0x221D, + psi: 0x03C8, + psicyrillic: 0x0471, + psilipneumatacyrilliccmb: 0x0486, + pssquare: 0x33B0, + puhiragana: 0x3077, + pukatakana: 0x30D7, + pvsquare: 0x33B4, + pwsquare: 0x33BA, + q: 0x0071, + qadeva: 0x0958, + qadmahebrew: 0x05A8, + qafarabic: 0x0642, + qaffinalarabic: 0xFED6, + qafinitialarabic: 0xFED7, + qafmedialarabic: 0xFED8, + qamats: 0x05B8, + qamats10: 0x05B8, + qamats1a: 0x05B8, + qamats1c: 0x05B8, + qamats27: 0x05B8, + qamats29: 0x05B8, + qamats33: 0x05B8, + qamatsde: 0x05B8, + qamatshebrew: 0x05B8, + qamatsnarrowhebrew: 0x05B8, + qamatsqatanhebrew: 0x05B8, + qamatsqatannarrowhebrew: 0x05B8, + qamatsqatanquarterhebrew: 0x05B8, + qamatsqatanwidehebrew: 0x05B8, + qamatsquarterhebrew: 0x05B8, + qamatswidehebrew: 0x05B8, + qarneyparahebrew: 0x059F, + qbopomofo: 0x3111, + qcircle: 0x24E0, + qhook: 0x02A0, + qmonospace: 0xFF51, + qof: 0x05E7, + qofdagesh: 0xFB47, + qofdageshhebrew: 0xFB47, + qofhebrew: 0x05E7, + qparen: 0x24AC, + quarternote: 0x2669, + qubuts: 0x05BB, + qubuts18: 0x05BB, + qubuts25: 0x05BB, + qubuts31: 0x05BB, + qubutshebrew: 0x05BB, + qubutsnarrowhebrew: 0x05BB, + qubutsquarterhebrew: 0x05BB, + qubutswidehebrew: 0x05BB, + question: 0x003F, + questionarabic: 0x061F, + questionarmenian: 0x055E, + questiondown: 0x00BF, + questiondownsmall: 0xF7BF, + questiongreek: 0x037E, + questionmonospace: 0xFF1F, + questionsmall: 0xF73F, + quotedbl: 0x0022, + quotedblbase: 0x201E, + quotedblleft: 0x201C, + quotedblmonospace: 0xFF02, + quotedblprime: 0x301E, + quotedblprimereversed: 0x301D, + quotedblright: 0x201D, + quoteleft: 0x2018, + quoteleftreversed: 0x201B, + quotereversed: 0x201B, + quoteright: 0x2019, + quoterightn: 0x0149, + quotesinglbase: 0x201A, + quotesingle: 0x0027, + quotesinglemonospace: 0xFF07, + r: 0x0072, + raarmenian: 0x057C, + rabengali: 0x09B0, + racute: 0x0155, + radeva: 0x0930, + radical: 0x221A, + radicalex: 0xF8E5, + radoverssquare: 0x33AE, + radoverssquaredsquare: 0x33AF, + radsquare: 0x33AD, + rafe: 0x05BF, + rafehebrew: 0x05BF, + ragujarati: 0x0AB0, + ragurmukhi: 0x0A30, + rahiragana: 0x3089, + rakatakana: 0x30E9, + rakatakanahalfwidth: 0xFF97, + ralowerdiagonalbengali: 0x09F1, + ramiddlediagonalbengali: 0x09F0, + ramshorn: 0x0264, + ratio: 0x2236, + rbopomofo: 0x3116, + rcaron: 0x0159, + rcedilla: 0x0157, + rcircle: 0x24E1, + rcommaaccent: 0x0157, + rdblgrave: 0x0211, + rdotaccent: 0x1E59, + rdotbelow: 0x1E5B, + rdotbelowmacron: 0x1E5D, + referencemark: 0x203B, + reflexsubset: 0x2286, + reflexsuperset: 0x2287, + registered: 0x00AE, + registersans: 0xF8E8, + registerserif: 0xF6DA, + reharabic: 0x0631, + reharmenian: 0x0580, + rehfinalarabic: 0xFEAE, + rehiragana: 0x308C, + rekatakana: 0x30EC, + rekatakanahalfwidth: 0xFF9A, + resh: 0x05E8, + reshdageshhebrew: 0xFB48, + reshhebrew: 0x05E8, + reversedtilde: 0x223D, + reviahebrew: 0x0597, + reviamugrashhebrew: 0x0597, + revlogicalnot: 0x2310, + rfishhook: 0x027E, + rfishhookreversed: 0x027F, + rhabengali: 0x09DD, + rhadeva: 0x095D, + rho: 0x03C1, + rhook: 0x027D, + rhookturned: 0x027B, + rhookturnedsuperior: 0x02B5, + rhosymbolgreek: 0x03F1, + rhotichookmod: 0x02DE, + rieulacirclekorean: 0x3271, + rieulaparenkorean: 0x3211, + rieulcirclekorean: 0x3263, + rieulhieuhkorean: 0x3140, + rieulkiyeokkorean: 0x313A, + rieulkiyeoksioskorean: 0x3169, + rieulkorean: 0x3139, + rieulmieumkorean: 0x313B, + rieulpansioskorean: 0x316C, + rieulparenkorean: 0x3203, + rieulphieuphkorean: 0x313F, + rieulpieupkorean: 0x313C, + rieulpieupsioskorean: 0x316B, + rieulsioskorean: 0x313D, + rieulthieuthkorean: 0x313E, + rieultikeutkorean: 0x316A, + rieulyeorinhieuhkorean: 0x316D, + rightangle: 0x221F, + righttackbelowcmb: 0x0319, + righttriangle: 0x22BF, + rihiragana: 0x308A, + rikatakana: 0x30EA, + rikatakanahalfwidth: 0xFF98, + ring: 0x02DA, + ringbelowcmb: 0x0325, + ringcmb: 0x030A, + ringhalfleft: 0x02BF, + ringhalfleftarmenian: 0x0559, + ringhalfleftbelowcmb: 0x031C, + ringhalfleftcentered: 0x02D3, + ringhalfright: 0x02BE, + ringhalfrightbelowcmb: 0x0339, + ringhalfrightcentered: 0x02D2, + rinvertedbreve: 0x0213, + rittorusquare: 0x3351, + rlinebelow: 0x1E5F, + rlongleg: 0x027C, + rlonglegturned: 0x027A, + rmonospace: 0xFF52, + rohiragana: 0x308D, + rokatakana: 0x30ED, + rokatakanahalfwidth: 0xFF9B, + roruathai: 0x0E23, + rparen: 0x24AD, + rrabengali: 0x09DC, + rradeva: 0x0931, + rragurmukhi: 0x0A5C, + rreharabic: 0x0691, + rrehfinalarabic: 0xFB8D, + rrvocalicbengali: 0x09E0, + rrvocalicdeva: 0x0960, + rrvocalicgujarati: 0x0AE0, + rrvocalicvowelsignbengali: 0x09C4, + rrvocalicvowelsigndeva: 0x0944, + rrvocalicvowelsigngujarati: 0x0AC4, + rsuperior: 0xF6F1, + rtblock: 0x2590, + rturned: 0x0279, + rturnedsuperior: 0x02B4, + ruhiragana: 0x308B, + rukatakana: 0x30EB, + rukatakanahalfwidth: 0xFF99, + rupeemarkbengali: 0x09F2, + rupeesignbengali: 0x09F3, + rupiah: 0xF6DD, + ruthai: 0x0E24, + rvocalicbengali: 0x098B, + rvocalicdeva: 0x090B, + rvocalicgujarati: 0x0A8B, + rvocalicvowelsignbengali: 0x09C3, + rvocalicvowelsigndeva: 0x0943, + rvocalicvowelsigngujarati: 0x0AC3, + s: 0x0073, + sabengali: 0x09B8, + sacute: 0x015B, + sacutedotaccent: 0x1E65, + sadarabic: 0x0635, + sadeva: 0x0938, + sadfinalarabic: 0xFEBA, + sadinitialarabic: 0xFEBB, + sadmedialarabic: 0xFEBC, + sagujarati: 0x0AB8, + sagurmukhi: 0x0A38, + sahiragana: 0x3055, + sakatakana: 0x30B5, + sakatakanahalfwidth: 0xFF7B, + sallallahoualayhewasallamarabic: 0xFDFA, + samekh: 0x05E1, + samekhdagesh: 0xFB41, + samekhdageshhebrew: 0xFB41, + samekhhebrew: 0x05E1, + saraaathai: 0x0E32, + saraaethai: 0x0E41, + saraaimaimalaithai: 0x0E44, + saraaimaimuanthai: 0x0E43, + saraamthai: 0x0E33, + saraathai: 0x0E30, + saraethai: 0x0E40, + saraiileftthai: 0xF886, + saraiithai: 0x0E35, + saraileftthai: 0xF885, + saraithai: 0x0E34, + saraothai: 0x0E42, + saraueeleftthai: 0xF888, + saraueethai: 0x0E37, + saraueleftthai: 0xF887, + sarauethai: 0x0E36, + sarauthai: 0x0E38, + sarauuthai: 0x0E39, + sbopomofo: 0x3119, + scaron: 0x0161, + scarondotaccent: 0x1E67, + scedilla: 0x015F, + schwa: 0x0259, + schwacyrillic: 0x04D9, + schwadieresiscyrillic: 0x04DB, + schwahook: 0x025A, + scircle: 0x24E2, + scircumflex: 0x015D, + scommaaccent: 0x0219, + sdotaccent: 0x1E61, + sdotbelow: 0x1E63, + sdotbelowdotaccent: 0x1E69, + seagullbelowcmb: 0x033C, + second: 0x2033, + secondtonechinese: 0x02CA, + section: 0x00A7, + seenarabic: 0x0633, + seenfinalarabic: 0xFEB2, + seeninitialarabic: 0xFEB3, + seenmedialarabic: 0xFEB4, + segol: 0x05B6, + segol13: 0x05B6, + segol1f: 0x05B6, + segol2c: 0x05B6, + segolhebrew: 0x05B6, + segolnarrowhebrew: 0x05B6, + segolquarterhebrew: 0x05B6, + segoltahebrew: 0x0592, + segolwidehebrew: 0x05B6, + seharmenian: 0x057D, + sehiragana: 0x305B, + sekatakana: 0x30BB, + sekatakanahalfwidth: 0xFF7E, + semicolon: 0x003B, + semicolonarabic: 0x061B, + semicolonmonospace: 0xFF1B, + semicolonsmall: 0xFE54, + semivoicedmarkkana: 0x309C, + semivoicedmarkkanahalfwidth: 0xFF9F, + sentisquare: 0x3322, + sentosquare: 0x3323, + seven: 0x0037, + sevenarabic: 0x0667, + sevenbengali: 0x09ED, + sevencircle: 0x2466, + sevencircleinversesansserif: 0x2790, + sevendeva: 0x096D, + seveneighths: 0x215E, + sevengujarati: 0x0AED, + sevengurmukhi: 0x0A6D, + sevenhackarabic: 0x0667, + sevenhangzhou: 0x3027, + sevenideographicparen: 0x3226, + seveninferior: 0x2087, + sevenmonospace: 0xFF17, + sevenoldstyle: 0xF737, + sevenparen: 0x247A, + sevenperiod: 0x248E, + sevenpersian: 0x06F7, + sevenroman: 0x2176, + sevensuperior: 0x2077, + seventeencircle: 0x2470, + seventeenparen: 0x2484, + seventeenperiod: 0x2498, + seventhai: 0x0E57, + sfthyphen: 0x00AD, + shaarmenian: 0x0577, + shabengali: 0x09B6, + shacyrillic: 0x0448, + shaddaarabic: 0x0651, + shaddadammaarabic: 0xFC61, + shaddadammatanarabic: 0xFC5E, + shaddafathaarabic: 0xFC60, + shaddakasraarabic: 0xFC62, + shaddakasratanarabic: 0xFC5F, + shade: 0x2592, + shadedark: 0x2593, + shadelight: 0x2591, + shademedium: 0x2592, + shadeva: 0x0936, + shagujarati: 0x0AB6, + shagurmukhi: 0x0A36, + shalshelethebrew: 0x0593, + shbopomofo: 0x3115, + shchacyrillic: 0x0449, + sheenarabic: 0x0634, + sheenfinalarabic: 0xFEB6, + sheeninitialarabic: 0xFEB7, + sheenmedialarabic: 0xFEB8, + sheicoptic: 0x03E3, + sheqel: 0x20AA, + sheqelhebrew: 0x20AA, + sheva: 0x05B0, + sheva115: 0x05B0, + sheva15: 0x05B0, + sheva22: 0x05B0, + sheva2e: 0x05B0, + shevahebrew: 0x05B0, + shevanarrowhebrew: 0x05B0, + shevaquarterhebrew: 0x05B0, + shevawidehebrew: 0x05B0, + shhacyrillic: 0x04BB, + shimacoptic: 0x03ED, + shin: 0x05E9, + shindagesh: 0xFB49, + shindageshhebrew: 0xFB49, + shindageshshindot: 0xFB2C, + shindageshshindothebrew: 0xFB2C, + shindageshsindot: 0xFB2D, + shindageshsindothebrew: 0xFB2D, + shindothebrew: 0x05C1, + shinhebrew: 0x05E9, + shinshindot: 0xFB2A, + shinshindothebrew: 0xFB2A, + shinsindot: 0xFB2B, + shinsindothebrew: 0xFB2B, + shook: 0x0282, + sigma: 0x03C3, + sigma1: 0x03C2, + sigmafinal: 0x03C2, + sigmalunatesymbolgreek: 0x03F2, + sihiragana: 0x3057, + sikatakana: 0x30B7, + sikatakanahalfwidth: 0xFF7C, + siluqhebrew: 0x05BD, + siluqlefthebrew: 0x05BD, + similar: 0x223C, + sindothebrew: 0x05C2, + siosacirclekorean: 0x3274, + siosaparenkorean: 0x3214, + sioscieuckorean: 0x317E, + sioscirclekorean: 0x3266, + sioskiyeokkorean: 0x317A, + sioskorean: 0x3145, + siosnieunkorean: 0x317B, + siosparenkorean: 0x3206, + siospieupkorean: 0x317D, + siostikeutkorean: 0x317C, + six: 0x0036, + sixarabic: 0x0666, + sixbengali: 0x09EC, + sixcircle: 0x2465, + sixcircleinversesansserif: 0x278F, + sixdeva: 0x096C, + sixgujarati: 0x0AEC, + sixgurmukhi: 0x0A6C, + sixhackarabic: 0x0666, + sixhangzhou: 0x3026, + sixideographicparen: 0x3225, + sixinferior: 0x2086, + sixmonospace: 0xFF16, + sixoldstyle: 0xF736, + sixparen: 0x2479, + sixperiod: 0x248D, + sixpersian: 0x06F6, + sixroman: 0x2175, + sixsuperior: 0x2076, + sixteencircle: 0x246F, + sixteencurrencydenominatorbengali: 0x09F9, + sixteenparen: 0x2483, + sixteenperiod: 0x2497, + sixthai: 0x0E56, + slash: 0x002F, + slashmonospace: 0xFF0F, + slong: 0x017F, + slongdotaccent: 0x1E9B, + smileface: 0x263A, + smonospace: 0xFF53, + sofpasuqhebrew: 0x05C3, + softhyphen: 0x00AD, + softsigncyrillic: 0x044C, + sohiragana: 0x305D, + sokatakana: 0x30BD, + sokatakanahalfwidth: 0xFF7F, + soliduslongoverlaycmb: 0x0338, + solidusshortoverlaycmb: 0x0337, + sorusithai: 0x0E29, + sosalathai: 0x0E28, + sosothai: 0x0E0B, + sosuathai: 0x0E2A, + space: 0x0020, + spacehackarabic: 0x0020, + spade: 0x2660, + spadesuitblack: 0x2660, + spadesuitwhite: 0x2664, + sparen: 0x24AE, + squarebelowcmb: 0x033B, + squarecc: 0x33C4, + squarecm: 0x339D, + squarediagonalcrosshatchfill: 0x25A9, + squarehorizontalfill: 0x25A4, + squarekg: 0x338F, + squarekm: 0x339E, + squarekmcapital: 0x33CE, + squareln: 0x33D1, + squarelog: 0x33D2, + squaremg: 0x338E, + squaremil: 0x33D5, + squaremm: 0x339C, + squaremsquared: 0x33A1, + squareorthogonalcrosshatchfill: 0x25A6, + squareupperlefttolowerrightfill: 0x25A7, + squareupperrighttolowerleftfill: 0x25A8, + squareverticalfill: 0x25A5, + squarewhitewithsmallblack: 0x25A3, + srsquare: 0x33DB, + ssabengali: 0x09B7, + ssadeva: 0x0937, + ssagujarati: 0x0AB7, + ssangcieuckorean: 0x3149, + ssanghieuhkorean: 0x3185, + ssangieungkorean: 0x3180, + ssangkiyeokkorean: 0x3132, + ssangnieunkorean: 0x3165, + ssangpieupkorean: 0x3143, + ssangsioskorean: 0x3146, + ssangtikeutkorean: 0x3138, + ssuperior: 0xF6F2, + sterling: 0x00A3, + sterlingmonospace: 0xFFE1, + strokelongoverlaycmb: 0x0336, + strokeshortoverlaycmb: 0x0335, + subset: 0x2282, + subsetnotequal: 0x228A, + subsetorequal: 0x2286, + succeeds: 0x227B, + suchthat: 0x220B, + suhiragana: 0x3059, + sukatakana: 0x30B9, + sukatakanahalfwidth: 0xFF7D, + sukunarabic: 0x0652, + summation: 0x2211, + sun: 0x263C, + superset: 0x2283, + supersetnotequal: 0x228B, + supersetorequal: 0x2287, + svsquare: 0x33DC, + syouwaerasquare: 0x337C, + t: 0x0074, + tabengali: 0x09A4, + tackdown: 0x22A4, + tackleft: 0x22A3, + tadeva: 0x0924, + tagujarati: 0x0AA4, + tagurmukhi: 0x0A24, + taharabic: 0x0637, + tahfinalarabic: 0xFEC2, + tahinitialarabic: 0xFEC3, + tahiragana: 0x305F, + tahmedialarabic: 0xFEC4, + taisyouerasquare: 0x337D, + takatakana: 0x30BF, + takatakanahalfwidth: 0xFF80, + tatweelarabic: 0x0640, + tau: 0x03C4, + tav: 0x05EA, + tavdages: 0xFB4A, + tavdagesh: 0xFB4A, + tavdageshhebrew: 0xFB4A, + tavhebrew: 0x05EA, + tbar: 0x0167, + tbopomofo: 0x310A, + tcaron: 0x0165, + tccurl: 0x02A8, + tcedilla: 0x0163, + tcheharabic: 0x0686, + tchehfinalarabic: 0xFB7B, + tchehinitialarabic: 0xFB7C, + tchehmedialarabic: 0xFB7D, + tcircle: 0x24E3, + tcircumflexbelow: 0x1E71, + tcommaaccent: 0x0163, + tdieresis: 0x1E97, + tdotaccent: 0x1E6B, + tdotbelow: 0x1E6D, + tecyrillic: 0x0442, + tedescendercyrillic: 0x04AD, + teharabic: 0x062A, + tehfinalarabic: 0xFE96, + tehhahinitialarabic: 0xFCA2, + tehhahisolatedarabic: 0xFC0C, + tehinitialarabic: 0xFE97, + tehiragana: 0x3066, + tehjeeminitialarabic: 0xFCA1, + tehjeemisolatedarabic: 0xFC0B, + tehmarbutaarabic: 0x0629, + tehmarbutafinalarabic: 0xFE94, + tehmedialarabic: 0xFE98, + tehmeeminitialarabic: 0xFCA4, + tehmeemisolatedarabic: 0xFC0E, + tehnoonfinalarabic: 0xFC73, + tekatakana: 0x30C6, + tekatakanahalfwidth: 0xFF83, + telephone: 0x2121, + telephoneblack: 0x260E, + telishagedolahebrew: 0x05A0, + telishaqetanahebrew: 0x05A9, + tencircle: 0x2469, + tenideographicparen: 0x3229, + tenparen: 0x247D, + tenperiod: 0x2491, + tenroman: 0x2179, + tesh: 0x02A7, + tet: 0x05D8, + tetdagesh: 0xFB38, + tetdageshhebrew: 0xFB38, + tethebrew: 0x05D8, + tetsecyrillic: 0x04B5, + tevirhebrew: 0x059B, + tevirlefthebrew: 0x059B, + thabengali: 0x09A5, + thadeva: 0x0925, + thagujarati: 0x0AA5, + thagurmukhi: 0x0A25, + thalarabic: 0x0630, + thalfinalarabic: 0xFEAC, + thanthakhatlowleftthai: 0xF898, + thanthakhatlowrightthai: 0xF897, + thanthakhatthai: 0x0E4C, + thanthakhatupperleftthai: 0xF896, + theharabic: 0x062B, + thehfinalarabic: 0xFE9A, + thehinitialarabic: 0xFE9B, + thehmedialarabic: 0xFE9C, + thereexists: 0x2203, + therefore: 0x2234, + theta: 0x03B8, + theta1: 0x03D1, + thetasymbolgreek: 0x03D1, + thieuthacirclekorean: 0x3279, + thieuthaparenkorean: 0x3219, + thieuthcirclekorean: 0x326B, + thieuthkorean: 0x314C, + thieuthparenkorean: 0x320B, + thirteencircle: 0x246C, + thirteenparen: 0x2480, + thirteenperiod: 0x2494, + thonangmonthothai: 0x0E11, + thook: 0x01AD, + thophuthaothai: 0x0E12, + thorn: 0x00FE, + thothahanthai: 0x0E17, + thothanthai: 0x0E10, + thothongthai: 0x0E18, + thothungthai: 0x0E16, + thousandcyrillic: 0x0482, + thousandsseparatorarabic: 0x066C, + thousandsseparatorpersian: 0x066C, + three: 0x0033, + threearabic: 0x0663, + threebengali: 0x09E9, + threecircle: 0x2462, + threecircleinversesansserif: 0x278C, + threedeva: 0x0969, + threeeighths: 0x215C, + threegujarati: 0x0AE9, + threegurmukhi: 0x0A69, + threehackarabic: 0x0663, + threehangzhou: 0x3023, + threeideographicparen: 0x3222, + threeinferior: 0x2083, + threemonospace: 0xFF13, + threenumeratorbengali: 0x09F6, + threeoldstyle: 0xF733, + threeparen: 0x2476, + threeperiod: 0x248A, + threepersian: 0x06F3, + threequarters: 0x00BE, + threequartersemdash: 0xF6DE, + threeroman: 0x2172, + threesuperior: 0x00B3, + threethai: 0x0E53, + thzsquare: 0x3394, + tihiragana: 0x3061, + tikatakana: 0x30C1, + tikatakanahalfwidth: 0xFF81, + tikeutacirclekorean: 0x3270, + tikeutaparenkorean: 0x3210, + tikeutcirclekorean: 0x3262, + tikeutkorean: 0x3137, + tikeutparenkorean: 0x3202, + tilde: 0x02DC, + tildebelowcmb: 0x0330, + tildecmb: 0x0303, + tildecomb: 0x0303, + tildedoublecmb: 0x0360, + tildeoperator: 0x223C, + tildeoverlaycmb: 0x0334, + tildeverticalcmb: 0x033E, + timescircle: 0x2297, + tipehahebrew: 0x0596, + tipehalefthebrew: 0x0596, + tippigurmukhi: 0x0A70, + titlocyrilliccmb: 0x0483, + tiwnarmenian: 0x057F, + tlinebelow: 0x1E6F, + tmonospace: 0xFF54, + toarmenian: 0x0569, + tohiragana: 0x3068, + tokatakana: 0x30C8, + tokatakanahalfwidth: 0xFF84, + tonebarextrahighmod: 0x02E5, + tonebarextralowmod: 0x02E9, + tonebarhighmod: 0x02E6, + tonebarlowmod: 0x02E8, + tonebarmidmod: 0x02E7, + tonefive: 0x01BD, + tonesix: 0x0185, + tonetwo: 0x01A8, + tonos: 0x0384, + tonsquare: 0x3327, + topatakthai: 0x0E0F, + tortoiseshellbracketleft: 0x3014, + tortoiseshellbracketleftsmall: 0xFE5D, + tortoiseshellbracketleftvertical: 0xFE39, + tortoiseshellbracketright: 0x3015, + tortoiseshellbracketrightsmall: 0xFE5E, + tortoiseshellbracketrightvertical: 0xFE3A, + totaothai: 0x0E15, + tpalatalhook: 0x01AB, + tparen: 0x24AF, + trademark: 0x2122, + trademarksans: 0xF8EA, + trademarkserif: 0xF6DB, + tretroflexhook: 0x0288, + triagdn: 0x25BC, + triaglf: 0x25C4, + triagrt: 0x25BA, + triagup: 0x25B2, + ts: 0x02A6, + tsadi: 0x05E6, + tsadidagesh: 0xFB46, + tsadidageshhebrew: 0xFB46, + tsadihebrew: 0x05E6, + tsecyrillic: 0x0446, + tsere: 0x05B5, + tsere12: 0x05B5, + tsere1e: 0x05B5, + tsere2b: 0x05B5, + tserehebrew: 0x05B5, + tserenarrowhebrew: 0x05B5, + tserequarterhebrew: 0x05B5, + tserewidehebrew: 0x05B5, + tshecyrillic: 0x045B, + tsuperior: 0xF6F3, + ttabengali: 0x099F, + ttadeva: 0x091F, + ttagujarati: 0x0A9F, + ttagurmukhi: 0x0A1F, + tteharabic: 0x0679, + ttehfinalarabic: 0xFB67, + ttehinitialarabic: 0xFB68, + ttehmedialarabic: 0xFB69, + tthabengali: 0x09A0, + tthadeva: 0x0920, + tthagujarati: 0x0AA0, + tthagurmukhi: 0x0A20, + tturned: 0x0287, + tuhiragana: 0x3064, + tukatakana: 0x30C4, + tukatakanahalfwidth: 0xFF82, + tusmallhiragana: 0x3063, + tusmallkatakana: 0x30C3, + tusmallkatakanahalfwidth: 0xFF6F, + twelvecircle: 0x246B, + twelveparen: 0x247F, + twelveperiod: 0x2493, + twelveroman: 0x217B, + twentycircle: 0x2473, + twentyhangzhou: 0x5344, + twentyparen: 0x2487, + twentyperiod: 0x249B, + two: 0x0032, + twoarabic: 0x0662, + twobengali: 0x09E8, + twocircle: 0x2461, + twocircleinversesansserif: 0x278B, + twodeva: 0x0968, + twodotenleader: 0x2025, + twodotleader: 0x2025, + twodotleadervertical: 0xFE30, + twogujarati: 0x0AE8, + twogurmukhi: 0x0A68, + twohackarabic: 0x0662, + twohangzhou: 0x3022, + twoideographicparen: 0x3221, + twoinferior: 0x2082, + twomonospace: 0xFF12, + twonumeratorbengali: 0x09F5, + twooldstyle: 0xF732, + twoparen: 0x2475, + twoperiod: 0x2489, + twopersian: 0x06F2, + tworoman: 0x2171, + twostroke: 0x01BB, + twosuperior: 0x00B2, + twothai: 0x0E52, + twothirds: 0x2154, + u: 0x0075, + uacute: 0x00FA, + ubar: 0x0289, + ubengali: 0x0989, + ubopomofo: 0x3128, + ubreve: 0x016D, + ucaron: 0x01D4, + ucircle: 0x24E4, + ucircumflex: 0x00FB, + ucircumflexbelow: 0x1E77, + ucyrillic: 0x0443, + udattadeva: 0x0951, + udblacute: 0x0171, + udblgrave: 0x0215, + udeva: 0x0909, + udieresis: 0x00FC, + udieresisacute: 0x01D8, + udieresisbelow: 0x1E73, + udieresiscaron: 0x01DA, + udieresiscyrillic: 0x04F1, + udieresisgrave: 0x01DC, + udieresismacron: 0x01D6, + udotbelow: 0x1EE5, + ugrave: 0x00F9, + ugujarati: 0x0A89, + ugurmukhi: 0x0A09, + uhiragana: 0x3046, + uhookabove: 0x1EE7, + uhorn: 0x01B0, + uhornacute: 0x1EE9, + uhorndotbelow: 0x1EF1, + uhorngrave: 0x1EEB, + uhornhookabove: 0x1EED, + uhorntilde: 0x1EEF, + uhungarumlaut: 0x0171, + uhungarumlautcyrillic: 0x04F3, + uinvertedbreve: 0x0217, + ukatakana: 0x30A6, + ukatakanahalfwidth: 0xFF73, + ukcyrillic: 0x0479, + ukorean: 0x315C, + umacron: 0x016B, + umacroncyrillic: 0x04EF, + umacrondieresis: 0x1E7B, + umatragurmukhi: 0x0A41, + umonospace: 0xFF55, + underscore: 0x005F, + underscoredbl: 0x2017, + underscoremonospace: 0xFF3F, + underscorevertical: 0xFE33, + underscorewavy: 0xFE4F, + union: 0x222A, + universal: 0x2200, + uogonek: 0x0173, + uparen: 0x24B0, + upblock: 0x2580, + upperdothebrew: 0x05C4, + upsilon: 0x03C5, + upsilondieresis: 0x03CB, + upsilondieresistonos: 0x03B0, + upsilonlatin: 0x028A, + upsilontonos: 0x03CD, + uptackbelowcmb: 0x031D, + uptackmod: 0x02D4, + uragurmukhi: 0x0A73, + uring: 0x016F, + ushortcyrillic: 0x045E, + usmallhiragana: 0x3045, + usmallkatakana: 0x30A5, + usmallkatakanahalfwidth: 0xFF69, + ustraightcyrillic: 0x04AF, + ustraightstrokecyrillic: 0x04B1, + utilde: 0x0169, + utildeacute: 0x1E79, + utildebelow: 0x1E75, + uubengali: 0x098A, + uudeva: 0x090A, + uugujarati: 0x0A8A, + uugurmukhi: 0x0A0A, + uumatragurmukhi: 0x0A42, + uuvowelsignbengali: 0x09C2, + uuvowelsigndeva: 0x0942, + uuvowelsigngujarati: 0x0AC2, + uvowelsignbengali: 0x09C1, + uvowelsigndeva: 0x0941, + uvowelsigngujarati: 0x0AC1, + v: 0x0076, + vadeva: 0x0935, + vagujarati: 0x0AB5, + vagurmukhi: 0x0A35, + vakatakana: 0x30F7, + vav: 0x05D5, + vavdagesh: 0xFB35, + vavdagesh65: 0xFB35, + vavdageshhebrew: 0xFB35, + vavhebrew: 0x05D5, + vavholam: 0xFB4B, + vavholamhebrew: 0xFB4B, + vavvavhebrew: 0x05F0, + vavyodhebrew: 0x05F1, + vcircle: 0x24E5, + vdotbelow: 0x1E7F, + vecyrillic: 0x0432, + veharabic: 0x06A4, + vehfinalarabic: 0xFB6B, + vehinitialarabic: 0xFB6C, + vehmedialarabic: 0xFB6D, + vekatakana: 0x30F9, + venus: 0x2640, + verticalbar: 0x007C, + verticallineabovecmb: 0x030D, + verticallinebelowcmb: 0x0329, + verticallinelowmod: 0x02CC, + verticallinemod: 0x02C8, + vewarmenian: 0x057E, + vhook: 0x028B, + vikatakana: 0x30F8, + viramabengali: 0x09CD, + viramadeva: 0x094D, + viramagujarati: 0x0ACD, + visargabengali: 0x0983, + visargadeva: 0x0903, + visargagujarati: 0x0A83, + vmonospace: 0xFF56, + voarmenian: 0x0578, + voicediterationhiragana: 0x309E, + voicediterationkatakana: 0x30FE, + voicedmarkkana: 0x309B, + voicedmarkkanahalfwidth: 0xFF9E, + vokatakana: 0x30FA, + vparen: 0x24B1, + vtilde: 0x1E7D, + vturned: 0x028C, + vuhiragana: 0x3094, + vukatakana: 0x30F4, + w: 0x0077, + wacute: 0x1E83, + waekorean: 0x3159, + wahiragana: 0x308F, + wakatakana: 0x30EF, + wakatakanahalfwidth: 0xFF9C, + wakorean: 0x3158, + wasmallhiragana: 0x308E, + wasmallkatakana: 0x30EE, + wattosquare: 0x3357, + wavedash: 0x301C, + wavyunderscorevertical: 0xFE34, + wawarabic: 0x0648, + wawfinalarabic: 0xFEEE, + wawhamzaabovearabic: 0x0624, + wawhamzaabovefinalarabic: 0xFE86, + wbsquare: 0x33DD, + wcircle: 0x24E6, + wcircumflex: 0x0175, + wdieresis: 0x1E85, + wdotaccent: 0x1E87, + wdotbelow: 0x1E89, + wehiragana: 0x3091, + weierstrass: 0x2118, + wekatakana: 0x30F1, + wekorean: 0x315E, + weokorean: 0x315D, + wgrave: 0x1E81, + whitebullet: 0x25E6, + whitecircle: 0x25CB, + whitecircleinverse: 0x25D9, + whitecornerbracketleft: 0x300E, + whitecornerbracketleftvertical: 0xFE43, + whitecornerbracketright: 0x300F, + whitecornerbracketrightvertical: 0xFE44, + whitediamond: 0x25C7, + whitediamondcontainingblacksmalldiamond: 0x25C8, + whitedownpointingsmalltriangle: 0x25BF, + whitedownpointingtriangle: 0x25BD, + whiteleftpointingsmalltriangle: 0x25C3, + whiteleftpointingtriangle: 0x25C1, + whitelenticularbracketleft: 0x3016, + whitelenticularbracketright: 0x3017, + whiterightpointingsmalltriangle: 0x25B9, + whiterightpointingtriangle: 0x25B7, + whitesmallsquare: 0x25AB, + whitesmilingface: 0x263A, + whitesquare: 0x25A1, + whitestar: 0x2606, + whitetelephone: 0x260F, + whitetortoiseshellbracketleft: 0x3018, + whitetortoiseshellbracketright: 0x3019, + whiteuppointingsmalltriangle: 0x25B5, + whiteuppointingtriangle: 0x25B3, + wihiragana: 0x3090, + wikatakana: 0x30F0, + wikorean: 0x315F, + wmonospace: 0xFF57, + wohiragana: 0x3092, + wokatakana: 0x30F2, + wokatakanahalfwidth: 0xFF66, + won: 0x20A9, + wonmonospace: 0xFFE6, + wowaenthai: 0x0E27, + wparen: 0x24B2, + wring: 0x1E98, + wsuperior: 0x02B7, + wturned: 0x028D, + wynn: 0x01BF, + x: 0x0078, + xabovecmb: 0x033D, + xbopomofo: 0x3112, + xcircle: 0x24E7, + xdieresis: 0x1E8D, + xdotaccent: 0x1E8B, + xeharmenian: 0x056D, + xi: 0x03BE, + xmonospace: 0xFF58, + xparen: 0x24B3, + xsuperior: 0x02E3, + y: 0x0079, + yaadosquare: 0x334E, + yabengali: 0x09AF, + yacute: 0x00FD, + yadeva: 0x092F, + yaekorean: 0x3152, + yagujarati: 0x0AAF, + yagurmukhi: 0x0A2F, + yahiragana: 0x3084, + yakatakana: 0x30E4, + yakatakanahalfwidth: 0xFF94, + yakorean: 0x3151, + yamakkanthai: 0x0E4E, + yasmallhiragana: 0x3083, + yasmallkatakana: 0x30E3, + yasmallkatakanahalfwidth: 0xFF6C, + yatcyrillic: 0x0463, + ycircle: 0x24E8, + ycircumflex: 0x0177, + ydieresis: 0x00FF, + ydotaccent: 0x1E8F, + ydotbelow: 0x1EF5, + yeharabic: 0x064A, + yehbarreearabic: 0x06D2, + yehbarreefinalarabic: 0xFBAF, + yehfinalarabic: 0xFEF2, + yehhamzaabovearabic: 0x0626, + yehhamzaabovefinalarabic: 0xFE8A, + yehhamzaaboveinitialarabic: 0xFE8B, + yehhamzaabovemedialarabic: 0xFE8C, + yehinitialarabic: 0xFEF3, + yehmedialarabic: 0xFEF4, + yehmeeminitialarabic: 0xFCDD, + yehmeemisolatedarabic: 0xFC58, + yehnoonfinalarabic: 0xFC94, + yehthreedotsbelowarabic: 0x06D1, + yekorean: 0x3156, + yen: 0x00A5, + yenmonospace: 0xFFE5, + yeokorean: 0x3155, + yeorinhieuhkorean: 0x3186, + yerahbenyomohebrew: 0x05AA, + yerahbenyomolefthebrew: 0x05AA, + yericyrillic: 0x044B, + yerudieresiscyrillic: 0x04F9, + yesieungkorean: 0x3181, + yesieungpansioskorean: 0x3183, + yesieungsioskorean: 0x3182, + yetivhebrew: 0x059A, + ygrave: 0x1EF3, + yhook: 0x01B4, + yhookabove: 0x1EF7, + yiarmenian: 0x0575, + yicyrillic: 0x0457, + yikorean: 0x3162, + yinyang: 0x262F, + yiwnarmenian: 0x0582, + ymonospace: 0xFF59, + yod: 0x05D9, + yoddagesh: 0xFB39, + yoddageshhebrew: 0xFB39, + yodhebrew: 0x05D9, + yodyodhebrew: 0x05F2, + yodyodpatahhebrew: 0xFB1F, + yohiragana: 0x3088, + yoikorean: 0x3189, + yokatakana: 0x30E8, + yokatakanahalfwidth: 0xFF96, + yokorean: 0x315B, + yosmallhiragana: 0x3087, + yosmallkatakana: 0x30E7, + yosmallkatakanahalfwidth: 0xFF6E, + yotgreek: 0x03F3, + yoyaekorean: 0x3188, + yoyakorean: 0x3187, + yoyakthai: 0x0E22, + yoyingthai: 0x0E0D, + yparen: 0x24B4, + ypogegrammeni: 0x037A, + ypogegrammenigreekcmb: 0x0345, + yr: 0x01A6, + yring: 0x1E99, + ysuperior: 0x02B8, + ytilde: 0x1EF9, + yturned: 0x028E, + yuhiragana: 0x3086, + yuikorean: 0x318C, + yukatakana: 0x30E6, + yukatakanahalfwidth: 0xFF95, + yukorean: 0x3160, + yusbigcyrillic: 0x046B, + yusbigiotifiedcyrillic: 0x046D, + yuslittlecyrillic: 0x0467, + yuslittleiotifiedcyrillic: 0x0469, + yusmallhiragana: 0x3085, + yusmallkatakana: 0x30E5, + yusmallkatakanahalfwidth: 0xFF6D, + yuyekorean: 0x318B, + yuyeokorean: 0x318A, + yyabengali: 0x09DF, + yyadeva: 0x095F, + z: 0x007A, + zaarmenian: 0x0566, + zacute: 0x017A, + zadeva: 0x095B, + zagurmukhi: 0x0A5B, + zaharabic: 0x0638, + zahfinalarabic: 0xFEC6, + zahinitialarabic: 0xFEC7, + zahiragana: 0x3056, + zahmedialarabic: 0xFEC8, + zainarabic: 0x0632, + zainfinalarabic: 0xFEB0, + zakatakana: 0x30B6, + zaqefgadolhebrew: 0x0595, + zaqefqatanhebrew: 0x0594, + zarqahebrew: 0x0598, + zayin: 0x05D6, + zayindagesh: 0xFB36, + zayindageshhebrew: 0xFB36, + zayinhebrew: 0x05D6, + zbopomofo: 0x3117, + zcaron: 0x017E, + zcircle: 0x24E9, + zcircumflex: 0x1E91, + zcurl: 0x0291, + zdot: 0x017C, + zdotaccent: 0x017C, + zdotbelow: 0x1E93, + zecyrillic: 0x0437, + zedescendercyrillic: 0x0499, + zedieresiscyrillic: 0x04DF, + zehiragana: 0x305C, + zekatakana: 0x30BC, + zero: 0x0030, + zeroarabic: 0x0660, + zerobengali: 0x09E6, + zerodeva: 0x0966, + zerogujarati: 0x0AE6, + zerogurmukhi: 0x0A66, + zerohackarabic: 0x0660, + zeroinferior: 0x2080, + zeromonospace: 0xFF10, + zerooldstyle: 0xF730, + zeropersian: 0x06F0, + zerosuperior: 0x2070, + zerothai: 0x0E50, + zerowidthjoiner: 0xFEFF, + zerowidthnonjoiner: 0x200C, + zerowidthspace: 0x200B, + zeta: 0x03B6, + zhbopomofo: 0x3113, + zhearmenian: 0x056A, + zhebrevecyrillic: 0x04C2, + zhecyrillic: 0x0436, + zhedescendercyrillic: 0x0497, + zhedieresiscyrillic: 0x04DD, + zihiragana: 0x3058, + zikatakana: 0x30B8, + zinorhebrew: 0x05AE, + zlinebelow: 0x1E95, + zmonospace: 0xFF5A, + zohiragana: 0x305E, + zokatakana: 0x30BE, + zparen: 0x24B5, + zretroflexhook: 0x0290, + zstroke: 0x01B6, + zuhiragana: 0x305A, + zukatakana: 0x30BA, + '.notdef': 0x0000 +}; + +var DingbatsGlyphsUnicode = { + space: 0x0020, + a1: 0x2701, + a2: 0x2702, + a202: 0x2703, + a3: 0x2704, + a4: 0x260E, + a5: 0x2706, + a119: 0x2707, + a118: 0x2708, + a117: 0x2709, + a11: 0x261B, + a12: 0x261E, + a13: 0x270C, + a14: 0x270D, + a15: 0x270E, + a16: 0x270F, + a105: 0x2710, + a17: 0x2711, + a18: 0x2712, + a19: 0x2713, + a20: 0x2714, + a21: 0x2715, + a22: 0x2716, + a23: 0x2717, + a24: 0x2718, + a25: 0x2719, + a26: 0x271A, + a27: 0x271B, + a28: 0x271C, + a6: 0x271D, + a7: 0x271E, + a8: 0x271F, + a9: 0x2720, + a10: 0x2721, + a29: 0x2722, + a30: 0x2723, + a31: 0x2724, + a32: 0x2725, + a33: 0x2726, + a34: 0x2727, + a35: 0x2605, + a36: 0x2729, + a37: 0x272A, + a38: 0x272B, + a39: 0x272C, + a40: 0x272D, + a41: 0x272E, + a42: 0x272F, + a43: 0x2730, + a44: 0x2731, + a45: 0x2732, + a46: 0x2733, + a47: 0x2734, + a48: 0x2735, + a49: 0x2736, + a50: 0x2737, + a51: 0x2738, + a52: 0x2739, + a53: 0x273A, + a54: 0x273B, + a55: 0x273C, + a56: 0x273D, + a57: 0x273E, + a58: 0x273F, + a59: 0x2740, + a60: 0x2741, + a61: 0x2742, + a62: 0x2743, + a63: 0x2744, + a64: 0x2745, + a65: 0x2746, + a66: 0x2747, + a67: 0x2748, + a68: 0x2749, + a69: 0x274A, + a70: 0x274B, + a71: 0x25CF, + a72: 0x274D, + a73: 0x25A0, + a74: 0x274F, + a203: 0x2750, + a75: 0x2751, + a204: 0x2752, + a76: 0x25B2, + a77: 0x25BC, + a78: 0x25C6, + a79: 0x2756, + a81: 0x25D7, + a82: 0x2758, + a83: 0x2759, + a84: 0x275A, + a97: 0x275B, + a98: 0x275C, + a99: 0x275D, + a100: 0x275E, + a101: 0x2761, + a102: 0x2762, + a103: 0x2763, + a104: 0x2764, + a106: 0x2765, + a107: 0x2766, + a108: 0x2767, + a112: 0x2663, + a111: 0x2666, + a110: 0x2665, + a109: 0x2660, + a120: 0x2460, + a121: 0x2461, + a122: 0x2462, + a123: 0x2463, + a124: 0x2464, + a125: 0x2465, + a126: 0x2466, + a127: 0x2467, + a128: 0x2468, + a129: 0x2469, + a130: 0x2776, + a131: 0x2777, + a132: 0x2778, + a133: 0x2779, + a134: 0x277A, + a135: 0x277B, + a136: 0x277C, + a137: 0x277D, + a138: 0x277E, + a139: 0x277F, + a140: 0x2780, + a141: 0x2781, + a142: 0x2782, + a143: 0x2783, + a144: 0x2784, + a145: 0x2785, + a146: 0x2786, + a147: 0x2787, + a148: 0x2788, + a149: 0x2789, + a150: 0x278A, + a151: 0x278B, + a152: 0x278C, + a153: 0x278D, + a154: 0x278E, + a155: 0x278F, + a156: 0x2790, + a157: 0x2791, + a158: 0x2792, + a159: 0x2793, + a160: 0x2794, + a161: 0x2192, + a163: 0x2194, + a164: 0x2195, + a196: 0x2798, + a165: 0x2799, + a192: 0x279A, + a166: 0x279B, + a167: 0x279C, + a168: 0x279D, + a169: 0x279E, + a170: 0x279F, + a171: 0x27A0, + a172: 0x27A1, + a173: 0x27A2, + a162: 0x27A3, + a174: 0x27A4, + a175: 0x27A5, + a176: 0x27A6, + a177: 0x27A7, + a178: 0x27A8, + a179: 0x27A9, + a193: 0x27AA, + a180: 0x27AB, + a199: 0x27AC, + a181: 0x27AD, + a200: 0x27AE, + a182: 0x27AF, + a201: 0x27B1, + a183: 0x27B2, + a184: 0x27B3, + a197: 0x27B4, + a185: 0x27B5, + a194: 0x27B6, + a198: 0x27B7, + a186: 0x27B8, + a195: 0x27B9, + a187: 0x27BA, + a188: 0x27BB, + a189: 0x27BC, + a190: 0x27BD, + a191: 0x27BE, + a89: 0x2768, // 0xF8D7 + a90: 0x2769, // 0xF8D8 + a93: 0x276A, // 0xF8D9 + a94: 0x276B, // 0xF8DA + a91: 0x276C, // 0xF8DB + a92: 0x276D, // 0xF8DC + a205: 0x276E, // 0xF8DD + a85: 0x276F, // 0xF8DE + a206: 0x2770, // 0xF8DF + a86: 0x2771, // 0xF8E0 + a87: 0x2772, // 0xF8E1 + a88: 0x2773, // 0xF8E2 + a95: 0x2774, // 0xF8E3 + a96: 0x2775, // 0xF8E4 + '.notdef': 0x0000 +}; + + +var PDFImage = (function PDFImageClosure() { + /** + * Decode the image in the main thread if it supported. Resovles the promise + * when the image data is ready. + */ + function handleImageData(handler, xref, res, image) { + if (image instanceof JpegStream && image.isNativelyDecodable(xref, res)) { + // For natively supported jpegs send them to the main thread for decoding. + var dict = image.dict; + var colorSpace = dict.get('ColorSpace', 'CS'); + colorSpace = ColorSpace.parse(colorSpace, xref, res); + var numComps = colorSpace.numComps; + var decodePromise = handler.sendWithPromise('JpegDecode', + [image.getIR(), numComps]); + return decodePromise.then(function (message) { + var data = message.data; + return new Stream(data, 0, data.length, image.dict); + }); + } else { + return Promise.resolve(image); + } + } + + /** + * Decode and clamp a value. The formula is different from the spec because we + * don't decode to float range [0,1], we decode it in the [0,max] range. + */ + function decodeAndClamp(value, addend, coefficient, max) { + value = addend + value * coefficient; + // Clamp the value to the range + return (value < 0 ? 0 : (value > max ? max : value)); + } + + function PDFImage(xref, res, image, inline, smask, mask, isMask) { + this.image = image; + var dict = image.dict; + if (dict.has('Filter')) { + var filter = dict.get('Filter').name; + if (filter === 'JPXDecode') { + var jpxImage = new JpxImage(); + jpxImage.parseImageProperties(image.stream); + image.stream.reset(); + image.bitsPerComponent = jpxImage.bitsPerComponent; + image.numComps = jpxImage.componentsCount; + } else if (filter === 'JBIG2Decode') { + image.bitsPerComponent = 1; + image.numComps = 1; + } + } + // TODO cache rendered images? + + this.width = dict.get('Width', 'W'); + this.height = dict.get('Height', 'H'); + + if (this.width < 1 || this.height < 1) { + error('Invalid image width: ' + this.width + ' or height: ' + + this.height); + } + + this.interpolate = dict.get('Interpolate', 'I') || false; + this.imageMask = dict.get('ImageMask', 'IM') || false; + this.matte = dict.get('Matte') || false; + + var bitsPerComponent = image.bitsPerComponent; + if (!bitsPerComponent) { + bitsPerComponent = dict.get('BitsPerComponent', 'BPC'); + if (!bitsPerComponent) { + if (this.imageMask) { + bitsPerComponent = 1; + } else { + error('Bits per component missing in image: ' + this.imageMask); + } + } + } + this.bpc = bitsPerComponent; + + if (!this.imageMask) { + var colorSpace = dict.get('ColorSpace', 'CS'); + if (!colorSpace) { + info('JPX images (which do not require color spaces)'); + switch (image.numComps) { + case 1: + colorSpace = Name.get('DeviceGray'); + break; + case 3: + colorSpace = Name.get('DeviceRGB'); + break; + case 4: + colorSpace = Name.get('DeviceCMYK'); + break; + default: + error('JPX images with ' + this.numComps + + ' color components not supported.'); + } + } + this.colorSpace = ColorSpace.parse(colorSpace, xref, res); + this.numComps = this.colorSpace.numComps; + } + + this.decode = dict.get('Decode', 'D'); + this.needsDecode = false; + if (this.decode && + ((this.colorSpace && !this.colorSpace.isDefaultDecode(this.decode)) || + (isMask && !ColorSpace.isDefaultDecode(this.decode, 1)))) { + this.needsDecode = true; + // Do some preprocessing to avoid more math. + var max = (1 << bitsPerComponent) - 1; + this.decodeCoefficients = []; + this.decodeAddends = []; + for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) { + var dmin = this.decode[i]; + var dmax = this.decode[i + 1]; + this.decodeCoefficients[j] = dmax - dmin; + this.decodeAddends[j] = max * dmin; + } + } + + if (smask) { + this.smask = new PDFImage(xref, res, smask, false); + } else if (mask) { + if (isStream(mask)) { + this.mask = new PDFImage(xref, res, mask, false, null, null, true); + } else { + // Color key mask (just an array). + this.mask = mask; + } + } + } + /** + * Handles processing of image data and returns the Promise that is resolved + * with a PDFImage when the image is ready to be used. + */ + PDFImage.buildImage = function PDFImage_buildImage(handler, xref, + res, image, inline) { + var imagePromise = handleImageData(handler, xref, res, image); + var smaskPromise; + var maskPromise; + + var smask = image.dict.get('SMask'); + var mask = image.dict.get('Mask'); + + if (smask) { + smaskPromise = handleImageData(handler, xref, res, smask); + maskPromise = Promise.resolve(null); + } else { + smaskPromise = Promise.resolve(null); + if (mask) { + if (isStream(mask)) { + maskPromise = handleImageData(handler, xref, res, mask); + } else if (isArray(mask)) { + maskPromise = Promise.resolve(mask); + } else { + warn('Unsupported mask format.'); + maskPromise = Promise.resolve(null); + } + } else { + maskPromise = Promise.resolve(null); + } + } + return Promise.all([imagePromise, smaskPromise, maskPromise]).then( + function(results) { + var imageData = results[0]; + var smaskData = results[1]; + var maskData = results[2]; + return new PDFImage(xref, res, imageData, inline, smaskData, maskData); + }); + }; + + /** + * Resize an image using the nearest neighbor algorithm. Currently only + * supports one and three component images. + * @param {TypedArray} pixels The original image with one component. + * @param {Number} bpc Number of bits per component. + * @param {Number} components Number of color components, 1 or 3 is supported. + * @param {Number} w1 Original width. + * @param {Number} h1 Original height. + * @param {Number} w2 New width. + * @param {Number} h2 New height. + * @param {TypedArray} dest (Optional) The destination buffer. + * @param {Number} alpha01 (Optional) Size reserved for the alpha channel. + * @return {TypedArray} Resized image data. + */ + PDFImage.resize = function PDFImage_resize(pixels, bpc, components, + w1, h1, w2, h2, dest, alpha01) { + + if (components !== 1 && components !== 3) { + error('Unsupported component count for resizing.'); + } + + var length = w2 * h2 * components; + var temp = dest ? dest : (bpc <= 8 ? new Uint8Array(length) : + (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length))); + var xRatio = w1 / w2; + var yRatio = h1 / h2; + var i, j, py, newIndex = 0, oldIndex; + var xScaled = new Uint16Array(w2); + var w1Scanline = w1 * components; + if (alpha01 !== 1) { + alpha01 = 0; + } + + for (j = 0; j < w2; j++) { + xScaled[j] = Math.floor(j * xRatio) * components; + } + + if (components === 1) { + for (i = 0; i < h2; i++) { + py = Math.floor(i * yRatio) * w1Scanline; + for (j = 0; j < w2; j++) { + oldIndex = py + xScaled[j]; + temp[newIndex++] = pixels[oldIndex]; + } + } + } else if (components === 3) { + for (i = 0; i < h2; i++) { + py = Math.floor(i * yRatio) * w1Scanline; + for (j = 0; j < w2; j++) { + oldIndex = py + xScaled[j]; + temp[newIndex++] = pixels[oldIndex++]; + temp[newIndex++] = pixels[oldIndex++]; + temp[newIndex++] = pixels[oldIndex++]; + newIndex += alpha01; + } + } + } + return temp; + }; + + PDFImage.createMask = + function PDFImage_createMask(imgArray, width, height, + imageIsFromDecodeStream, inverseDecode) { + + // |imgArray| might not contain full data for every pixel of the mask, so + // we need to distinguish between |computedLength| and |actualLength|. + // In particular, if inverseDecode is true, then the array we return must + // have a length of |computedLength|. + + var computedLength = ((width + 7) >> 3) * height; + var actualLength = imgArray.byteLength; + var haveFullData = computedLength === actualLength; + var data, i; + + if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) { + // imgArray came from a DecodeStream and its data is in an appropriate + // form, so we can just transfer it. + data = imgArray; + } else if (!inverseDecode) { + data = new Uint8Array(actualLength); + data.set(imgArray); + } else { + data = new Uint8Array(computedLength); + data.set(imgArray); + for (i = actualLength; i < computedLength; i++) { + data[i] = 0xff; + } + } + + // If necessary, invert the original mask data (but not any extra we might + // have added above). It's safe to modify the array -- whether it's the + // original or a copy, we're about to transfer it anyway, so nothing else + // in this thread can be relying on its contents. + if (inverseDecode) { + for (i = 0; i < actualLength; i++) { + data[i] = ~data[i]; + } + } + + return {data: data, width: width, height: height}; + }; + + PDFImage.prototype = { + get drawWidth() { + return Math.max(this.width, + this.smask && this.smask.width || 0, + this.mask && this.mask.width || 0); + }, + + get drawHeight() { + return Math.max(this.height, + this.smask && this.smask.height || 0, + this.mask && this.mask.height || 0); + }, + + decodeBuffer: function PDFImage_decodeBuffer(buffer) { + var bpc = this.bpc; + var numComps = this.numComps; + + var decodeAddends = this.decodeAddends; + var decodeCoefficients = this.decodeCoefficients; + var max = (1 << bpc) - 1; + var i, ii; + + if (bpc === 1) { + // If the buffer needed decode that means it just needs to be inverted. + for (i = 0, ii = buffer.length; i < ii; i++) { + buffer[i] = +!(buffer[i]); + } + return; + } + var index = 0; + for (i = 0, ii = this.width * this.height; i < ii; i++) { + for (var j = 0; j < numComps; j++) { + buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j], + decodeCoefficients[j], max); + index++; + } + } + }, + + getComponents: function PDFImage_getComponents(buffer) { + var bpc = this.bpc; + + // This image doesn't require any extra work. + if (bpc === 8) { + return buffer; + } + + var width = this.width; + var height = this.height; + var numComps = this.numComps; + + var length = width * height * numComps; + var bufferPos = 0; + var output = (bpc <= 8 ? new Uint8Array(length) : + (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length))); + var rowComps = width * numComps; + + var max = (1 << bpc) - 1; + var i = 0, ii, buf; + + if (bpc === 1) { + // Optimization for reading 1 bpc images. + var mask, loop1End, loop2End; + for (var j = 0; j < height; j++) { + loop1End = i + (rowComps & ~7); + loop2End = i + rowComps; + + // unroll loop for all full bytes + while (i < loop1End) { + buf = buffer[bufferPos++]; + output[i] = (buf >> 7) & 1; + output[i + 1] = (buf >> 6) & 1; + output[i + 2] = (buf >> 5) & 1; + output[i + 3] = (buf >> 4) & 1; + output[i + 4] = (buf >> 3) & 1; + output[i + 5] = (buf >> 2) & 1; + output[i + 6] = (buf >> 1) & 1; + output[i + 7] = buf & 1; + i += 8; + } + + // handle remaing bits + if (i < loop2End) { + buf = buffer[bufferPos++]; + mask = 128; + while (i < loop2End) { + output[i++] = +!!(buf & mask); + mask >>= 1; + } + } + } + } else { + // The general case that handles all other bpc values. + var bits = 0; + buf = 0; + for (i = 0, ii = length; i < ii; ++i) { + if (i % rowComps === 0) { + buf = 0; + bits = 0; + } + + while (bits < bpc) { + buf = (buf << 8) | buffer[bufferPos++]; + bits += 8; + } + + var remainingBits = bits - bpc; + var value = buf >> remainingBits; + output[i] = (value < 0 ? 0 : (value > max ? max : value)); + buf = buf & ((1 << remainingBits) - 1); + bits = remainingBits; + } + } + return output; + }, + + fillOpacity: function PDFImage_fillOpacity(rgbaBuf, width, height, + actualHeight, image) { + var smask = this.smask; + var mask = this.mask; + var alphaBuf, sw, sh, i, ii, j; + + if (smask) { + sw = smask.width; + sh = smask.height; + alphaBuf = new Uint8Array(sw * sh); + smask.fillGrayBuffer(alphaBuf); + if (sw !== width || sh !== height) { + alphaBuf = PDFImage.resize(alphaBuf, smask.bpc, 1, sw, sh, width, + height); + } + } else if (mask) { + if (mask instanceof PDFImage) { + sw = mask.width; + sh = mask.height; + alphaBuf = new Uint8Array(sw * sh); + mask.numComps = 1; + mask.fillGrayBuffer(alphaBuf); + + // Need to invert values in rgbaBuf + for (i = 0, ii = sw * sh; i < ii; ++i) { + alphaBuf[i] = 255 - alphaBuf[i]; + } + + if (sw !== width || sh !== height) { + alphaBuf = PDFImage.resize(alphaBuf, mask.bpc, 1, sw, sh, width, + height); + } + } else if (isArray(mask)) { + // Color key mask: if any of the compontents are outside the range + // then they should be painted. + alphaBuf = new Uint8Array(width * height); + var numComps = this.numComps; + for (i = 0, ii = width * height; i < ii; ++i) { + var opacity = 0; + var imageOffset = i * numComps; + for (j = 0; j < numComps; ++j) { + var color = image[imageOffset + j]; + var maskOffset = j * 2; + if (color < mask[maskOffset] || color > mask[maskOffset + 1]) { + opacity = 255; + break; + } + } + alphaBuf[i] = opacity; + } + } else { + error('Unknown mask format.'); + } + } + + if (alphaBuf) { + for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) { + rgbaBuf[j] = alphaBuf[i]; + } + } else { + // No mask. + for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) { + rgbaBuf[j] = 255; + } + } + }, + + undoPreblend: function PDFImage_undoPreblend(buffer, width, height) { + var matte = this.smask && this.smask.matte; + if (!matte) { + return; + } + var matteRgb = this.colorSpace.getRgb(matte, 0); + var matteR = matteRgb[0]; + var matteG = matteRgb[1]; + var matteB = matteRgb[2]; + var length = width * height * 4; + var r, g, b; + for (var i = 0; i < length; i += 4) { + var alpha = buffer[i + 3]; + if (alpha === 0) { + // according formula we have to get Infinity in all components + // making it white (typical paper color) should be okay + buffer[i] = 255; + buffer[i + 1] = 255; + buffer[i + 2] = 255; + continue; + } + var k = 255 / alpha; + r = (buffer[i] - matteR) * k + matteR; + g = (buffer[i + 1] - matteG) * k + matteG; + b = (buffer[i + 2] - matteB) * k + matteB; + buffer[i] = r <= 0 ? 0 : r >= 255 ? 255 : r | 0; + buffer[i + 1] = g <= 0 ? 0 : g >= 255 ? 255 : g | 0; + buffer[i + 2] = b <= 0 ? 0 : b >= 255 ? 255 : b | 0; + } + }, + + createImageData: function PDFImage_createImageData(forceRGBA) { + var drawWidth = this.drawWidth; + var drawHeight = this.drawHeight; + var imgData = { // other fields are filled in below + width: drawWidth, + height: drawHeight + }; + + var numComps = this.numComps; + var originalWidth = this.width; + var originalHeight = this.height; + var bpc = this.bpc; + + // Rows start at byte boundary. + var rowBytes = (originalWidth * numComps * bpc + 7) >> 3; + var imgArray; + + if (!forceRGBA) { + // If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image + // without any complications, we pass a same-sized copy to the main + // thread rather than expanding by 32x to RGBA form. This saves *lots* + // of memory for many scanned documents. It's also much faster. + // + // Similarly, if it is a 24-bit-per pixel RGB image without any + // complications, we avoid expanding by 1.333x to RGBA form. + var kind; + if (this.colorSpace.name === 'DeviceGray' && bpc === 1) { + kind = ImageKind.GRAYSCALE_1BPP; + } else if (this.colorSpace.name === 'DeviceRGB' && bpc === 8 && + !this.needsDecode) { + kind = ImageKind.RGB_24BPP; + } + if (kind && !this.smask && !this.mask && + drawWidth === originalWidth && drawHeight === originalHeight) { + imgData.kind = kind; + + imgArray = this.getImageBytes(originalHeight * rowBytes); + // If imgArray came from a DecodeStream, we're safe to transfer it + // (and thus neuter it) because it will constitute the entire + // DecodeStream's data. But if it came from a Stream, we need to + // copy it because it'll only be a portion of the Stream's data, and + // the rest will be read later on. + if (this.image instanceof DecodeStream) { + imgData.data = imgArray; + } else { + var newArray = new Uint8Array(imgArray.length); + newArray.set(imgArray); + imgData.data = newArray; + } + if (this.needsDecode) { + // Invert the buffer (which must be grayscale if we reached here). + assert(kind === ImageKind.GRAYSCALE_1BPP); + var buffer = imgData.data; + for (var i = 0, ii = buffer.length; i < ii; i++) { + buffer[i] ^= 0xff; + } + } + return imgData; + } + if (this.image instanceof JpegStream && !this.smask && !this.mask) { + imgData.kind = ImageKind.RGB_24BPP; + imgData.data = this.getImageBytes(originalHeight * rowBytes, + drawWidth, drawHeight, true); + return imgData; + } + } + + imgArray = this.getImageBytes(originalHeight * rowBytes); + // imgArray can be incomplete (e.g. after CCITT fax encoding). + var actualHeight = 0 | (imgArray.length / rowBytes * + drawHeight / originalHeight); + + var comps = this.getComponents(imgArray); + + // If opacity data is present, use RGBA_32BPP form. Otherwise, use the + // more compact RGB_24BPP form if allowable. + var alpha01, maybeUndoPreblend; + if (!forceRGBA && !this.smask && !this.mask) { + imgData.kind = ImageKind.RGB_24BPP; + imgData.data = new Uint8Array(drawWidth * drawHeight * 3); + alpha01 = 0; + maybeUndoPreblend = false; + } else { + imgData.kind = ImageKind.RGBA_32BPP; + imgData.data = new Uint8Array(drawWidth * drawHeight * 4); + alpha01 = 1; + maybeUndoPreblend = true; + + // Color key masking (opacity) must be performed before decoding. + this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight, + comps); + } + + if (this.needsDecode) { + this.decodeBuffer(comps); + } + this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight, + drawWidth, drawHeight, actualHeight, bpc, comps, + alpha01); + if (maybeUndoPreblend) { + this.undoPreblend(imgData.data, drawWidth, actualHeight); + } + + return imgData; + }, + + fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) { + var numComps = this.numComps; + if (numComps !== 1) { + error('Reading gray scale from a color image: ' + numComps); + } + + var width = this.width; + var height = this.height; + var bpc = this.bpc; + + // rows start at byte boundary + var rowBytes = (width * numComps * bpc + 7) >> 3; + var imgArray = this.getImageBytes(height * rowBytes); + + var comps = this.getComponents(imgArray); + var i, length; + + if (bpc === 1) { + // inline decoding (= inversion) for 1 bpc images + length = width * height; + if (this.needsDecode) { + // invert and scale to {0, 255} + for (i = 0; i < length; ++i) { + buffer[i] = (comps[i] - 1) & 255; + } + } else { + // scale to {0, 255} + for (i = 0; i < length; ++i) { + buffer[i] = (-comps[i]) & 255; + } + } + return; + } + + if (this.needsDecode) { + this.decodeBuffer(comps); + } + length = width * height; + // we aren't using a colorspace so we need to scale the value + var scale = 255 / ((1 << bpc) - 1); + for (i = 0; i < length; ++i) { + buffer[i] = (scale * comps[i]) | 0; + } + }, + + getImageBytes: function PDFImage_getImageBytes(length, + drawWidth, drawHeight, + forceRGB) { + this.image.reset(); + this.image.drawWidth = drawWidth || this.width; + this.image.drawHeight = drawHeight || this.height; + this.image.forceRGB = !!forceRGB; + return this.image.getBytes(length); + } + }; + return PDFImage; +})(); + + +// The Metrics object contains glyph widths (in glyph space units). +// As per PDF spec, for most fonts (Type 3 being an exception) a glyph +// space unit corresponds to 1/1000th of text space unit. +var Metrics = { + 'Courier': 600, + 'Courier-Bold': 600, + 'Courier-BoldOblique': 600, + 'Courier-Oblique': 600, + 'Helvetica' : { + 'space': 278, + 'exclam': 278, + 'quotedbl': 355, + 'numbersign': 556, + 'dollar': 556, + 'percent': 889, + 'ampersand': 667, + 'quoteright': 222, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 389, + 'plus': 584, + 'comma': 278, + 'hyphen': 333, + 'period': 278, + 'slash': 278, + 'zero': 556, + 'one': 556, + 'two': 556, + 'three': 556, + 'four': 556, + 'five': 556, + 'six': 556, + 'seven': 556, + 'eight': 556, + 'nine': 556, + 'colon': 278, + 'semicolon': 278, + 'less': 584, + 'equal': 584, + 'greater': 584, + 'question': 556, + 'at': 1015, + 'A': 667, + 'B': 667, + 'C': 722, + 'D': 722, + 'E': 667, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'J': 500, + 'K': 667, + 'L': 556, + 'M': 833, + 'N': 722, + 'O': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'T': 611, + 'U': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Z': 611, + 'bracketleft': 278, + 'backslash': 278, + 'bracketright': 278, + 'asciicircum': 469, + 'underscore': 556, + 'quoteleft': 222, + 'a': 556, + 'b': 556, + 'c': 500, + 'd': 556, + 'e': 556, + 'f': 278, + 'g': 556, + 'h': 556, + 'i': 222, + 'j': 222, + 'k': 500, + 'l': 222, + 'm': 833, + 'n': 556, + 'o': 556, + 'p': 556, + 'q': 556, + 'r': 333, + 's': 500, + 't': 278, + 'u': 556, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'z': 500, + 'braceleft': 334, + 'bar': 260, + 'braceright': 334, + 'asciitilde': 584, + 'exclamdown': 333, + 'cent': 556, + 'sterling': 556, + 'fraction': 167, + 'yen': 556, + 'florin': 556, + 'section': 556, + 'currency': 556, + 'quotesingle': 191, + 'quotedblleft': 333, + 'guillemotleft': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 500, + 'fl': 500, + 'endash': 556, + 'dagger': 556, + 'daggerdbl': 556, + 'periodcentered': 278, + 'paragraph': 537, + 'bullet': 350, + 'quotesinglbase': 222, + 'quotedblbase': 333, + 'quotedblright': 333, + 'guillemotright': 556, + 'ellipsis': 1000, + 'perthousand': 1000, + 'questiondown': 611, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 1000, + 'AE': 1000, + 'ordfeminine': 370, + 'Lslash': 556, + 'Oslash': 778, + 'OE': 1000, + 'ordmasculine': 365, + 'ae': 889, + 'dotlessi': 278, + 'lslash': 222, + 'oslash': 611, + 'oe': 944, + 'germandbls': 611, + 'Idieresis': 278, + 'eacute': 556, + 'abreve': 556, + 'uhungarumlaut': 556, + 'ecaron': 556, + 'Ydieresis': 667, + 'divide': 584, + 'Yacute': 667, + 'Acircumflex': 667, + 'aacute': 556, + 'Ucircumflex': 722, + 'yacute': 500, + 'scommaaccent': 500, + 'ecircumflex': 556, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 556, + 'Uacute': 722, + 'uogonek': 556, + 'Edieresis': 667, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 737, + 'Emacron': 667, + 'ccaron': 500, + 'aring': 556, + 'Ncommaaccent': 722, + 'lacute': 222, + 'agrave': 556, + 'Tcommaaccent': 611, + 'Cacute': 722, + 'atilde': 556, + 'Edotaccent': 667, + 'scaron': 500, + 'scedilla': 500, + 'iacute': 278, + 'lozenge': 471, + 'Rcaron': 722, + 'Gcommaaccent': 778, + 'ucircumflex': 556, + 'acircumflex': 556, + 'Amacron': 667, + 'rcaron': 333, + 'ccedilla': 500, + 'Zdotaccent': 611, + 'Thorn': 667, + 'Omacron': 778, + 'Racute': 722, + 'Sacute': 667, + 'dcaron': 643, + 'Umacron': 722, + 'uring': 556, + 'threesuperior': 333, + 'Ograve': 778, + 'Agrave': 667, + 'Abreve': 667, + 'multiply': 584, + 'uacute': 556, + 'Tcaron': 611, + 'partialdiff': 476, + 'ydieresis': 500, + 'Nacute': 722, + 'icircumflex': 278, + 'Ecircumflex': 667, + 'adieresis': 556, + 'edieresis': 556, + 'cacute': 500, + 'nacute': 556, + 'umacron': 556, + 'Ncaron': 722, + 'Iacute': 278, + 'plusminus': 584, + 'brokenbar': 260, + 'registered': 737, + 'Gbreve': 778, + 'Idotaccent': 278, + 'summation': 600, + 'Egrave': 667, + 'racute': 333, + 'omacron': 556, + 'Zacute': 611, + 'Zcaron': 611, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 722, + 'lcommaaccent': 222, + 'tcaron': 317, + 'eogonek': 556, + 'Uogonek': 722, + 'Aacute': 667, + 'Adieresis': 667, + 'egrave': 556, + 'zacute': 500, + 'iogonek': 222, + 'Oacute': 778, + 'oacute': 556, + 'amacron': 556, + 'sacute': 500, + 'idieresis': 278, + 'Ocircumflex': 778, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 556, + 'twosuperior': 333, + 'Odieresis': 778, + 'mu': 556, + 'igrave': 278, + 'ohungarumlaut': 556, + 'Eogonek': 667, + 'dcroat': 556, + 'threequarters': 834, + 'Scedilla': 667, + 'lcaron': 299, + 'Kcommaaccent': 667, + 'Lacute': 556, + 'trademark': 1000, + 'edotaccent': 556, + 'Igrave': 278, + 'Imacron': 278, + 'Lcaron': 556, + 'onehalf': 834, + 'lessequal': 549, + 'ocircumflex': 556, + 'ntilde': 556, + 'Uhungarumlaut': 722, + 'Eacute': 667, + 'emacron': 556, + 'gbreve': 556, + 'onequarter': 834, + 'Scaron': 667, + 'Scommaaccent': 667, + 'Ohungarumlaut': 778, + 'degree': 400, + 'ograve': 556, + 'Ccaron': 722, + 'ugrave': 556, + 'radical': 453, + 'Dcaron': 722, + 'rcommaaccent': 333, + 'Ntilde': 722, + 'otilde': 556, + 'Rcommaaccent': 722, + 'Lcommaaccent': 556, + 'Atilde': 667, + 'Aogonek': 667, + 'Aring': 667, + 'Otilde': 778, + 'zdotaccent': 500, + 'Ecaron': 667, + 'Iogonek': 278, + 'kcommaaccent': 500, + 'minus': 584, + 'Icircumflex': 278, + 'ncaron': 556, + 'tcommaaccent': 278, + 'logicalnot': 584, + 'odieresis': 556, + 'udieresis': 556, + 'notequal': 549, + 'gcommaaccent': 556, + 'eth': 556, + 'zcaron': 500, + 'ncommaaccent': 556, + 'onesuperior': 333, + 'imacron': 278, + 'Euro': 556 + }, + 'Helvetica-Bold': { + 'space': 278, + 'exclam': 333, + 'quotedbl': 474, + 'numbersign': 556, + 'dollar': 556, + 'percent': 889, + 'ampersand': 722, + 'quoteright': 278, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 389, + 'plus': 584, + 'comma': 278, + 'hyphen': 333, + 'period': 278, + 'slash': 278, + 'zero': 556, + 'one': 556, + 'two': 556, + 'three': 556, + 'four': 556, + 'five': 556, + 'six': 556, + 'seven': 556, + 'eight': 556, + 'nine': 556, + 'colon': 333, + 'semicolon': 333, + 'less': 584, + 'equal': 584, + 'greater': 584, + 'question': 611, + 'at': 975, + 'A': 722, + 'B': 722, + 'C': 722, + 'D': 722, + 'E': 667, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'J': 556, + 'K': 722, + 'L': 611, + 'M': 833, + 'N': 722, + 'O': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'T': 611, + 'U': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Z': 611, + 'bracketleft': 333, + 'backslash': 278, + 'bracketright': 333, + 'asciicircum': 584, + 'underscore': 556, + 'quoteleft': 278, + 'a': 556, + 'b': 611, + 'c': 556, + 'd': 611, + 'e': 556, + 'f': 333, + 'g': 611, + 'h': 611, + 'i': 278, + 'j': 278, + 'k': 556, + 'l': 278, + 'm': 889, + 'n': 611, + 'o': 611, + 'p': 611, + 'q': 611, + 'r': 389, + 's': 556, + 't': 333, + 'u': 611, + 'v': 556, + 'w': 778, + 'x': 556, + 'y': 556, + 'z': 500, + 'braceleft': 389, + 'bar': 280, + 'braceright': 389, + 'asciitilde': 584, + 'exclamdown': 333, + 'cent': 556, + 'sterling': 556, + 'fraction': 167, + 'yen': 556, + 'florin': 556, + 'section': 556, + 'currency': 556, + 'quotesingle': 238, + 'quotedblleft': 500, + 'guillemotleft': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 611, + 'fl': 611, + 'endash': 556, + 'dagger': 556, + 'daggerdbl': 556, + 'periodcentered': 278, + 'paragraph': 556, + 'bullet': 350, + 'quotesinglbase': 278, + 'quotedblbase': 500, + 'quotedblright': 500, + 'guillemotright': 556, + 'ellipsis': 1000, + 'perthousand': 1000, + 'questiondown': 611, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 1000, + 'AE': 1000, + 'ordfeminine': 370, + 'Lslash': 611, + 'Oslash': 778, + 'OE': 1000, + 'ordmasculine': 365, + 'ae': 889, + 'dotlessi': 278, + 'lslash': 278, + 'oslash': 611, + 'oe': 944, + 'germandbls': 611, + 'Idieresis': 278, + 'eacute': 556, + 'abreve': 556, + 'uhungarumlaut': 611, + 'ecaron': 556, + 'Ydieresis': 667, + 'divide': 584, + 'Yacute': 667, + 'Acircumflex': 722, + 'aacute': 556, + 'Ucircumflex': 722, + 'yacute': 556, + 'scommaaccent': 556, + 'ecircumflex': 556, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 556, + 'Uacute': 722, + 'uogonek': 611, + 'Edieresis': 667, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 737, + 'Emacron': 667, + 'ccaron': 556, + 'aring': 556, + 'Ncommaaccent': 722, + 'lacute': 278, + 'agrave': 556, + 'Tcommaaccent': 611, + 'Cacute': 722, + 'atilde': 556, + 'Edotaccent': 667, + 'scaron': 556, + 'scedilla': 556, + 'iacute': 278, + 'lozenge': 494, + 'Rcaron': 722, + 'Gcommaaccent': 778, + 'ucircumflex': 611, + 'acircumflex': 556, + 'Amacron': 722, + 'rcaron': 389, + 'ccedilla': 556, + 'Zdotaccent': 611, + 'Thorn': 667, + 'Omacron': 778, + 'Racute': 722, + 'Sacute': 667, + 'dcaron': 743, + 'Umacron': 722, + 'uring': 611, + 'threesuperior': 333, + 'Ograve': 778, + 'Agrave': 722, + 'Abreve': 722, + 'multiply': 584, + 'uacute': 611, + 'Tcaron': 611, + 'partialdiff': 494, + 'ydieresis': 556, + 'Nacute': 722, + 'icircumflex': 278, + 'Ecircumflex': 667, + 'adieresis': 556, + 'edieresis': 556, + 'cacute': 556, + 'nacute': 611, + 'umacron': 611, + 'Ncaron': 722, + 'Iacute': 278, + 'plusminus': 584, + 'brokenbar': 280, + 'registered': 737, + 'Gbreve': 778, + 'Idotaccent': 278, + 'summation': 600, + 'Egrave': 667, + 'racute': 389, + 'omacron': 611, + 'Zacute': 611, + 'Zcaron': 611, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 722, + 'lcommaaccent': 278, + 'tcaron': 389, + 'eogonek': 556, + 'Uogonek': 722, + 'Aacute': 722, + 'Adieresis': 722, + 'egrave': 556, + 'zacute': 500, + 'iogonek': 278, + 'Oacute': 778, + 'oacute': 611, + 'amacron': 556, + 'sacute': 556, + 'idieresis': 278, + 'Ocircumflex': 778, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 611, + 'twosuperior': 333, + 'Odieresis': 778, + 'mu': 611, + 'igrave': 278, + 'ohungarumlaut': 611, + 'Eogonek': 667, + 'dcroat': 611, + 'threequarters': 834, + 'Scedilla': 667, + 'lcaron': 400, + 'Kcommaaccent': 722, + 'Lacute': 611, + 'trademark': 1000, + 'edotaccent': 556, + 'Igrave': 278, + 'Imacron': 278, + 'Lcaron': 611, + 'onehalf': 834, + 'lessequal': 549, + 'ocircumflex': 611, + 'ntilde': 611, + 'Uhungarumlaut': 722, + 'Eacute': 667, + 'emacron': 556, + 'gbreve': 611, + 'onequarter': 834, + 'Scaron': 667, + 'Scommaaccent': 667, + 'Ohungarumlaut': 778, + 'degree': 400, + 'ograve': 611, + 'Ccaron': 722, + 'ugrave': 611, + 'radical': 549, + 'Dcaron': 722, + 'rcommaaccent': 389, + 'Ntilde': 722, + 'otilde': 611, + 'Rcommaaccent': 722, + 'Lcommaaccent': 611, + 'Atilde': 722, + 'Aogonek': 722, + 'Aring': 722, + 'Otilde': 778, + 'zdotaccent': 500, + 'Ecaron': 667, + 'Iogonek': 278, + 'kcommaaccent': 556, + 'minus': 584, + 'Icircumflex': 278, + 'ncaron': 611, + 'tcommaaccent': 333, + 'logicalnot': 584, + 'odieresis': 611, + 'udieresis': 611, + 'notequal': 549, + 'gcommaaccent': 611, + 'eth': 611, + 'zcaron': 500, + 'ncommaaccent': 611, + 'onesuperior': 333, + 'imacron': 278, + 'Euro': 556 + }, + 'Helvetica-BoldOblique': { + 'space': 278, + 'exclam': 333, + 'quotedbl': 474, + 'numbersign': 556, + 'dollar': 556, + 'percent': 889, + 'ampersand': 722, + 'quoteright': 278, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 389, + 'plus': 584, + 'comma': 278, + 'hyphen': 333, + 'period': 278, + 'slash': 278, + 'zero': 556, + 'one': 556, + 'two': 556, + 'three': 556, + 'four': 556, + 'five': 556, + 'six': 556, + 'seven': 556, + 'eight': 556, + 'nine': 556, + 'colon': 333, + 'semicolon': 333, + 'less': 584, + 'equal': 584, + 'greater': 584, + 'question': 611, + 'at': 975, + 'A': 722, + 'B': 722, + 'C': 722, + 'D': 722, + 'E': 667, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'J': 556, + 'K': 722, + 'L': 611, + 'M': 833, + 'N': 722, + 'O': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'T': 611, + 'U': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Z': 611, + 'bracketleft': 333, + 'backslash': 278, + 'bracketright': 333, + 'asciicircum': 584, + 'underscore': 556, + 'quoteleft': 278, + 'a': 556, + 'b': 611, + 'c': 556, + 'd': 611, + 'e': 556, + 'f': 333, + 'g': 611, + 'h': 611, + 'i': 278, + 'j': 278, + 'k': 556, + 'l': 278, + 'm': 889, + 'n': 611, + 'o': 611, + 'p': 611, + 'q': 611, + 'r': 389, + 's': 556, + 't': 333, + 'u': 611, + 'v': 556, + 'w': 778, + 'x': 556, + 'y': 556, + 'z': 500, + 'braceleft': 389, + 'bar': 280, + 'braceright': 389, + 'asciitilde': 584, + 'exclamdown': 333, + 'cent': 556, + 'sterling': 556, + 'fraction': 167, + 'yen': 556, + 'florin': 556, + 'section': 556, + 'currency': 556, + 'quotesingle': 238, + 'quotedblleft': 500, + 'guillemotleft': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 611, + 'fl': 611, + 'endash': 556, + 'dagger': 556, + 'daggerdbl': 556, + 'periodcentered': 278, + 'paragraph': 556, + 'bullet': 350, + 'quotesinglbase': 278, + 'quotedblbase': 500, + 'quotedblright': 500, + 'guillemotright': 556, + 'ellipsis': 1000, + 'perthousand': 1000, + 'questiondown': 611, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 1000, + 'AE': 1000, + 'ordfeminine': 370, + 'Lslash': 611, + 'Oslash': 778, + 'OE': 1000, + 'ordmasculine': 365, + 'ae': 889, + 'dotlessi': 278, + 'lslash': 278, + 'oslash': 611, + 'oe': 944, + 'germandbls': 611, + 'Idieresis': 278, + 'eacute': 556, + 'abreve': 556, + 'uhungarumlaut': 611, + 'ecaron': 556, + 'Ydieresis': 667, + 'divide': 584, + 'Yacute': 667, + 'Acircumflex': 722, + 'aacute': 556, + 'Ucircumflex': 722, + 'yacute': 556, + 'scommaaccent': 556, + 'ecircumflex': 556, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 556, + 'Uacute': 722, + 'uogonek': 611, + 'Edieresis': 667, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 737, + 'Emacron': 667, + 'ccaron': 556, + 'aring': 556, + 'Ncommaaccent': 722, + 'lacute': 278, + 'agrave': 556, + 'Tcommaaccent': 611, + 'Cacute': 722, + 'atilde': 556, + 'Edotaccent': 667, + 'scaron': 556, + 'scedilla': 556, + 'iacute': 278, + 'lozenge': 494, + 'Rcaron': 722, + 'Gcommaaccent': 778, + 'ucircumflex': 611, + 'acircumflex': 556, + 'Amacron': 722, + 'rcaron': 389, + 'ccedilla': 556, + 'Zdotaccent': 611, + 'Thorn': 667, + 'Omacron': 778, + 'Racute': 722, + 'Sacute': 667, + 'dcaron': 743, + 'Umacron': 722, + 'uring': 611, + 'threesuperior': 333, + 'Ograve': 778, + 'Agrave': 722, + 'Abreve': 722, + 'multiply': 584, + 'uacute': 611, + 'Tcaron': 611, + 'partialdiff': 494, + 'ydieresis': 556, + 'Nacute': 722, + 'icircumflex': 278, + 'Ecircumflex': 667, + 'adieresis': 556, + 'edieresis': 556, + 'cacute': 556, + 'nacute': 611, + 'umacron': 611, + 'Ncaron': 722, + 'Iacute': 278, + 'plusminus': 584, + 'brokenbar': 280, + 'registered': 737, + 'Gbreve': 778, + 'Idotaccent': 278, + 'summation': 600, + 'Egrave': 667, + 'racute': 389, + 'omacron': 611, + 'Zacute': 611, + 'Zcaron': 611, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 722, + 'lcommaaccent': 278, + 'tcaron': 389, + 'eogonek': 556, + 'Uogonek': 722, + 'Aacute': 722, + 'Adieresis': 722, + 'egrave': 556, + 'zacute': 500, + 'iogonek': 278, + 'Oacute': 778, + 'oacute': 611, + 'amacron': 556, + 'sacute': 556, + 'idieresis': 278, + 'Ocircumflex': 778, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 611, + 'twosuperior': 333, + 'Odieresis': 778, + 'mu': 611, + 'igrave': 278, + 'ohungarumlaut': 611, + 'Eogonek': 667, + 'dcroat': 611, + 'threequarters': 834, + 'Scedilla': 667, + 'lcaron': 400, + 'Kcommaaccent': 722, + 'Lacute': 611, + 'trademark': 1000, + 'edotaccent': 556, + 'Igrave': 278, + 'Imacron': 278, + 'Lcaron': 611, + 'onehalf': 834, + 'lessequal': 549, + 'ocircumflex': 611, + 'ntilde': 611, + 'Uhungarumlaut': 722, + 'Eacute': 667, + 'emacron': 556, + 'gbreve': 611, + 'onequarter': 834, + 'Scaron': 667, + 'Scommaaccent': 667, + 'Ohungarumlaut': 778, + 'degree': 400, + 'ograve': 611, + 'Ccaron': 722, + 'ugrave': 611, + 'radical': 549, + 'Dcaron': 722, + 'rcommaaccent': 389, + 'Ntilde': 722, + 'otilde': 611, + 'Rcommaaccent': 722, + 'Lcommaaccent': 611, + 'Atilde': 722, + 'Aogonek': 722, + 'Aring': 722, + 'Otilde': 778, + 'zdotaccent': 500, + 'Ecaron': 667, + 'Iogonek': 278, + 'kcommaaccent': 556, + 'minus': 584, + 'Icircumflex': 278, + 'ncaron': 611, + 'tcommaaccent': 333, + 'logicalnot': 584, + 'odieresis': 611, + 'udieresis': 611, + 'notequal': 549, + 'gcommaaccent': 611, + 'eth': 611, + 'zcaron': 500, + 'ncommaaccent': 611, + 'onesuperior': 333, + 'imacron': 278, + 'Euro': 556 + }, + 'Helvetica-Oblique' : { + 'space': 278, + 'exclam': 278, + 'quotedbl': 355, + 'numbersign': 556, + 'dollar': 556, + 'percent': 889, + 'ampersand': 667, + 'quoteright': 222, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 389, + 'plus': 584, + 'comma': 278, + 'hyphen': 333, + 'period': 278, + 'slash': 278, + 'zero': 556, + 'one': 556, + 'two': 556, + 'three': 556, + 'four': 556, + 'five': 556, + 'six': 556, + 'seven': 556, + 'eight': 556, + 'nine': 556, + 'colon': 278, + 'semicolon': 278, + 'less': 584, + 'equal': 584, + 'greater': 584, + 'question': 556, + 'at': 1015, + 'A': 667, + 'B': 667, + 'C': 722, + 'D': 722, + 'E': 667, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'J': 500, + 'K': 667, + 'L': 556, + 'M': 833, + 'N': 722, + 'O': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'T': 611, + 'U': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Z': 611, + 'bracketleft': 278, + 'backslash': 278, + 'bracketright': 278, + 'asciicircum': 469, + 'underscore': 556, + 'quoteleft': 222, + 'a': 556, + 'b': 556, + 'c': 500, + 'd': 556, + 'e': 556, + 'f': 278, + 'g': 556, + 'h': 556, + 'i': 222, + 'j': 222, + 'k': 500, + 'l': 222, + 'm': 833, + 'n': 556, + 'o': 556, + 'p': 556, + 'q': 556, + 'r': 333, + 's': 500, + 't': 278, + 'u': 556, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'z': 500, + 'braceleft': 334, + 'bar': 260, + 'braceright': 334, + 'asciitilde': 584, + 'exclamdown': 333, + 'cent': 556, + 'sterling': 556, + 'fraction': 167, + 'yen': 556, + 'florin': 556, + 'section': 556, + 'currency': 556, + 'quotesingle': 191, + 'quotedblleft': 333, + 'guillemotleft': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 500, + 'fl': 500, + 'endash': 556, + 'dagger': 556, + 'daggerdbl': 556, + 'periodcentered': 278, + 'paragraph': 537, + 'bullet': 350, + 'quotesinglbase': 222, + 'quotedblbase': 333, + 'quotedblright': 333, + 'guillemotright': 556, + 'ellipsis': 1000, + 'perthousand': 1000, + 'questiondown': 611, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 1000, + 'AE': 1000, + 'ordfeminine': 370, + 'Lslash': 556, + 'Oslash': 778, + 'OE': 1000, + 'ordmasculine': 365, + 'ae': 889, + 'dotlessi': 278, + 'lslash': 222, + 'oslash': 611, + 'oe': 944, + 'germandbls': 611, + 'Idieresis': 278, + 'eacute': 556, + 'abreve': 556, + 'uhungarumlaut': 556, + 'ecaron': 556, + 'Ydieresis': 667, + 'divide': 584, + 'Yacute': 667, + 'Acircumflex': 667, + 'aacute': 556, + 'Ucircumflex': 722, + 'yacute': 500, + 'scommaaccent': 500, + 'ecircumflex': 556, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 556, + 'Uacute': 722, + 'uogonek': 556, + 'Edieresis': 667, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 737, + 'Emacron': 667, + 'ccaron': 500, + 'aring': 556, + 'Ncommaaccent': 722, + 'lacute': 222, + 'agrave': 556, + 'Tcommaaccent': 611, + 'Cacute': 722, + 'atilde': 556, + 'Edotaccent': 667, + 'scaron': 500, + 'scedilla': 500, + 'iacute': 278, + 'lozenge': 471, + 'Rcaron': 722, + 'Gcommaaccent': 778, + 'ucircumflex': 556, + 'acircumflex': 556, + 'Amacron': 667, + 'rcaron': 333, + 'ccedilla': 500, + 'Zdotaccent': 611, + 'Thorn': 667, + 'Omacron': 778, + 'Racute': 722, + 'Sacute': 667, + 'dcaron': 643, + 'Umacron': 722, + 'uring': 556, + 'threesuperior': 333, + 'Ograve': 778, + 'Agrave': 667, + 'Abreve': 667, + 'multiply': 584, + 'uacute': 556, + 'Tcaron': 611, + 'partialdiff': 476, + 'ydieresis': 500, + 'Nacute': 722, + 'icircumflex': 278, + 'Ecircumflex': 667, + 'adieresis': 556, + 'edieresis': 556, + 'cacute': 500, + 'nacute': 556, + 'umacron': 556, + 'Ncaron': 722, + 'Iacute': 278, + 'plusminus': 584, + 'brokenbar': 260, + 'registered': 737, + 'Gbreve': 778, + 'Idotaccent': 278, + 'summation': 600, + 'Egrave': 667, + 'racute': 333, + 'omacron': 556, + 'Zacute': 611, + 'Zcaron': 611, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 722, + 'lcommaaccent': 222, + 'tcaron': 317, + 'eogonek': 556, + 'Uogonek': 722, + 'Aacute': 667, + 'Adieresis': 667, + 'egrave': 556, + 'zacute': 500, + 'iogonek': 222, + 'Oacute': 778, + 'oacute': 556, + 'amacron': 556, + 'sacute': 500, + 'idieresis': 278, + 'Ocircumflex': 778, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 556, + 'twosuperior': 333, + 'Odieresis': 778, + 'mu': 556, + 'igrave': 278, + 'ohungarumlaut': 556, + 'Eogonek': 667, + 'dcroat': 556, + 'threequarters': 834, + 'Scedilla': 667, + 'lcaron': 299, + 'Kcommaaccent': 667, + 'Lacute': 556, + 'trademark': 1000, + 'edotaccent': 556, + 'Igrave': 278, + 'Imacron': 278, + 'Lcaron': 556, + 'onehalf': 834, + 'lessequal': 549, + 'ocircumflex': 556, + 'ntilde': 556, + 'Uhungarumlaut': 722, + 'Eacute': 667, + 'emacron': 556, + 'gbreve': 556, + 'onequarter': 834, + 'Scaron': 667, + 'Scommaaccent': 667, + 'Ohungarumlaut': 778, + 'degree': 400, + 'ograve': 556, + 'Ccaron': 722, + 'ugrave': 556, + 'radical': 453, + 'Dcaron': 722, + 'rcommaaccent': 333, + 'Ntilde': 722, + 'otilde': 556, + 'Rcommaaccent': 722, + 'Lcommaaccent': 556, + 'Atilde': 667, + 'Aogonek': 667, + 'Aring': 667, + 'Otilde': 778, + 'zdotaccent': 500, + 'Ecaron': 667, + 'Iogonek': 278, + 'kcommaaccent': 500, + 'minus': 584, + 'Icircumflex': 278, + 'ncaron': 556, + 'tcommaaccent': 278, + 'logicalnot': 584, + 'odieresis': 556, + 'udieresis': 556, + 'notequal': 549, + 'gcommaaccent': 556, + 'eth': 556, + 'zcaron': 500, + 'ncommaaccent': 556, + 'onesuperior': 333, + 'imacron': 278, + 'Euro': 556 + }, + 'Symbol': { + 'space': 250, + 'exclam': 333, + 'universal': 713, + 'numbersign': 500, + 'existential': 549, + 'percent': 833, + 'ampersand': 778, + 'suchthat': 439, + 'parenleft': 333, + 'parenright': 333, + 'asteriskmath': 500, + 'plus': 549, + 'comma': 250, + 'minus': 549, + 'period': 250, + 'slash': 278, + 'zero': 500, + 'one': 500, + 'two': 500, + 'three': 500, + 'four': 500, + 'five': 500, + 'six': 500, + 'seven': 500, + 'eight': 500, + 'nine': 500, + 'colon': 278, + 'semicolon': 278, + 'less': 549, + 'equal': 549, + 'greater': 549, + 'question': 444, + 'congruent': 549, + 'Alpha': 722, + 'Beta': 667, + 'Chi': 722, + 'Delta': 612, + 'Epsilon': 611, + 'Phi': 763, + 'Gamma': 603, + 'Eta': 722, + 'Iota': 333, + 'theta1': 631, + 'Kappa': 722, + 'Lambda': 686, + 'Mu': 889, + 'Nu': 722, + 'Omicron': 722, + 'Pi': 768, + 'Theta': 741, + 'Rho': 556, + 'Sigma': 592, + 'Tau': 611, + 'Upsilon': 690, + 'sigma1': 439, + 'Omega': 768, + 'Xi': 645, + 'Psi': 795, + 'Zeta': 611, + 'bracketleft': 333, + 'therefore': 863, + 'bracketright': 333, + 'perpendicular': 658, + 'underscore': 500, + 'radicalex': 500, + 'alpha': 631, + 'beta': 549, + 'chi': 549, + 'delta': 494, + 'epsilon': 439, + 'phi': 521, + 'gamma': 411, + 'eta': 603, + 'iota': 329, + 'phi1': 603, + 'kappa': 549, + 'lambda': 549, + 'mu': 576, + 'nu': 521, + 'omicron': 549, + 'pi': 549, + 'theta': 521, + 'rho': 549, + 'sigma': 603, + 'tau': 439, + 'upsilon': 576, + 'omega1': 713, + 'omega': 686, + 'xi': 493, + 'psi': 686, + 'zeta': 494, + 'braceleft': 480, + 'bar': 200, + 'braceright': 480, + 'similar': 549, + 'Euro': 750, + 'Upsilon1': 620, + 'minute': 247, + 'lessequal': 549, + 'fraction': 167, + 'infinity': 713, + 'florin': 500, + 'club': 753, + 'diamond': 753, + 'heart': 753, + 'spade': 753, + 'arrowboth': 1042, + 'arrowleft': 987, + 'arrowup': 603, + 'arrowright': 987, + 'arrowdown': 603, + 'degree': 400, + 'plusminus': 549, + 'second': 411, + 'greaterequal': 549, + 'multiply': 549, + 'proportional': 713, + 'partialdiff': 494, + 'bullet': 460, + 'divide': 549, + 'notequal': 549, + 'equivalence': 549, + 'approxequal': 549, + 'ellipsis': 1000, + 'arrowvertex': 603, + 'arrowhorizex': 1000, + 'carriagereturn': 658, + 'aleph': 823, + 'Ifraktur': 686, + 'Rfraktur': 795, + 'weierstrass': 987, + 'circlemultiply': 768, + 'circleplus': 768, + 'emptyset': 823, + 'intersection': 768, + 'union': 768, + 'propersuperset': 713, + 'reflexsuperset': 713, + 'notsubset': 713, + 'propersubset': 713, + 'reflexsubset': 713, + 'element': 713, + 'notelement': 713, + 'angle': 768, + 'gradient': 713, + 'registerserif': 790, + 'copyrightserif': 790, + 'trademarkserif': 890, + 'product': 823, + 'radical': 549, + 'dotmath': 250, + 'logicalnot': 713, + 'logicaland': 603, + 'logicalor': 603, + 'arrowdblboth': 1042, + 'arrowdblleft': 987, + 'arrowdblup': 603, + 'arrowdblright': 987, + 'arrowdbldown': 603, + 'lozenge': 494, + 'angleleft': 329, + 'registersans': 790, + 'copyrightsans': 790, + 'trademarksans': 786, + 'summation': 713, + 'parenlefttp': 384, + 'parenleftex': 384, + 'parenleftbt': 384, + 'bracketlefttp': 384, + 'bracketleftex': 384, + 'bracketleftbt': 384, + 'bracelefttp': 494, + 'braceleftmid': 494, + 'braceleftbt': 494, + 'braceex': 494, + 'angleright': 329, + 'integral': 274, + 'integraltp': 686, + 'integralex': 686, + 'integralbt': 686, + 'parenrighttp': 384, + 'parenrightex': 384, + 'parenrightbt': 384, + 'bracketrighttp': 384, + 'bracketrightex': 384, + 'bracketrightbt': 384, + 'bracerighttp': 494, + 'bracerightmid': 494, + 'bracerightbt': 494, + 'apple': 790 + }, + 'Times-Roman': { + 'space': 250, + 'exclam': 333, + 'quotedbl': 408, + 'numbersign': 500, + 'dollar': 500, + 'percent': 833, + 'ampersand': 778, + 'quoteright': 333, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 500, + 'plus': 564, + 'comma': 250, + 'hyphen': 333, + 'period': 250, + 'slash': 278, + 'zero': 500, + 'one': 500, + 'two': 500, + 'three': 500, + 'four': 500, + 'five': 500, + 'six': 500, + 'seven': 500, + 'eight': 500, + 'nine': 500, + 'colon': 278, + 'semicolon': 278, + 'less': 564, + 'equal': 564, + 'greater': 564, + 'question': 444, + 'at': 921, + 'A': 722, + 'B': 667, + 'C': 667, + 'D': 722, + 'E': 611, + 'F': 556, + 'G': 722, + 'H': 722, + 'I': 333, + 'J': 389, + 'K': 722, + 'L': 611, + 'M': 889, + 'N': 722, + 'O': 722, + 'P': 556, + 'Q': 722, + 'R': 667, + 'S': 556, + 'T': 611, + 'U': 722, + 'V': 722, + 'W': 944, + 'X': 722, + 'Y': 722, + 'Z': 611, + 'bracketleft': 333, + 'backslash': 278, + 'bracketright': 333, + 'asciicircum': 469, + 'underscore': 500, + 'quoteleft': 333, + 'a': 444, + 'b': 500, + 'c': 444, + 'd': 500, + 'e': 444, + 'f': 333, + 'g': 500, + 'h': 500, + 'i': 278, + 'j': 278, + 'k': 500, + 'l': 278, + 'm': 778, + 'n': 500, + 'o': 500, + 'p': 500, + 'q': 500, + 'r': 333, + 's': 389, + 't': 278, + 'u': 500, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'z': 444, + 'braceleft': 480, + 'bar': 200, + 'braceright': 480, + 'asciitilde': 541, + 'exclamdown': 333, + 'cent': 500, + 'sterling': 500, + 'fraction': 167, + 'yen': 500, + 'florin': 500, + 'section': 500, + 'currency': 500, + 'quotesingle': 180, + 'quotedblleft': 444, + 'guillemotleft': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 556, + 'fl': 556, + 'endash': 500, + 'dagger': 500, + 'daggerdbl': 500, + 'periodcentered': 250, + 'paragraph': 453, + 'bullet': 350, + 'quotesinglbase': 333, + 'quotedblbase': 444, + 'quotedblright': 444, + 'guillemotright': 500, + 'ellipsis': 1000, + 'perthousand': 1000, + 'questiondown': 444, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 1000, + 'AE': 889, + 'ordfeminine': 276, + 'Lslash': 611, + 'Oslash': 722, + 'OE': 889, + 'ordmasculine': 310, + 'ae': 667, + 'dotlessi': 278, + 'lslash': 278, + 'oslash': 500, + 'oe': 722, + 'germandbls': 500, + 'Idieresis': 333, + 'eacute': 444, + 'abreve': 444, + 'uhungarumlaut': 500, + 'ecaron': 444, + 'Ydieresis': 722, + 'divide': 564, + 'Yacute': 722, + 'Acircumflex': 722, + 'aacute': 444, + 'Ucircumflex': 722, + 'yacute': 500, + 'scommaaccent': 389, + 'ecircumflex': 444, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 444, + 'Uacute': 722, + 'uogonek': 500, + 'Edieresis': 611, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 760, + 'Emacron': 611, + 'ccaron': 444, + 'aring': 444, + 'Ncommaaccent': 722, + 'lacute': 278, + 'agrave': 444, + 'Tcommaaccent': 611, + 'Cacute': 667, + 'atilde': 444, + 'Edotaccent': 611, + 'scaron': 389, + 'scedilla': 389, + 'iacute': 278, + 'lozenge': 471, + 'Rcaron': 667, + 'Gcommaaccent': 722, + 'ucircumflex': 500, + 'acircumflex': 444, + 'Amacron': 722, + 'rcaron': 333, + 'ccedilla': 444, + 'Zdotaccent': 611, + 'Thorn': 556, + 'Omacron': 722, + 'Racute': 667, + 'Sacute': 556, + 'dcaron': 588, + 'Umacron': 722, + 'uring': 500, + 'threesuperior': 300, + 'Ograve': 722, + 'Agrave': 722, + 'Abreve': 722, + 'multiply': 564, + 'uacute': 500, + 'Tcaron': 611, + 'partialdiff': 476, + 'ydieresis': 500, + 'Nacute': 722, + 'icircumflex': 278, + 'Ecircumflex': 611, + 'adieresis': 444, + 'edieresis': 444, + 'cacute': 444, + 'nacute': 500, + 'umacron': 500, + 'Ncaron': 722, + 'Iacute': 333, + 'plusminus': 564, + 'brokenbar': 200, + 'registered': 760, + 'Gbreve': 722, + 'Idotaccent': 333, + 'summation': 600, + 'Egrave': 611, + 'racute': 333, + 'omacron': 500, + 'Zacute': 611, + 'Zcaron': 611, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 667, + 'lcommaaccent': 278, + 'tcaron': 326, + 'eogonek': 444, + 'Uogonek': 722, + 'Aacute': 722, + 'Adieresis': 722, + 'egrave': 444, + 'zacute': 444, + 'iogonek': 278, + 'Oacute': 722, + 'oacute': 500, + 'amacron': 444, + 'sacute': 389, + 'idieresis': 278, + 'Ocircumflex': 722, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 500, + 'twosuperior': 300, + 'Odieresis': 722, + 'mu': 500, + 'igrave': 278, + 'ohungarumlaut': 500, + 'Eogonek': 611, + 'dcroat': 500, + 'threequarters': 750, + 'Scedilla': 556, + 'lcaron': 344, + 'Kcommaaccent': 722, + 'Lacute': 611, + 'trademark': 980, + 'edotaccent': 444, + 'Igrave': 333, + 'Imacron': 333, + 'Lcaron': 611, + 'onehalf': 750, + 'lessequal': 549, + 'ocircumflex': 500, + 'ntilde': 500, + 'Uhungarumlaut': 722, + 'Eacute': 611, + 'emacron': 444, + 'gbreve': 500, + 'onequarter': 750, + 'Scaron': 556, + 'Scommaaccent': 556, + 'Ohungarumlaut': 722, + 'degree': 400, + 'ograve': 500, + 'Ccaron': 667, + 'ugrave': 500, + 'radical': 453, + 'Dcaron': 722, + 'rcommaaccent': 333, + 'Ntilde': 722, + 'otilde': 500, + 'Rcommaaccent': 667, + 'Lcommaaccent': 611, + 'Atilde': 722, + 'Aogonek': 722, + 'Aring': 722, + 'Otilde': 722, + 'zdotaccent': 444, + 'Ecaron': 611, + 'Iogonek': 333, + 'kcommaaccent': 500, + 'minus': 564, + 'Icircumflex': 333, + 'ncaron': 500, + 'tcommaaccent': 278, + 'logicalnot': 564, + 'odieresis': 500, + 'udieresis': 500, + 'notequal': 549, + 'gcommaaccent': 500, + 'eth': 500, + 'zcaron': 444, + 'ncommaaccent': 500, + 'onesuperior': 300, + 'imacron': 278, + 'Euro': 500 + }, + 'Times-Bold': { + 'space': 250, + 'exclam': 333, + 'quotedbl': 555, + 'numbersign': 500, + 'dollar': 500, + 'percent': 1000, + 'ampersand': 833, + 'quoteright': 333, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 500, + 'plus': 570, + 'comma': 250, + 'hyphen': 333, + 'period': 250, + 'slash': 278, + 'zero': 500, + 'one': 500, + 'two': 500, + 'three': 500, + 'four': 500, + 'five': 500, + 'six': 500, + 'seven': 500, + 'eight': 500, + 'nine': 500, + 'colon': 333, + 'semicolon': 333, + 'less': 570, + 'equal': 570, + 'greater': 570, + 'question': 500, + 'at': 930, + 'A': 722, + 'B': 667, + 'C': 722, + 'D': 722, + 'E': 667, + 'F': 611, + 'G': 778, + 'H': 778, + 'I': 389, + 'J': 500, + 'K': 778, + 'L': 667, + 'M': 944, + 'N': 722, + 'O': 778, + 'P': 611, + 'Q': 778, + 'R': 722, + 'S': 556, + 'T': 667, + 'U': 722, + 'V': 722, + 'W': 1000, + 'X': 722, + 'Y': 722, + 'Z': 667, + 'bracketleft': 333, + 'backslash': 278, + 'bracketright': 333, + 'asciicircum': 581, + 'underscore': 500, + 'quoteleft': 333, + 'a': 500, + 'b': 556, + 'c': 444, + 'd': 556, + 'e': 444, + 'f': 333, + 'g': 500, + 'h': 556, + 'i': 278, + 'j': 333, + 'k': 556, + 'l': 278, + 'm': 833, + 'n': 556, + 'o': 500, + 'p': 556, + 'q': 556, + 'r': 444, + 's': 389, + 't': 333, + 'u': 556, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'z': 444, + 'braceleft': 394, + 'bar': 220, + 'braceright': 394, + 'asciitilde': 520, + 'exclamdown': 333, + 'cent': 500, + 'sterling': 500, + 'fraction': 167, + 'yen': 500, + 'florin': 500, + 'section': 500, + 'currency': 500, + 'quotesingle': 278, + 'quotedblleft': 500, + 'guillemotleft': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 556, + 'fl': 556, + 'endash': 500, + 'dagger': 500, + 'daggerdbl': 500, + 'periodcentered': 250, + 'paragraph': 540, + 'bullet': 350, + 'quotesinglbase': 333, + 'quotedblbase': 500, + 'quotedblright': 500, + 'guillemotright': 500, + 'ellipsis': 1000, + 'perthousand': 1000, + 'questiondown': 500, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 1000, + 'AE': 1000, + 'ordfeminine': 300, + 'Lslash': 667, + 'Oslash': 778, + 'OE': 1000, + 'ordmasculine': 330, + 'ae': 722, + 'dotlessi': 278, + 'lslash': 278, + 'oslash': 500, + 'oe': 722, + 'germandbls': 556, + 'Idieresis': 389, + 'eacute': 444, + 'abreve': 500, + 'uhungarumlaut': 556, + 'ecaron': 444, + 'Ydieresis': 722, + 'divide': 570, + 'Yacute': 722, + 'Acircumflex': 722, + 'aacute': 500, + 'Ucircumflex': 722, + 'yacute': 500, + 'scommaaccent': 389, + 'ecircumflex': 444, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 500, + 'Uacute': 722, + 'uogonek': 556, + 'Edieresis': 667, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 747, + 'Emacron': 667, + 'ccaron': 444, + 'aring': 500, + 'Ncommaaccent': 722, + 'lacute': 278, + 'agrave': 500, + 'Tcommaaccent': 667, + 'Cacute': 722, + 'atilde': 500, + 'Edotaccent': 667, + 'scaron': 389, + 'scedilla': 389, + 'iacute': 278, + 'lozenge': 494, + 'Rcaron': 722, + 'Gcommaaccent': 778, + 'ucircumflex': 556, + 'acircumflex': 500, + 'Amacron': 722, + 'rcaron': 444, + 'ccedilla': 444, + 'Zdotaccent': 667, + 'Thorn': 611, + 'Omacron': 778, + 'Racute': 722, + 'Sacute': 556, + 'dcaron': 672, + 'Umacron': 722, + 'uring': 556, + 'threesuperior': 300, + 'Ograve': 778, + 'Agrave': 722, + 'Abreve': 722, + 'multiply': 570, + 'uacute': 556, + 'Tcaron': 667, + 'partialdiff': 494, + 'ydieresis': 500, + 'Nacute': 722, + 'icircumflex': 278, + 'Ecircumflex': 667, + 'adieresis': 500, + 'edieresis': 444, + 'cacute': 444, + 'nacute': 556, + 'umacron': 556, + 'Ncaron': 722, + 'Iacute': 389, + 'plusminus': 570, + 'brokenbar': 220, + 'registered': 747, + 'Gbreve': 778, + 'Idotaccent': 389, + 'summation': 600, + 'Egrave': 667, + 'racute': 444, + 'omacron': 500, + 'Zacute': 667, + 'Zcaron': 667, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 722, + 'lcommaaccent': 278, + 'tcaron': 416, + 'eogonek': 444, + 'Uogonek': 722, + 'Aacute': 722, + 'Adieresis': 722, + 'egrave': 444, + 'zacute': 444, + 'iogonek': 278, + 'Oacute': 778, + 'oacute': 500, + 'amacron': 500, + 'sacute': 389, + 'idieresis': 278, + 'Ocircumflex': 778, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 556, + 'twosuperior': 300, + 'Odieresis': 778, + 'mu': 556, + 'igrave': 278, + 'ohungarumlaut': 500, + 'Eogonek': 667, + 'dcroat': 556, + 'threequarters': 750, + 'Scedilla': 556, + 'lcaron': 394, + 'Kcommaaccent': 778, + 'Lacute': 667, + 'trademark': 1000, + 'edotaccent': 444, + 'Igrave': 389, + 'Imacron': 389, + 'Lcaron': 667, + 'onehalf': 750, + 'lessequal': 549, + 'ocircumflex': 500, + 'ntilde': 556, + 'Uhungarumlaut': 722, + 'Eacute': 667, + 'emacron': 444, + 'gbreve': 500, + 'onequarter': 750, + 'Scaron': 556, + 'Scommaaccent': 556, + 'Ohungarumlaut': 778, + 'degree': 400, + 'ograve': 500, + 'Ccaron': 722, + 'ugrave': 556, + 'radical': 549, + 'Dcaron': 722, + 'rcommaaccent': 444, + 'Ntilde': 722, + 'otilde': 500, + 'Rcommaaccent': 722, + 'Lcommaaccent': 667, + 'Atilde': 722, + 'Aogonek': 722, + 'Aring': 722, + 'Otilde': 778, + 'zdotaccent': 444, + 'Ecaron': 667, + 'Iogonek': 389, + 'kcommaaccent': 556, + 'minus': 570, + 'Icircumflex': 389, + 'ncaron': 556, + 'tcommaaccent': 333, + 'logicalnot': 570, + 'odieresis': 500, + 'udieresis': 556, + 'notequal': 549, + 'gcommaaccent': 500, + 'eth': 500, + 'zcaron': 444, + 'ncommaaccent': 556, + 'onesuperior': 300, + 'imacron': 278, + 'Euro': 500 + }, + 'Times-BoldItalic': { + 'space': 250, + 'exclam': 389, + 'quotedbl': 555, + 'numbersign': 500, + 'dollar': 500, + 'percent': 833, + 'ampersand': 778, + 'quoteright': 333, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 500, + 'plus': 570, + 'comma': 250, + 'hyphen': 333, + 'period': 250, + 'slash': 278, + 'zero': 500, + 'one': 500, + 'two': 500, + 'three': 500, + 'four': 500, + 'five': 500, + 'six': 500, + 'seven': 500, + 'eight': 500, + 'nine': 500, + 'colon': 333, + 'semicolon': 333, + 'less': 570, + 'equal': 570, + 'greater': 570, + 'question': 500, + 'at': 832, + 'A': 667, + 'B': 667, + 'C': 667, + 'D': 722, + 'E': 667, + 'F': 667, + 'G': 722, + 'H': 778, + 'I': 389, + 'J': 500, + 'K': 667, + 'L': 611, + 'M': 889, + 'N': 722, + 'O': 722, + 'P': 611, + 'Q': 722, + 'R': 667, + 'S': 556, + 'T': 611, + 'U': 722, + 'V': 667, + 'W': 889, + 'X': 667, + 'Y': 611, + 'Z': 611, + 'bracketleft': 333, + 'backslash': 278, + 'bracketright': 333, + 'asciicircum': 570, + 'underscore': 500, + 'quoteleft': 333, + 'a': 500, + 'b': 500, + 'c': 444, + 'd': 500, + 'e': 444, + 'f': 333, + 'g': 500, + 'h': 556, + 'i': 278, + 'j': 278, + 'k': 500, + 'l': 278, + 'm': 778, + 'n': 556, + 'o': 500, + 'p': 500, + 'q': 500, + 'r': 389, + 's': 389, + 't': 278, + 'u': 556, + 'v': 444, + 'w': 667, + 'x': 500, + 'y': 444, + 'z': 389, + 'braceleft': 348, + 'bar': 220, + 'braceright': 348, + 'asciitilde': 570, + 'exclamdown': 389, + 'cent': 500, + 'sterling': 500, + 'fraction': 167, + 'yen': 500, + 'florin': 500, + 'section': 500, + 'currency': 500, + 'quotesingle': 278, + 'quotedblleft': 500, + 'guillemotleft': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 556, + 'fl': 556, + 'endash': 500, + 'dagger': 500, + 'daggerdbl': 500, + 'periodcentered': 250, + 'paragraph': 500, + 'bullet': 350, + 'quotesinglbase': 333, + 'quotedblbase': 500, + 'quotedblright': 500, + 'guillemotright': 500, + 'ellipsis': 1000, + 'perthousand': 1000, + 'questiondown': 500, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 1000, + 'AE': 944, + 'ordfeminine': 266, + 'Lslash': 611, + 'Oslash': 722, + 'OE': 944, + 'ordmasculine': 300, + 'ae': 722, + 'dotlessi': 278, + 'lslash': 278, + 'oslash': 500, + 'oe': 722, + 'germandbls': 500, + 'Idieresis': 389, + 'eacute': 444, + 'abreve': 500, + 'uhungarumlaut': 556, + 'ecaron': 444, + 'Ydieresis': 611, + 'divide': 570, + 'Yacute': 611, + 'Acircumflex': 667, + 'aacute': 500, + 'Ucircumflex': 722, + 'yacute': 444, + 'scommaaccent': 389, + 'ecircumflex': 444, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 500, + 'Uacute': 722, + 'uogonek': 556, + 'Edieresis': 667, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 747, + 'Emacron': 667, + 'ccaron': 444, + 'aring': 500, + 'Ncommaaccent': 722, + 'lacute': 278, + 'agrave': 500, + 'Tcommaaccent': 611, + 'Cacute': 667, + 'atilde': 500, + 'Edotaccent': 667, + 'scaron': 389, + 'scedilla': 389, + 'iacute': 278, + 'lozenge': 494, + 'Rcaron': 667, + 'Gcommaaccent': 722, + 'ucircumflex': 556, + 'acircumflex': 500, + 'Amacron': 667, + 'rcaron': 389, + 'ccedilla': 444, + 'Zdotaccent': 611, + 'Thorn': 611, + 'Omacron': 722, + 'Racute': 667, + 'Sacute': 556, + 'dcaron': 608, + 'Umacron': 722, + 'uring': 556, + 'threesuperior': 300, + 'Ograve': 722, + 'Agrave': 667, + 'Abreve': 667, + 'multiply': 570, + 'uacute': 556, + 'Tcaron': 611, + 'partialdiff': 494, + 'ydieresis': 444, + 'Nacute': 722, + 'icircumflex': 278, + 'Ecircumflex': 667, + 'adieresis': 500, + 'edieresis': 444, + 'cacute': 444, + 'nacute': 556, + 'umacron': 556, + 'Ncaron': 722, + 'Iacute': 389, + 'plusminus': 570, + 'brokenbar': 220, + 'registered': 747, + 'Gbreve': 722, + 'Idotaccent': 389, + 'summation': 600, + 'Egrave': 667, + 'racute': 389, + 'omacron': 500, + 'Zacute': 611, + 'Zcaron': 611, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 667, + 'lcommaaccent': 278, + 'tcaron': 366, + 'eogonek': 444, + 'Uogonek': 722, + 'Aacute': 667, + 'Adieresis': 667, + 'egrave': 444, + 'zacute': 389, + 'iogonek': 278, + 'Oacute': 722, + 'oacute': 500, + 'amacron': 500, + 'sacute': 389, + 'idieresis': 278, + 'Ocircumflex': 722, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 500, + 'twosuperior': 300, + 'Odieresis': 722, + 'mu': 576, + 'igrave': 278, + 'ohungarumlaut': 500, + 'Eogonek': 667, + 'dcroat': 500, + 'threequarters': 750, + 'Scedilla': 556, + 'lcaron': 382, + 'Kcommaaccent': 667, + 'Lacute': 611, + 'trademark': 1000, + 'edotaccent': 444, + 'Igrave': 389, + 'Imacron': 389, + 'Lcaron': 611, + 'onehalf': 750, + 'lessequal': 549, + 'ocircumflex': 500, + 'ntilde': 556, + 'Uhungarumlaut': 722, + 'Eacute': 667, + 'emacron': 444, + 'gbreve': 500, + 'onequarter': 750, + 'Scaron': 556, + 'Scommaaccent': 556, + 'Ohungarumlaut': 722, + 'degree': 400, + 'ograve': 500, + 'Ccaron': 667, + 'ugrave': 556, + 'radical': 549, + 'Dcaron': 722, + 'rcommaaccent': 389, + 'Ntilde': 722, + 'otilde': 500, + 'Rcommaaccent': 667, + 'Lcommaaccent': 611, + 'Atilde': 667, + 'Aogonek': 667, + 'Aring': 667, + 'Otilde': 722, + 'zdotaccent': 389, + 'Ecaron': 667, + 'Iogonek': 389, + 'kcommaaccent': 500, + 'minus': 606, + 'Icircumflex': 389, + 'ncaron': 556, + 'tcommaaccent': 278, + 'logicalnot': 606, + 'odieresis': 500, + 'udieresis': 556, + 'notequal': 549, + 'gcommaaccent': 500, + 'eth': 500, + 'zcaron': 389, + 'ncommaaccent': 556, + 'onesuperior': 300, + 'imacron': 278, + 'Euro': 500 + }, + 'Times-Italic': { + 'space': 250, + 'exclam': 333, + 'quotedbl': 420, + 'numbersign': 500, + 'dollar': 500, + 'percent': 833, + 'ampersand': 778, + 'quoteright': 333, + 'parenleft': 333, + 'parenright': 333, + 'asterisk': 500, + 'plus': 675, + 'comma': 250, + 'hyphen': 333, + 'period': 250, + 'slash': 278, + 'zero': 500, + 'one': 500, + 'two': 500, + 'three': 500, + 'four': 500, + 'five': 500, + 'six': 500, + 'seven': 500, + 'eight': 500, + 'nine': 500, + 'colon': 333, + 'semicolon': 333, + 'less': 675, + 'equal': 675, + 'greater': 675, + 'question': 500, + 'at': 920, + 'A': 611, + 'B': 611, + 'C': 667, + 'D': 722, + 'E': 611, + 'F': 611, + 'G': 722, + 'H': 722, + 'I': 333, + 'J': 444, + 'K': 667, + 'L': 556, + 'M': 833, + 'N': 667, + 'O': 722, + 'P': 611, + 'Q': 722, + 'R': 611, + 'S': 500, + 'T': 556, + 'U': 722, + 'V': 611, + 'W': 833, + 'X': 611, + 'Y': 556, + 'Z': 556, + 'bracketleft': 389, + 'backslash': 278, + 'bracketright': 389, + 'asciicircum': 422, + 'underscore': 500, + 'quoteleft': 333, + 'a': 500, + 'b': 500, + 'c': 444, + 'd': 500, + 'e': 444, + 'f': 278, + 'g': 500, + 'h': 500, + 'i': 278, + 'j': 278, + 'k': 444, + 'l': 278, + 'm': 722, + 'n': 500, + 'o': 500, + 'p': 500, + 'q': 500, + 'r': 389, + 's': 389, + 't': 278, + 'u': 500, + 'v': 444, + 'w': 667, + 'x': 444, + 'y': 444, + 'z': 389, + 'braceleft': 400, + 'bar': 275, + 'braceright': 400, + 'asciitilde': 541, + 'exclamdown': 389, + 'cent': 500, + 'sterling': 500, + 'fraction': 167, + 'yen': 500, + 'florin': 500, + 'section': 500, + 'currency': 500, + 'quotesingle': 214, + 'quotedblleft': 556, + 'guillemotleft': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'fi': 500, + 'fl': 500, + 'endash': 500, + 'dagger': 500, + 'daggerdbl': 500, + 'periodcentered': 250, + 'paragraph': 523, + 'bullet': 350, + 'quotesinglbase': 333, + 'quotedblbase': 556, + 'quotedblright': 556, + 'guillemotright': 500, + 'ellipsis': 889, + 'perthousand': 1000, + 'questiondown': 500, + 'grave': 333, + 'acute': 333, + 'circumflex': 333, + 'tilde': 333, + 'macron': 333, + 'breve': 333, + 'dotaccent': 333, + 'dieresis': 333, + 'ring': 333, + 'cedilla': 333, + 'hungarumlaut': 333, + 'ogonek': 333, + 'caron': 333, + 'emdash': 889, + 'AE': 889, + 'ordfeminine': 276, + 'Lslash': 556, + 'Oslash': 722, + 'OE': 944, + 'ordmasculine': 310, + 'ae': 667, + 'dotlessi': 278, + 'lslash': 278, + 'oslash': 500, + 'oe': 667, + 'germandbls': 500, + 'Idieresis': 333, + 'eacute': 444, + 'abreve': 500, + 'uhungarumlaut': 500, + 'ecaron': 444, + 'Ydieresis': 556, + 'divide': 675, + 'Yacute': 556, + 'Acircumflex': 611, + 'aacute': 500, + 'Ucircumflex': 722, + 'yacute': 444, + 'scommaaccent': 389, + 'ecircumflex': 444, + 'Uring': 722, + 'Udieresis': 722, + 'aogonek': 500, + 'Uacute': 722, + 'uogonek': 500, + 'Edieresis': 611, + 'Dcroat': 722, + 'commaaccent': 250, + 'copyright': 760, + 'Emacron': 611, + 'ccaron': 444, + 'aring': 500, + 'Ncommaaccent': 667, + 'lacute': 278, + 'agrave': 500, + 'Tcommaaccent': 556, + 'Cacute': 667, + 'atilde': 500, + 'Edotaccent': 611, + 'scaron': 389, + 'scedilla': 389, + 'iacute': 278, + 'lozenge': 471, + 'Rcaron': 611, + 'Gcommaaccent': 722, + 'ucircumflex': 500, + 'acircumflex': 500, + 'Amacron': 611, + 'rcaron': 389, + 'ccedilla': 444, + 'Zdotaccent': 556, + 'Thorn': 611, + 'Omacron': 722, + 'Racute': 611, + 'Sacute': 500, + 'dcaron': 544, + 'Umacron': 722, + 'uring': 500, + 'threesuperior': 300, + 'Ograve': 722, + 'Agrave': 611, + 'Abreve': 611, + 'multiply': 675, + 'uacute': 500, + 'Tcaron': 556, + 'partialdiff': 476, + 'ydieresis': 444, + 'Nacute': 667, + 'icircumflex': 278, + 'Ecircumflex': 611, + 'adieresis': 500, + 'edieresis': 444, + 'cacute': 444, + 'nacute': 500, + 'umacron': 500, + 'Ncaron': 667, + 'Iacute': 333, + 'plusminus': 675, + 'brokenbar': 275, + 'registered': 760, + 'Gbreve': 722, + 'Idotaccent': 333, + 'summation': 600, + 'Egrave': 611, + 'racute': 389, + 'omacron': 500, + 'Zacute': 556, + 'Zcaron': 556, + 'greaterequal': 549, + 'Eth': 722, + 'Ccedilla': 667, + 'lcommaaccent': 278, + 'tcaron': 300, + 'eogonek': 444, + 'Uogonek': 722, + 'Aacute': 611, + 'Adieresis': 611, + 'egrave': 444, + 'zacute': 389, + 'iogonek': 278, + 'Oacute': 722, + 'oacute': 500, + 'amacron': 500, + 'sacute': 389, + 'idieresis': 278, + 'Ocircumflex': 722, + 'Ugrave': 722, + 'Delta': 612, + 'thorn': 500, + 'twosuperior': 300, + 'Odieresis': 722, + 'mu': 500, + 'igrave': 278, + 'ohungarumlaut': 500, + 'Eogonek': 611, + 'dcroat': 500, + 'threequarters': 750, + 'Scedilla': 500, + 'lcaron': 300, + 'Kcommaaccent': 667, + 'Lacute': 556, + 'trademark': 980, + 'edotaccent': 444, + 'Igrave': 333, + 'Imacron': 333, + 'Lcaron': 611, + 'onehalf': 750, + 'lessequal': 549, + 'ocircumflex': 500, + 'ntilde': 500, + 'Uhungarumlaut': 722, + 'Eacute': 611, + 'emacron': 444, + 'gbreve': 500, + 'onequarter': 750, + 'Scaron': 500, + 'Scommaaccent': 500, + 'Ohungarumlaut': 722, + 'degree': 400, + 'ograve': 500, + 'Ccaron': 667, + 'ugrave': 500, + 'radical': 453, + 'Dcaron': 722, + 'rcommaaccent': 389, + 'Ntilde': 667, + 'otilde': 500, + 'Rcommaaccent': 611, + 'Lcommaaccent': 556, + 'Atilde': 611, + 'Aogonek': 611, + 'Aring': 611, + 'Otilde': 722, + 'zdotaccent': 389, + 'Ecaron': 611, + 'Iogonek': 333, + 'kcommaaccent': 444, + 'minus': 675, + 'Icircumflex': 333, + 'ncaron': 500, + 'tcommaaccent': 278, + 'logicalnot': 675, + 'odieresis': 500, + 'udieresis': 500, + 'notequal': 549, + 'gcommaaccent': 500, + 'eth': 500, + 'zcaron': 389, + 'ncommaaccent': 500, + 'onesuperior': 300, + 'imacron': 278, + 'Euro': 500 + }, + 'ZapfDingbats': { + 'space': 278, + 'a1': 974, + 'a2': 961, + 'a202': 974, + 'a3': 980, + 'a4': 719, + 'a5': 789, + 'a119': 790, + 'a118': 791, + 'a117': 690, + 'a11': 960, + 'a12': 939, + 'a13': 549, + 'a14': 855, + 'a15': 911, + 'a16': 933, + 'a105': 911, + 'a17': 945, + 'a18': 974, + 'a19': 755, + 'a20': 846, + 'a21': 762, + 'a22': 761, + 'a23': 571, + 'a24': 677, + 'a25': 763, + 'a26': 760, + 'a27': 759, + 'a28': 754, + 'a6': 494, + 'a7': 552, + 'a8': 537, + 'a9': 577, + 'a10': 692, + 'a29': 786, + 'a30': 788, + 'a31': 788, + 'a32': 790, + 'a33': 793, + 'a34': 794, + 'a35': 816, + 'a36': 823, + 'a37': 789, + 'a38': 841, + 'a39': 823, + 'a40': 833, + 'a41': 816, + 'a42': 831, + 'a43': 923, + 'a44': 744, + 'a45': 723, + 'a46': 749, + 'a47': 790, + 'a48': 792, + 'a49': 695, + 'a50': 776, + 'a51': 768, + 'a52': 792, + 'a53': 759, + 'a54': 707, + 'a55': 708, + 'a56': 682, + 'a57': 701, + 'a58': 826, + 'a59': 815, + 'a60': 789, + 'a61': 789, + 'a62': 707, + 'a63': 687, + 'a64': 696, + 'a65': 689, + 'a66': 786, + 'a67': 787, + 'a68': 713, + 'a69': 791, + 'a70': 785, + 'a71': 791, + 'a72': 873, + 'a73': 761, + 'a74': 762, + 'a203': 762, + 'a75': 759, + 'a204': 759, + 'a76': 892, + 'a77': 892, + 'a78': 788, + 'a79': 784, + 'a81': 438, + 'a82': 138, + 'a83': 277, + 'a84': 415, + 'a97': 392, + 'a98': 392, + 'a99': 668, + 'a100': 668, + 'a89': 390, + 'a90': 390, + 'a93': 317, + 'a94': 317, + 'a91': 276, + 'a92': 276, + 'a205': 509, + 'a85': 509, + 'a206': 410, + 'a86': 410, + 'a87': 234, + 'a88': 234, + 'a95': 334, + 'a96': 334, + 'a101': 732, + 'a102': 544, + 'a103': 544, + 'a104': 910, + 'a106': 667, + 'a107': 760, + 'a108': 760, + 'a112': 776, + 'a111': 595, + 'a110': 694, + 'a109': 626, + 'a120': 788, + 'a121': 788, + 'a122': 788, + 'a123': 788, + 'a124': 788, + 'a125': 788, + 'a126': 788, + 'a127': 788, + 'a128': 788, + 'a129': 788, + 'a130': 788, + 'a131': 788, + 'a132': 788, + 'a133': 788, + 'a134': 788, + 'a135': 788, + 'a136': 788, + 'a137': 788, + 'a138': 788, + 'a139': 788, + 'a140': 788, + 'a141': 788, + 'a142': 788, + 'a143': 788, + 'a144': 788, + 'a145': 788, + 'a146': 788, + 'a147': 788, + 'a148': 788, + 'a149': 788, + 'a150': 788, + 'a151': 788, + 'a152': 788, + 'a153': 788, + 'a154': 788, + 'a155': 788, + 'a156': 788, + 'a157': 788, + 'a158': 788, + 'a159': 788, + 'a160': 894, + 'a161': 838, + 'a163': 1016, + 'a164': 458, + 'a196': 748, + 'a165': 924, + 'a192': 748, + 'a166': 918, + 'a167': 927, + 'a168': 928, + 'a169': 928, + 'a170': 834, + 'a171': 873, + 'a172': 828, + 'a173': 924, + 'a162': 924, + 'a174': 917, + 'a175': 930, + 'a176': 931, + 'a177': 463, + 'a178': 883, + 'a179': 836, + 'a193': 836, + 'a180': 867, + 'a199': 867, + 'a181': 696, + 'a200': 696, + 'a182': 874, + 'a201': 874, + 'a183': 760, + 'a184': 946, + 'a197': 771, + 'a185': 865, + 'a194': 771, + 'a198': 888, + 'a186': 967, + 'a195': 888, + 'a187': 831, + 'a188': 873, + 'a189': 927, + 'a190': 970, + 'a191': 918 + } +}; + + +var EOF = {}; + +function isEOF(v) { + return (v === EOF); +} + +var MAX_LENGTH_TO_CACHE = 1000; + +var Parser = (function ParserClosure() { + function Parser(lexer, allowStreams, xref) { + this.lexer = lexer; + this.allowStreams = allowStreams; + this.xref = xref; + this.imageCache = {}; + this.refill(); + } + + Parser.prototype = { + refill: function Parser_refill() { + this.buf1 = this.lexer.getObj(); + this.buf2 = this.lexer.getObj(); + }, + shift: function Parser_shift() { + if (isCmd(this.buf2, 'ID')) { + this.buf1 = this.buf2; + this.buf2 = null; + } else { + this.buf1 = this.buf2; + this.buf2 = this.lexer.getObj(); + } + }, + getObj: function Parser_getObj(cipherTransform) { + var buf1 = this.buf1; + this.shift(); + + if (buf1 instanceof Cmd) { + switch (buf1.cmd) { + case 'BI': // inline image + return this.makeInlineImage(cipherTransform); + case '[': // array + var array = []; + while (!isCmd(this.buf1, ']') && !isEOF(this.buf1)) { + array.push(this.getObj(cipherTransform)); + } + if (isEOF(this.buf1)) { + error('End of file inside array'); + } + this.shift(); + return array; + case '<<': // dictionary or stream + var dict = new Dict(this.xref); + while (!isCmd(this.buf1, '>>') && !isEOF(this.buf1)) { + if (!isName(this.buf1)) { + info('Malformed dictionary: key must be a name object'); + this.shift(); + continue; + } + + var key = this.buf1.name; + this.shift(); + if (isEOF(this.buf1)) { + break; + } + dict.set(key, this.getObj(cipherTransform)); + } + if (isEOF(this.buf1)) { + error('End of file inside dictionary'); + } + + // Stream objects are not allowed inside content streams or + // object streams. + if (isCmd(this.buf2, 'stream')) { + return (this.allowStreams ? + this.makeStream(dict, cipherTransform) : dict); + } + this.shift(); + return dict; + default: // simple object + return buf1; + } + } + + if (isInt(buf1)) { // indirect reference or integer + var num = buf1; + if (isInt(this.buf1) && isCmd(this.buf2, 'R')) { + var ref = new Ref(num, this.buf1); + this.shift(); + this.shift(); + return ref; + } + return num; + } + + if (isString(buf1)) { // string + var str = buf1; + if (cipherTransform) { + str = cipherTransform.decryptString(str); + } + return str; + } + + // simple object + return buf1; + }, + /** + * Find the end of the stream by searching for the /EI\s/. + * @returns {number} The inline stream length. + */ + findDefaultInlineStreamEnd: + function Parser_findDefaultInlineStreamEnd(stream) { + var E = 0x45, I = 0x49, SPACE = 0x20, LF = 0xA, CR = 0xD; + var startPos = stream.pos, state = 0, ch, i, n, followingBytes; + while ((ch = stream.getByte()) !== -1) { + if (state === 0) { + state = (ch === E) ? 1 : 0; + } else if (state === 1) { + state = (ch === I) ? 2 : 0; + } else { + assert(state === 2); + if (ch === SPACE || ch === LF || ch === CR) { + // Let's check the next five bytes are ASCII... just be sure. + n = 5; + followingBytes = stream.peekBytes(n); + for (i = 0; i < n; i++) { + ch = followingBytes[i]; + if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7F)) { + // Not a LF, CR, SPACE or any visible ASCII character, i.e. + // it's binary stuff. Resetting the state. + state = 0; + break; + } + } + if (state === 2) { + break; // Finished! + } + } else { + state = 0; + } + } + } + return ((stream.pos - 4) - startPos); + }, + /** + * Find the EOI (end-of-image) marker 0xFFD9 of the stream. + * @returns {number} The inline stream length. + */ + findDCTDecodeInlineStreamEnd: + function Parser_findDCTDecodeInlineStreamEnd(stream) { + var startPos = stream.pos, foundEOI = false, b, markerLength, length; + while ((b = stream.getByte()) !== -1) { + if (b !== 0xFF) { // Not a valid marker. + continue; + } + switch (stream.getByte()) { + case 0x00: // Byte stuffing. + // 0xFF00 appears to be a very common byte sequence in JPEG images. + break; + + case 0xFF: // Fill byte. + // Avoid skipping a valid marker, resetting the stream position. + stream.skip(-1); + break; + + case 0xD9: // EOI + foundEOI = true; + break; + + case 0xC0: // SOF0 + case 0xC1: // SOF1 + case 0xC2: // SOF2 + case 0xC3: // SOF3 + + case 0xC5: // SOF5 + case 0xC6: // SOF6 + case 0xC7: // SOF7 + + case 0xC9: // SOF9 + case 0xCA: // SOF10 + case 0xCB: // SOF11 + + case 0xCD: // SOF13 + case 0xCE: // SOF14 + case 0xCF: // SOF15 + + case 0xC4: // DHT + case 0xCC: // DAC + + case 0xDA: // SOS + case 0xDB: // DQT + case 0xDC: // DNL + case 0xDD: // DRI + case 0xDE: // DHP + case 0xDF: // EXP + + case 0xE0: // APP0 + case 0xE1: // APP1 + case 0xE2: // APP2 + case 0xE3: // APP3 + case 0xE4: // APP4 + case 0xE5: // APP5 + case 0xE6: // APP6 + case 0xE7: // APP7 + case 0xE8: // APP8 + case 0xE9: // APP9 + case 0xEA: // APP10 + case 0xEB: // APP11 + case 0xEC: // APP12 + case 0xED: // APP13 + case 0xEE: // APP14 + case 0xEF: // APP15 + + case 0xFE: // COM + // The marker should be followed by the length of the segment. + markerLength = stream.getUint16(); + if (markerLength > 2) { + // |markerLength| contains the byte length of the marker segment, + // including its own length (2 bytes) and excluding the marker. + stream.skip(markerLength - 2); // Jump to the next marker. + } else { + // The marker length is invalid, resetting the stream position. + stream.skip(-2); + } + break; + } + if (foundEOI) { + break; + } + } + length = stream.pos - startPos; + if (b === -1) { + warn('Inline DCTDecode image stream: ' + + 'EOI marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCII85DecodeInlineStreamEnd: + function Parser_findASCII85DecodeInlineStreamEnd(stream) { + var TILDE = 0x7E, GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === TILDE && stream.peekByte() === GT) { + stream.skip(); + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCII85Decode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Find the EOD (end-of-data) marker '>' (i.e. GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCIIHexDecodeInlineStreamEnd: + function Parser_findASCIIHexDecodeInlineStreamEnd(stream) { + var GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === GT) { + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCIIHexDecode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Skip over the /EI/ for streams where we search for an EOD marker. + */ + inlineStreamSkipEI: function Parser_inlineStreamSkipEI(stream) { + var E = 0x45, I = 0x49; + var state = 0, ch; + while ((ch = stream.getByte()) !== -1) { + if (state === 0) { + state = (ch === E) ? 1 : 0; + } else if (state === 1) { + state = (ch === I) ? 2 : 0; + } else if (state === 2) { + break; + } + } + }, + makeInlineImage: function Parser_makeInlineImage(cipherTransform) { + var lexer = this.lexer; + var stream = lexer.stream; + + // Parse dictionary. + var dict = new Dict(null); + while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { + if (!isName(this.buf1)) { + error('Dictionary key must be a name object'); + } + var key = this.buf1.name; + this.shift(); + if (isEOF(this.buf1)) { + break; + } + dict.set(key, this.getObj(cipherTransform)); + } + + // Extract the name of the first (i.e. the current) image filter. + var filter = this.fetchIfRef(dict.get('Filter', 'F')), filterName; + if (isName(filter)) { + filterName = filter.name; + } else if (isArray(filter) && isName(filter[0])) { + filterName = filter[0].name; + } + + // Parse image stream. + var startPos = stream.pos, length, i, ii; + if (filterName === 'DCTDecode' || filterName === 'DCT') { + length = this.findDCTDecodeInlineStreamEnd(stream); + } else if (filterName === 'ASCII85Decide' || filterName === 'A85') { + length = this.findASCII85DecodeInlineStreamEnd(stream); + } else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') { + length = this.findASCIIHexDecodeInlineStreamEnd(stream); + } else { + length = this.findDefaultInlineStreamEnd(stream); + } + var imageStream = stream.makeSubStream(startPos, length, dict); + + // Cache all images below the MAX_LENGTH_TO_CACHE threshold by their + // adler32 checksum. + var adler32; + if (length < MAX_LENGTH_TO_CACHE) { + var imageBytes = imageStream.getBytes(); + imageStream.reset(); + + var a = 1; + var b = 0; + for (i = 0, ii = imageBytes.length; i < ii; ++i) { + // No modulo required in the loop if imageBytes.length < 5552. + a += imageBytes[i] & 0xff; + b += a; + } + adler32 = ((b % 65521) << 16) | (a % 65521); + + if (this.imageCache.adler32 === adler32) { + this.buf2 = Cmd.get('EI'); + this.shift(); + + this.imageCache[adler32].reset(); + return this.imageCache[adler32]; + } + } + + if (cipherTransform) { + imageStream = cipherTransform.createStream(imageStream, length); + } + + imageStream = this.filter(imageStream, dict, length); + imageStream.dict = dict; + if (adler32 !== undefined) { + imageStream.cacheKey = 'inline_' + length + '_' + adler32; + this.imageCache[adler32] = imageStream; + } + + this.buf2 = Cmd.get('EI'); + this.shift(); + + return imageStream; + }, + fetchIfRef: function Parser_fetchIfRef(obj) { + // not relying on the xref.fetchIfRef -- xref might not be set + return (isRef(obj) ? this.xref.fetch(obj) : obj); + }, + makeStream: function Parser_makeStream(dict, cipherTransform) { + var lexer = this.lexer; + var stream = lexer.stream; + + // get stream start position + lexer.skipToNextLine(); + var pos = stream.pos - 1; + + // get length + var length = this.fetchIfRef(dict.get('Length')); + if (!isInt(length)) { + info('Bad ' + length + ' attribute in stream'); + length = 0; + } + + // skip over the stream data + stream.pos = pos + length; + lexer.nextChar(); + + this.shift(); // '>>' + this.shift(); // 'stream' + if (!isCmd(this.buf1, 'endstream')) { + // bad stream length, scanning for endstream + stream.pos = pos; + var SCAN_BLOCK_SIZE = 2048; + var ENDSTREAM_SIGNATURE_LENGTH = 9; + var ENDSTREAM_SIGNATURE = [0x65, 0x6E, 0x64, 0x73, 0x74, 0x72, 0x65, + 0x61, 0x6D]; + var skipped = 0, found = false, i, j; + while (stream.pos < stream.end) { + var scanBytes = stream.peekBytes(SCAN_BLOCK_SIZE); + var scanLength = scanBytes.length - ENDSTREAM_SIGNATURE_LENGTH; + if (scanLength <= 0) { + break; + } + found = false; + for (i = 0, j = 0; i < scanLength; i++) { + var b = scanBytes[i]; + if (b !== ENDSTREAM_SIGNATURE[j]) { + i -= j; + j = 0; + } else { + j++; + if (j >= ENDSTREAM_SIGNATURE_LENGTH) { + i++; + found = true; + break; + } + } + } + if (found) { + skipped += i - ENDSTREAM_SIGNATURE_LENGTH; + stream.pos += i - ENDSTREAM_SIGNATURE_LENGTH; + break; + } + skipped += scanLength; + stream.pos += scanLength; + } + if (!found) { + error('Missing endstream'); + } + length = skipped; + + lexer.nextChar(); + this.shift(); + this.shift(); + } + this.shift(); // 'endstream' + + stream = stream.makeSubStream(pos, length, dict); + if (cipherTransform) { + stream = cipherTransform.createStream(stream, length); + } + stream = this.filter(stream, dict, length); + stream.dict = dict; + return stream; + }, + filter: function Parser_filter(stream, dict, length) { + var filter = this.fetchIfRef(dict.get('Filter', 'F')); + var params = this.fetchIfRef(dict.get('DecodeParms', 'DP')); + if (isName(filter)) { + return this.makeFilter(stream, filter.name, length, params); + } + + var maybeLength = length; + if (isArray(filter)) { + var filterArray = filter; + var paramsArray = params; + for (var i = 0, ii = filterArray.length; i < ii; ++i) { + filter = filterArray[i]; + if (!isName(filter)) { + error('Bad filter name: ' + filter); + } + + params = null; + if (isArray(paramsArray) && (i in paramsArray)) { + params = paramsArray[i]; + } + stream = this.makeFilter(stream, filter.name, maybeLength, params); + // after the first stream the length variable is invalid + maybeLength = null; + } + } + return stream; + }, + makeFilter: function Parser_makeFilter(stream, name, maybeLength, params) { + if (stream.dict.get('Length') === 0) { + return new NullStream(stream); + } + try { + if (params) { + params = this.fetchIfRef(params); + } + var xrefStreamStats = this.xref.stats.streamTypes; + if (name === 'FlateDecode' || name === 'Fl') { + xrefStreamStats[StreamType.FLATE] = true; + if (params) { + return new PredictorStream(new FlateStream(stream, maybeLength), + maybeLength, params); + } + return new FlateStream(stream, maybeLength); + } + if (name === 'LZWDecode' || name === 'LZW') { + xrefStreamStats[StreamType.LZW] = true; + var earlyChange = 1; + if (params) { + if (params.has('EarlyChange')) { + earlyChange = params.get('EarlyChange'); + } + return new PredictorStream( + new LZWStream(stream, maybeLength, earlyChange), + maybeLength, params); + } + return new LZWStream(stream, maybeLength, earlyChange); + } + if (name === 'DCTDecode' || name === 'DCT') { + xrefStreamStats[StreamType.DCT] = true; + return new JpegStream(stream, maybeLength, stream.dict, this.xref); + } + if (name === 'JPXDecode' || name === 'JPX') { + xrefStreamStats[StreamType.JPX] = true; + return new JpxStream(stream, maybeLength, stream.dict); + } + if (name === 'ASCII85Decode' || name === 'A85') { + xrefStreamStats[StreamType.A85] = true; + return new Ascii85Stream(stream, maybeLength); + } + if (name === 'ASCIIHexDecode' || name === 'AHx') { + xrefStreamStats[StreamType.AHX] = true; + return new AsciiHexStream(stream, maybeLength); + } + if (name === 'CCITTFaxDecode' || name === 'CCF') { + xrefStreamStats[StreamType.CCF] = true; + return new CCITTFaxStream(stream, maybeLength, params); + } + if (name === 'RunLengthDecode' || name === 'RL') { + xrefStreamStats[StreamType.RL] = true; + return new RunLengthStream(stream, maybeLength); + } + if (name === 'JBIG2Decode') { + xrefStreamStats[StreamType.JBIG] = true; + return new Jbig2Stream(stream, maybeLength, stream.dict); + } + warn('filter "' + name + '" not supported yet'); + return stream; + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + warn('Invalid stream: \"' + ex + '\"'); + return new NullStream(stream); + } + } + }; + + return Parser; +})(); + +var Lexer = (function LexerClosure() { + function Lexer(stream, knownCommands) { + this.stream = stream; + this.nextChar(); + + // While lexing, we build up many strings one char at a time. Using += for + // this can result in lots of garbage strings. It's better to build an + // array of single-char strings and then join() them together at the end. + // And reusing a single array (i.e. |this.strBuf|) over and over for this + // purpose uses less memory than using a new array for each string. + this.strBuf = []; + + // The PDFs might have "glued" commands with other commands, operands or + // literals, e.g. "q1". The knownCommands is a dictionary of the valid + // commands and their prefixes. The prefixes are built the following way: + // if there a command that is a prefix of the other valid command or + // literal (e.g. 'f' and 'false') the following prefixes must be included, + // 'fa', 'fal', 'fals'. The prefixes are not needed, if the command has no + // other commands or literals as a prefix. The knowCommands is optional. + this.knownCommands = knownCommands; + } + + Lexer.isSpace = function Lexer_isSpace(ch) { + // Space is one of the following characters: SPACE, TAB, CR or LF. + return (ch === 0x20 || ch === 0x09 || ch === 0x0D || ch === 0x0A); + }; + + // A '1' in this array means the character is white space. A '1' or + // '2' means the character ends a name or command. + var specialChars = [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, // 0x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x + 1, 0, 0, 0, 0, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, // 2x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, // 3x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, // 5x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, // 7x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ax + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // bx + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // cx + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // dx + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ex + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // fx + ]; + + function toHexDigit(ch) { + if (ch >= 0x30 && ch <= 0x39) { // '0'-'9' + return ch & 0x0F; + } + if ((ch >= 0x41 && ch <= 0x46) || (ch >= 0x61 && ch <= 0x66)) { + // 'A'-'F', 'a'-'f' + return (ch & 0x0F) + 9; + } + return -1; + } + + Lexer.prototype = { + nextChar: function Lexer_nextChar() { + return (this.currentChar = this.stream.getByte()); + }, + peekChar: function Lexer_peekChar() { + return this.stream.peekByte(); + }, + getNumber: function Lexer_getNumber() { + var ch = this.currentChar; + var eNotation = false; + var divideBy = 0; // different from 0 if it's a floating point value + var sign = 1; + + if (ch === 0x2D) { // '-' + sign = -1; + ch = this.nextChar(); + } else if (ch === 0x2B) { // '+' + ch = this.nextChar(); + } + if (ch === 0x2E) { // '.' + divideBy = 10; + ch = this.nextChar(); + } + if (ch < 0x30 || ch > 0x39) { // '0' - '9' + error('Invalid number: ' + String.fromCharCode(ch)); + return 0; + } + + var baseValue = ch - 0x30; // '0' + var powerValue = 0; + var powerValueSign = 1; + + while ((ch = this.nextChar()) >= 0) { + if (0x30 <= ch && ch <= 0x39) { // '0' - '9' + var currentDigit = ch - 0x30; // '0' + if (eNotation) { // We are after an 'e' or 'E' + powerValue = powerValue * 10 + currentDigit; + } else { + if (divideBy !== 0) { // We are after a point + divideBy *= 10; + } + baseValue = baseValue * 10 + currentDigit; + } + } else if (ch === 0x2E) { // '.' + if (divideBy === 0) { + divideBy = 1; + } else { + // A number can have only one '.' + break; + } + } else if (ch === 0x2D) { // '-' + // ignore minus signs in the middle of numbers to match + // Adobe's behavior + warn('Badly formated number'); + } else if (ch === 0x45 || ch === 0x65) { // 'E', 'e' + // 'E' can be either a scientific notation or the beginning of a new + // operator + ch = this.peekChar(); + if (ch === 0x2B || ch === 0x2D) { // '+', '-' + powerValueSign = (ch === 0x2D) ? -1 : 1; + this.nextChar(); // Consume the sign character + } else if (ch < 0x30 || ch > 0x39) { // '0' - '9' + // The 'E' must be the beginning of a new operator + break; + } + eNotation = true; + } else { + // the last character doesn't belong to us + break; + } + } + + if (divideBy !== 0) { + baseValue /= divideBy; + } + if (eNotation) { + baseValue *= Math.pow(10, powerValueSign * powerValue); + } + return sign * baseValue; + }, + getString: function Lexer_getString() { + var numParen = 1; + var done = false; + var strBuf = this.strBuf; + strBuf.length = 0; + + var ch = this.nextChar(); + while (true) { + var charBuffered = false; + switch (ch | 0) { + case -1: + warn('Unterminated string'); + done = true; + break; + case 0x28: // '(' + ++numParen; + strBuf.push('('); + break; + case 0x29: // ')' + if (--numParen === 0) { + this.nextChar(); // consume strings ')' + done = true; + } else { + strBuf.push(')'); + } + break; + case 0x5C: // '\\' + ch = this.nextChar(); + switch (ch) { + case -1: + warn('Unterminated string'); + done = true; + break; + case 0x6E: // 'n' + strBuf.push('\n'); + break; + case 0x72: // 'r' + strBuf.push('\r'); + break; + case 0x74: // 't' + strBuf.push('\t'); + break; + case 0x62: // 'b' + strBuf.push('\b'); + break; + case 0x66: // 'f' + strBuf.push('\f'); + break; + case 0x5C: // '\' + case 0x28: // '(' + case 0x29: // ')' + strBuf.push(String.fromCharCode(ch)); + break; + case 0x30: case 0x31: case 0x32: case 0x33: // '0'-'3' + case 0x34: case 0x35: case 0x36: case 0x37: // '4'-'7' + var x = ch & 0x0F; + ch = this.nextChar(); + charBuffered = true; + if (ch >= 0x30 && ch <= 0x37) { // '0'-'7' + x = (x << 3) + (ch & 0x0F); + ch = this.nextChar(); + if (ch >= 0x30 && ch <= 0x37) { // '0'-'7' + charBuffered = false; + x = (x << 3) + (ch & 0x0F); + } + } + strBuf.push(String.fromCharCode(x)); + break; + case 0x0D: // CR + if (this.peekChar() === 0x0A) { // LF + this.nextChar(); + } + break; + case 0x0A: // LF + break; + default: + strBuf.push(String.fromCharCode(ch)); + break; + } + break; + default: + strBuf.push(String.fromCharCode(ch)); + break; + } + if (done) { + break; + } + if (!charBuffered) { + ch = this.nextChar(); + } + } + return strBuf.join(''); + }, + getName: function Lexer_getName() { + var ch; + var strBuf = this.strBuf; + strBuf.length = 0; + while ((ch = this.nextChar()) >= 0 && !specialChars[ch]) { + if (ch === 0x23) { // '#' + ch = this.nextChar(); + var x = toHexDigit(ch); + if (x !== -1) { + var x2 = toHexDigit(this.nextChar()); + if (x2 === -1) { + error('Illegal digit in hex char in name: ' + x2); + } + strBuf.push(String.fromCharCode((x << 4) | x2)); + } else { + strBuf.push('#', String.fromCharCode(ch)); + } + } else { + strBuf.push(String.fromCharCode(ch)); + } + } + if (strBuf.length > 128) { + error('Warning: name token is longer than allowed by the spec: ' + + strBuf.length); + } + return Name.get(strBuf.join('')); + }, + getHexString: function Lexer_getHexString() { + var strBuf = this.strBuf; + strBuf.length = 0; + var ch = this.currentChar; + var isFirstHex = true; + var firstDigit; + var secondDigit; + while (true) { + if (ch < 0) { + warn('Unterminated hex string'); + break; + } else if (ch === 0x3E) { // '>' + this.nextChar(); + break; + } else if (specialChars[ch] === 1) { + ch = this.nextChar(); + continue; + } else { + if (isFirstHex) { + firstDigit = toHexDigit(ch); + if (firstDigit === -1) { + warn('Ignoring invalid character "' + ch + '" in hex string'); + ch = this.nextChar(); + continue; + } + } else { + secondDigit = toHexDigit(ch); + if (secondDigit === -1) { + warn('Ignoring invalid character "' + ch + '" in hex string'); + ch = this.nextChar(); + continue; + } + strBuf.push(String.fromCharCode((firstDigit << 4) | secondDigit)); + } + isFirstHex = !isFirstHex; + ch = this.nextChar(); + } + } + return strBuf.join(''); + }, + getObj: function Lexer_getObj() { + // skip whitespace and comments + var comment = false; + var ch = this.currentChar; + while (true) { + if (ch < 0) { + return EOF; + } + if (comment) { + if (ch === 0x0A || ch === 0x0D) { // LF, CR + comment = false; + } + } else if (ch === 0x25) { // '%' + comment = true; + } else if (specialChars[ch] !== 1) { + break; + } + ch = this.nextChar(); + } + + // start reading token + switch (ch | 0) { + case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4' + case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9' + case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.' + return this.getNumber(); + case 0x28: // '(' + return this.getString(); + case 0x2F: // '/' + return this.getName(); + // array punctuation + case 0x5B: // '[' + this.nextChar(); + return Cmd.get('['); + case 0x5D: // ']' + this.nextChar(); + return Cmd.get(']'); + // hex string or dict punctuation + case 0x3C: // '<' + ch = this.nextChar(); + if (ch === 0x3C) { + // dict punctuation + this.nextChar(); + return Cmd.get('<<'); + } + return this.getHexString(); + // dict punctuation + case 0x3E: // '>' + ch = this.nextChar(); + if (ch === 0x3E) { + this.nextChar(); + return Cmd.get('>>'); + } + return Cmd.get('>'); + case 0x7B: // '{' + this.nextChar(); + return Cmd.get('{'); + case 0x7D: // '}' + this.nextChar(); + return Cmd.get('}'); + case 0x29: // ')' + error('Illegal character: ' + ch); + break; + } + + // command + var str = String.fromCharCode(ch); + var knownCommands = this.knownCommands; + var knownCommandFound = knownCommands && knownCommands[str] !== undefined; + while ((ch = this.nextChar()) >= 0 && !specialChars[ch]) { + // stop if known command is found and next character does not make + // the str a command + var possibleCommand = str + String.fromCharCode(ch); + if (knownCommandFound && knownCommands[possibleCommand] === undefined) { + break; + } + if (str.length === 128) { + error('Command token too long: ' + str.length); + } + str = possibleCommand; + knownCommandFound = knownCommands && knownCommands[str] !== undefined; + } + if (str === 'true') { + return true; + } + if (str === 'false') { + return false; + } + if (str === 'null') { + return null; + } + return Cmd.get(str); + }, + skipToNextLine: function Lexer_skipToNextLine() { + var ch = this.currentChar; + while (ch >= 0) { + if (ch === 0x0D) { // CR + ch = this.nextChar(); + if (ch === 0x0A) { // LF + this.nextChar(); + } + break; + } else if (ch === 0x0A) { // LF + this.nextChar(); + break; + } + ch = this.nextChar(); + } + } + }; + + return Lexer; +})(); + +var Linearization = { + create: function LinearizationCreate(stream) { + function getInt(name, allowZeroValue) { + var obj = linDict.get(name); + if (isInt(obj) && (allowZeroValue ? obj >= 0 : obj > 0)) { + return obj; + } + throw new Error('The "' + name + '" parameter in the linearization ' + + 'dictionary is invalid.'); + } + function getHints() { + var hints = linDict.get('H'), hintsLength, item; + if (isArray(hints) && + ((hintsLength = hints.length) === 2 || hintsLength === 4)) { + for (var index = 0; index < hintsLength; index++) { + if (!(isInt(item = hints[index]) && item > 0)) { + throw new Error('Hint (' + index + + ') in the linearization dictionary is invalid.'); + } + } + return hints; + } + throw new Error('Hint array in the linearization dictionary is invalid.'); + } + var parser = new Parser(new Lexer(stream), false, null); + var obj1 = parser.getObj(); + var obj2 = parser.getObj(); + var obj3 = parser.getObj(); + var linDict = parser.getObj(); + var obj, length; + if (!(isInt(obj1) && isInt(obj2) && isCmd(obj3, 'obj') && isDict(linDict) && + isNum(obj = linDict.get('Linearized')) && obj > 0)) { + return null; // No valid linearization dictionary found. + } else if ((length = getInt('L')) !== stream.length) { + throw new Error('The "L" parameter in the linearization dictionary ' + + 'does not equal the stream length.'); + } + return { + length: length, + hints: getHints(), + objectNumberFirst: getInt('O'), + endFirst: getInt('E'), + numPages: getInt('N'), + mainXRefEntriesOffset: getInt('T'), + pageFirst: (linDict.has('P') ? getInt('P', true) : 0) + }; + } +}; + + +var PostScriptParser = (function PostScriptParserClosure() { + function PostScriptParser(lexer) { + this.lexer = lexer; + this.operators = []; + this.token = null; + this.prev = null; + } + PostScriptParser.prototype = { + nextToken: function PostScriptParser_nextToken() { + this.prev = this.token; + this.token = this.lexer.getToken(); + }, + accept: function PostScriptParser_accept(type) { + if (this.token.type === type) { + this.nextToken(); + return true; + } + return false; + }, + expect: function PostScriptParser_expect(type) { + if (this.accept(type)) { + return true; + } + error('Unexpected symbol: found ' + this.token.type + ' expected ' + + type + '.'); + }, + parse: function PostScriptParser_parse() { + this.nextToken(); + this.expect(PostScriptTokenTypes.LBRACE); + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + return this.operators; + }, + parseBlock: function PostScriptParser_parseBlock() { + while (true) { + if (this.accept(PostScriptTokenTypes.NUMBER)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.OPERATOR)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + this.parseCondition(); + } else { + return; + } + } + }, + parseCondition: function PostScriptParser_parseCondition() { + // Add two place holders that will be updated later + var conditionLocation = this.operators.length; + this.operators.push(null, null); + + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + if (this.accept(PostScriptTokenTypes.IF)) { + // The true block is right after the 'if' so it just falls through on + // true else it jumps and skips the true block. + this.operators[conditionLocation] = this.operators.length; + this.operators[conditionLocation + 1] = 'jz'; + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + var jumpLocation = this.operators.length; + this.operators.push(null, null); + var endOfTrue = this.operators.length; + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + this.expect(PostScriptTokenTypes.IFELSE); + // The jump is added at the end of the true block to skip the false + // block. + this.operators[jumpLocation] = this.operators.length; + this.operators[jumpLocation + 1] = 'j'; + + this.operators[conditionLocation] = endOfTrue; + this.operators[conditionLocation + 1] = 'jz'; + } else { + error('PS Function: error parsing conditional.'); + } + } + }; + return PostScriptParser; +})(); + +var PostScriptTokenTypes = { + LBRACE: 0, + RBRACE: 1, + NUMBER: 2, + OPERATOR: 3, + IF: 4, + IFELSE: 5 +}; + +var PostScriptToken = (function PostScriptTokenClosure() { + function PostScriptToken(type, value) { + this.type = type; + this.value = value; + } + + var opCache = {}; + + PostScriptToken.getOperator = function PostScriptToken_getOperator(op) { + var opValue = opCache[op]; + if (opValue) { + return opValue; + } + return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op); + }; + + PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE, + '{'); + PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE, + '}'); + PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF'); + PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE, + 'IFELSE'); + return PostScriptToken; +})(); + +var PostScriptLexer = (function PostScriptLexerClosure() { + function PostScriptLexer(stream) { + this.stream = stream; + this.nextChar(); + + this.strBuf = []; + } + PostScriptLexer.prototype = { + nextChar: function PostScriptLexer_nextChar() { + return (this.currentChar = this.stream.getByte()); + }, + getToken: function PostScriptLexer_getToken() { + var comment = false; + var ch = this.currentChar; + + // skip comments + while (true) { + if (ch < 0) { + return EOF; + } + + if (comment) { + if (ch === 0x0A || ch === 0x0D) { + comment = false; + } + } else if (ch === 0x25) { // '%' + comment = true; + } else if (!Lexer.isSpace(ch)) { + break; + } + ch = this.nextChar(); + } + switch (ch | 0) { + case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: // '0'-'4' + case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: // '5'-'9' + case 0x2B: case 0x2D: case 0x2E: // '+', '-', '.' + return new PostScriptToken(PostScriptTokenTypes.NUMBER, + this.getNumber()); + case 0x7B: // '{' + this.nextChar(); + return PostScriptToken.LBRACE; + case 0x7D: // '}' + this.nextChar(); + return PostScriptToken.RBRACE; + } + // operator + var strBuf = this.strBuf; + strBuf.length = 0; + strBuf[0] = String.fromCharCode(ch); + + while ((ch = this.nextChar()) >= 0 && // and 'A'-'Z', 'a'-'z' + ((ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A))) { + strBuf.push(String.fromCharCode(ch)); + } + var str = strBuf.join(''); + switch (str.toLowerCase()) { + case 'if': + return PostScriptToken.IF; + case 'ifelse': + return PostScriptToken.IFELSE; + default: + return PostScriptToken.getOperator(str); + } + }, + getNumber: function PostScriptLexer_getNumber() { + var ch = this.currentChar; + var strBuf = this.strBuf; + strBuf.length = 0; + strBuf[0] = String.fromCharCode(ch); + + while ((ch = this.nextChar()) >= 0) { + if ((ch >= 0x30 && ch <= 0x39) || // '0'-'9' + ch === 0x2D || ch === 0x2E) { // '-', '.' + strBuf.push(String.fromCharCode(ch)); + } else { + break; + } + } + var value = parseFloat(strBuf.join('')); + if (isNaN(value)) { + error('Invalid floating point number: ' + value); + } + return value; + } + }; + return PostScriptLexer; +})(); + + +var Stream = (function StreamClosure() { + function Stream(arrayBuffer, start, length, dict) { + this.bytes = (arrayBuffer instanceof Uint8Array ? + arrayBuffer : new Uint8Array(arrayBuffer)); + this.start = start || 0; + this.pos = this.start; + this.end = (start + length) || this.bytes.length; + this.dict = dict; + } + + // required methods for a stream. if a particular stream does not + // implement these, an error should be thrown + Stream.prototype = { + get length() { + return this.end - this.start; + }, + get isEmpty() { + return this.length === 0; + }, + getByte: function Stream_getByte() { + if (this.pos >= this.end) { + return -1; + } + return this.bytes[this.pos++]; + }, + getUint16: function Stream_getUint16() { + var b0 = this.getByte(); + var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } + return (b0 << 8) + b1; + }, + getInt32: function Stream_getInt32() { + var b0 = this.getByte(); + var b1 = this.getByte(); + var b2 = this.getByte(); + var b3 = this.getByte(); + return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3; + }, + // returns subarray of original buffer + // should only be read + getBytes: function Stream_getBytes(length) { + var bytes = this.bytes; + var pos = this.pos; + var strEnd = this.end; + + if (!length) { + return bytes.subarray(pos, strEnd); + } + var end = pos + length; + if (end > strEnd) { + end = strEnd; + } + this.pos = end; + return bytes.subarray(pos, end); + }, + peekByte: function Stream_peekByte() { + var peekedByte = this.getByte(); + this.pos--; + return peekedByte; + }, + peekBytes: function Stream_peekBytes(length) { + var bytes = this.getBytes(length); + this.pos -= bytes.length; + return bytes; + }, + skip: function Stream_skip(n) { + if (!n) { + n = 1; + } + this.pos += n; + }, + reset: function Stream_reset() { + this.pos = this.start; + }, + moveStart: function Stream_moveStart() { + this.start = this.pos; + }, + makeSubStream: function Stream_makeSubStream(start, length, dict) { + return new Stream(this.bytes.buffer, start, length, dict); + }, + isStream: true + }; + + return Stream; +})(); + +var StringStream = (function StringStreamClosure() { + function StringStream(str) { + var length = str.length; + var bytes = new Uint8Array(length); + for (var n = 0; n < length; ++n) { + bytes[n] = str.charCodeAt(n); + } + Stream.call(this, bytes); + } + + StringStream.prototype = Stream.prototype; + + return StringStream; +})(); + +// super class for the decoding streams +var DecodeStream = (function DecodeStreamClosure() { + // Lots of DecodeStreams are created whose buffers are never used. For these + // we share a single empty buffer. This is (a) space-efficient and (b) avoids + // having special cases that would be required if we used |null| for an empty + // buffer. + var emptyBuffer = new Uint8Array(0); + + function DecodeStream(maybeMinBufferLength) { + this.pos = 0; + this.bufferLength = 0; + this.eof = false; + this.buffer = emptyBuffer; + this.minBufferLength = 512; + if (maybeMinBufferLength) { + // Compute the first power of two that is as big as maybeMinBufferLength. + while (this.minBufferLength < maybeMinBufferLength) { + this.minBufferLength *= 2; + } + } + } + + DecodeStream.prototype = { + get isEmpty() { + while (!this.eof && this.bufferLength === 0) { + this.readBlock(); + } + return this.bufferLength === 0; + }, + ensureBuffer: function DecodeStream_ensureBuffer(requested) { + var buffer = this.buffer; + if (requested <= buffer.byteLength) { + return buffer; + } + var size = this.minBufferLength; + while (size < requested) { + size *= 2; + } + var buffer2 = new Uint8Array(size); + buffer2.set(buffer); + return (this.buffer = buffer2); + }, + getByte: function DecodeStream_getByte() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) { + return -1; + } + this.readBlock(); + } + return this.buffer[this.pos++]; + }, + getUint16: function DecodeStream_getUint16() { + var b0 = this.getByte(); + var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } + return (b0 << 8) + b1; + }, + getInt32: function DecodeStream_getInt32() { + var b0 = this.getByte(); + var b1 = this.getByte(); + var b2 = this.getByte(); + var b3 = this.getByte(); + return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3; + }, + getBytes: function DecodeStream_getBytes(length) { + var end, pos = this.pos; + + if (length) { + this.ensureBuffer(pos + length); + end = pos + length; + + while (!this.eof && this.bufferLength < end) { + this.readBlock(); + } + var bufEnd = this.bufferLength; + if (end > bufEnd) { + end = bufEnd; + } + } else { + while (!this.eof) { + this.readBlock(); + } + end = this.bufferLength; + } + + this.pos = end; + return this.buffer.subarray(pos, end); + }, + peekByte: function DecodeStream_peekByte() { + var peekedByte = this.getByte(); + this.pos--; + return peekedByte; + }, + peekBytes: function DecodeStream_peekBytes(length) { + var bytes = this.getBytes(length); + this.pos -= bytes.length; + return bytes; + }, + makeSubStream: function DecodeStream_makeSubStream(start, length, dict) { + var end = start + length; + while (this.bufferLength <= end && !this.eof) { + this.readBlock(); + } + return new Stream(this.buffer, start, length, dict); + }, + skip: function DecodeStream_skip(n) { + if (!n) { + n = 1; + } + this.pos += n; + }, + reset: function DecodeStream_reset() { + this.pos = 0; + }, + getBaseStreams: function DecodeStream_getBaseStreams() { + if (this.str && this.str.getBaseStreams) { + return this.str.getBaseStreams(); + } + return []; + } + }; + + return DecodeStream; +})(); + +var StreamsSequenceStream = (function StreamsSequenceStreamClosure() { + function StreamsSequenceStream(streams) { + this.streams = streams; + DecodeStream.call(this, /* maybeLength = */ null); + } + + StreamsSequenceStream.prototype = Object.create(DecodeStream.prototype); + + StreamsSequenceStream.prototype.readBlock = + function streamSequenceStreamReadBlock() { + + var streams = this.streams; + if (streams.length === 0) { + this.eof = true; + return; + } + var stream = streams.shift(); + var chunk = stream.getBytes(); + var bufferLength = this.bufferLength; + var newLength = bufferLength + chunk.length; + var buffer = this.ensureBuffer(newLength); + buffer.set(chunk, bufferLength); + this.bufferLength = newLength; + }; + + StreamsSequenceStream.prototype.getBaseStreams = + function StreamsSequenceStream_getBaseStreams() { + + var baseStreams = []; + for (var i = 0, ii = this.streams.length; i < ii; i++) { + var stream = this.streams[i]; + if (stream.getBaseStreams) { + Util.appendToArray(baseStreams, stream.getBaseStreams()); + } + } + return baseStreams; + }; + + return StreamsSequenceStream; +})(); + +var FlateStream = (function FlateStreamClosure() { + var codeLenCodeMap = new Int32Array([ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 + ]); + + var lengthDecode = new Int32Array([ + 0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a, + 0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f, + 0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, 0x40053, 0x40063, 0x40073, + 0x50083, 0x500a3, 0x500c3, 0x500e3, 0x00102, 0x00102, 0x00102 + ]); + + var distDecode = new Int32Array([ + 0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d, + 0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1, + 0x70101, 0x70181, 0x80201, 0x80301, 0x90401, 0x90601, 0xa0801, 0xa0c01, + 0xb1001, 0xb1801, 0xc2001, 0xc3001, 0xd4001, 0xd6001 + ]); + + var fixedLitCodeTab = [new Int32Array([ + 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0, + 0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0, + 0x70104, 0x80058, 0x80018, 0x90090, 0x70114, 0x80078, 0x80038, 0x900d0, + 0x7010c, 0x80068, 0x80028, 0x900b0, 0x80008, 0x80088, 0x80048, 0x900f0, + 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c8, + 0x7010a, 0x80064, 0x80024, 0x900a8, 0x80004, 0x80084, 0x80044, 0x900e8, + 0x70106, 0x8005c, 0x8001c, 0x90098, 0x70116, 0x8007c, 0x8003c, 0x900d8, + 0x7010e, 0x8006c, 0x8002c, 0x900b8, 0x8000c, 0x8008c, 0x8004c, 0x900f8, + 0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c4, + 0x70109, 0x80062, 0x80022, 0x900a4, 0x80002, 0x80082, 0x80042, 0x900e4, + 0x70105, 0x8005a, 0x8001a, 0x90094, 0x70115, 0x8007a, 0x8003a, 0x900d4, + 0x7010d, 0x8006a, 0x8002a, 0x900b4, 0x8000a, 0x8008a, 0x8004a, 0x900f4, + 0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cc, + 0x7010b, 0x80066, 0x80026, 0x900ac, 0x80006, 0x80086, 0x80046, 0x900ec, + 0x70107, 0x8005e, 0x8001e, 0x9009c, 0x70117, 0x8007e, 0x8003e, 0x900dc, + 0x7010f, 0x8006e, 0x8002e, 0x900bc, 0x8000e, 0x8008e, 0x8004e, 0x900fc, + 0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c2, + 0x70108, 0x80061, 0x80021, 0x900a2, 0x80001, 0x80081, 0x80041, 0x900e2, + 0x70104, 0x80059, 0x80019, 0x90092, 0x70114, 0x80079, 0x80039, 0x900d2, + 0x7010c, 0x80069, 0x80029, 0x900b2, 0x80009, 0x80089, 0x80049, 0x900f2, + 0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900ca, + 0x7010a, 0x80065, 0x80025, 0x900aa, 0x80005, 0x80085, 0x80045, 0x900ea, + 0x70106, 0x8005d, 0x8001d, 0x9009a, 0x70116, 0x8007d, 0x8003d, 0x900da, + 0x7010e, 0x8006d, 0x8002d, 0x900ba, 0x8000d, 0x8008d, 0x8004d, 0x900fa, + 0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c6, + 0x70109, 0x80063, 0x80023, 0x900a6, 0x80003, 0x80083, 0x80043, 0x900e6, + 0x70105, 0x8005b, 0x8001b, 0x90096, 0x70115, 0x8007b, 0x8003b, 0x900d6, + 0x7010d, 0x8006b, 0x8002b, 0x900b6, 0x8000b, 0x8008b, 0x8004b, 0x900f6, + 0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900ce, + 0x7010b, 0x80067, 0x80027, 0x900ae, 0x80007, 0x80087, 0x80047, 0x900ee, + 0x70107, 0x8005f, 0x8001f, 0x9009e, 0x70117, 0x8007f, 0x8003f, 0x900de, + 0x7010f, 0x8006f, 0x8002f, 0x900be, 0x8000f, 0x8008f, 0x8004f, 0x900fe, + 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c1, + 0x70108, 0x80060, 0x80020, 0x900a1, 0x80000, 0x80080, 0x80040, 0x900e1, + 0x70104, 0x80058, 0x80018, 0x90091, 0x70114, 0x80078, 0x80038, 0x900d1, + 0x7010c, 0x80068, 0x80028, 0x900b1, 0x80008, 0x80088, 0x80048, 0x900f1, + 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c9, + 0x7010a, 0x80064, 0x80024, 0x900a9, 0x80004, 0x80084, 0x80044, 0x900e9, + 0x70106, 0x8005c, 0x8001c, 0x90099, 0x70116, 0x8007c, 0x8003c, 0x900d9, + 0x7010e, 0x8006c, 0x8002c, 0x900b9, 0x8000c, 0x8008c, 0x8004c, 0x900f9, + 0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c5, + 0x70109, 0x80062, 0x80022, 0x900a5, 0x80002, 0x80082, 0x80042, 0x900e5, + 0x70105, 0x8005a, 0x8001a, 0x90095, 0x70115, 0x8007a, 0x8003a, 0x900d5, + 0x7010d, 0x8006a, 0x8002a, 0x900b5, 0x8000a, 0x8008a, 0x8004a, 0x900f5, + 0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cd, + 0x7010b, 0x80066, 0x80026, 0x900ad, 0x80006, 0x80086, 0x80046, 0x900ed, + 0x70107, 0x8005e, 0x8001e, 0x9009d, 0x70117, 0x8007e, 0x8003e, 0x900dd, + 0x7010f, 0x8006e, 0x8002e, 0x900bd, 0x8000e, 0x8008e, 0x8004e, 0x900fd, + 0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c3, + 0x70108, 0x80061, 0x80021, 0x900a3, 0x80001, 0x80081, 0x80041, 0x900e3, + 0x70104, 0x80059, 0x80019, 0x90093, 0x70114, 0x80079, 0x80039, 0x900d3, + 0x7010c, 0x80069, 0x80029, 0x900b3, 0x80009, 0x80089, 0x80049, 0x900f3, + 0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900cb, + 0x7010a, 0x80065, 0x80025, 0x900ab, 0x80005, 0x80085, 0x80045, 0x900eb, + 0x70106, 0x8005d, 0x8001d, 0x9009b, 0x70116, 0x8007d, 0x8003d, 0x900db, + 0x7010e, 0x8006d, 0x8002d, 0x900bb, 0x8000d, 0x8008d, 0x8004d, 0x900fb, + 0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c7, + 0x70109, 0x80063, 0x80023, 0x900a7, 0x80003, 0x80083, 0x80043, 0x900e7, + 0x70105, 0x8005b, 0x8001b, 0x90097, 0x70115, 0x8007b, 0x8003b, 0x900d7, + 0x7010d, 0x8006b, 0x8002b, 0x900b7, 0x8000b, 0x8008b, 0x8004b, 0x900f7, + 0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900cf, + 0x7010b, 0x80067, 0x80027, 0x900af, 0x80007, 0x80087, 0x80047, 0x900ef, + 0x70107, 0x8005f, 0x8001f, 0x9009f, 0x70117, 0x8007f, 0x8003f, 0x900df, + 0x7010f, 0x8006f, 0x8002f, 0x900bf, 0x8000f, 0x8008f, 0x8004f, 0x900ff + ]), 9]; + + var fixedDistCodeTab = [new Int32Array([ + 0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c, + 0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000, + 0x50001, 0x50011, 0x50009, 0x50019, 0x50005, 0x50015, 0x5000d, 0x5001d, + 0x50003, 0x50013, 0x5000b, 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000 + ]), 5]; + + function FlateStream(str, maybeLength) { + this.str = str; + this.dict = str.dict; + + var cmf = str.getByte(); + var flg = str.getByte(); + if (cmf === -1 || flg === -1) { + error('Invalid header in flate stream: ' + cmf + ', ' + flg); + } + if ((cmf & 0x0f) !== 0x08) { + error('Unknown compression method in flate stream: ' + cmf + ', ' + flg); + } + if ((((cmf << 8) + flg) % 31) !== 0) { + error('Bad FCHECK in flate stream: ' + cmf + ', ' + flg); + } + if (flg & 0x20) { + error('FDICT bit set in flate stream: ' + cmf + ', ' + flg); + } + + this.codeSize = 0; + this.codeBuf = 0; + + DecodeStream.call(this, maybeLength); + } + + FlateStream.prototype = Object.create(DecodeStream.prototype); + + FlateStream.prototype.getBits = function FlateStream_getBits(bits) { + var str = this.str; + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + + var b; + while (codeSize < bits) { + if ((b = str.getByte()) === -1) { + error('Bad encoding in flate stream'); + } + codeBuf |= b << codeSize; + codeSize += 8; + } + b = codeBuf & ((1 << bits) - 1); + this.codeBuf = codeBuf >> bits; + this.codeSize = codeSize -= bits; + + return b; + }; + + FlateStream.prototype.getCode = function FlateStream_getCode(table) { + var str = this.str; + var codes = table[0]; + var maxLen = table[1]; + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + + var b; + while (codeSize < maxLen) { + if ((b = str.getByte()) === -1) { + // premature end of stream. code might however still be valid. + // codeSize < codeLen check below guards against incomplete codeVal. + break; + } + codeBuf |= (b << codeSize); + codeSize += 8; + } + var code = codes[codeBuf & ((1 << maxLen) - 1)]; + var codeLen = code >> 16; + var codeVal = code & 0xffff; + if (codeLen < 1 || codeSize < codeLen) { + error('Bad encoding in flate stream'); + } + this.codeBuf = (codeBuf >> codeLen); + this.codeSize = (codeSize - codeLen); + return codeVal; + }; + + FlateStream.prototype.generateHuffmanTable = + function flateStreamGenerateHuffmanTable(lengths) { + var n = lengths.length; + + // find max code length + var maxLen = 0; + var i; + for (i = 0; i < n; ++i) { + if (lengths[i] > maxLen) { + maxLen = lengths[i]; + } + } + + // build the table + var size = 1 << maxLen; + var codes = new Int32Array(size); + for (var len = 1, code = 0, skip = 2; + len <= maxLen; + ++len, code <<= 1, skip <<= 1) { + for (var val = 0; val < n; ++val) { + if (lengths[val] === len) { + // bit-reverse the code + var code2 = 0; + var t = code; + for (i = 0; i < len; ++i) { + code2 = (code2 << 1) | (t & 1); + t >>= 1; + } + + // fill the table entries + for (i = code2; i < size; i += skip) { + codes[i] = (len << 16) | val; + } + ++code; + } + } + } + + return [codes, maxLen]; + }; + + FlateStream.prototype.readBlock = function FlateStream_readBlock() { + var buffer, len; + var str = this.str; + // read block header + var hdr = this.getBits(3); + if (hdr & 1) { + this.eof = true; + } + hdr >>= 1; + + if (hdr === 0) { // uncompressed block + var b; + + if ((b = str.getByte()) === -1) { + error('Bad block header in flate stream'); + } + var blockLen = b; + if ((b = str.getByte()) === -1) { + error('Bad block header in flate stream'); + } + blockLen |= (b << 8); + if ((b = str.getByte()) === -1) { + error('Bad block header in flate stream'); + } + var check = b; + if ((b = str.getByte()) === -1) { + error('Bad block header in flate stream'); + } + check |= (b << 8); + if (check !== (~blockLen & 0xffff) && + (blockLen !== 0 || check !== 0)) { + // Ignoring error for bad "empty" block (see issue 1277) + error('Bad uncompressed block length in flate stream'); + } + + this.codeBuf = 0; + this.codeSize = 0; + + var bufferLength = this.bufferLength; + buffer = this.ensureBuffer(bufferLength + blockLen); + var end = bufferLength + blockLen; + this.bufferLength = end; + if (blockLen === 0) { + if (str.peekByte() === -1) { + this.eof = true; + } + } else { + for (var n = bufferLength; n < end; ++n) { + if ((b = str.getByte()) === -1) { + this.eof = true; + break; + } + buffer[n] = b; + } + } + return; + } + + var litCodeTable; + var distCodeTable; + if (hdr === 1) { // compressed block, fixed codes + litCodeTable = fixedLitCodeTab; + distCodeTable = fixedDistCodeTab; + } else if (hdr === 2) { // compressed block, dynamic codes + var numLitCodes = this.getBits(5) + 257; + var numDistCodes = this.getBits(5) + 1; + var numCodeLenCodes = this.getBits(4) + 4; + + // build the code lengths code table + var codeLenCodeLengths = new Uint8Array(codeLenCodeMap.length); + + var i; + for (i = 0; i < numCodeLenCodes; ++i) { + codeLenCodeLengths[codeLenCodeMap[i]] = this.getBits(3); + } + var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths); + + // build the literal and distance code tables + len = 0; + i = 0; + var codes = numLitCodes + numDistCodes; + var codeLengths = new Uint8Array(codes); + var bitsLength, bitsOffset, what; + while (i < codes) { + var code = this.getCode(codeLenCodeTab); + if (code === 16) { + bitsLength = 2; bitsOffset = 3; what = len; + } else if (code === 17) { + bitsLength = 3; bitsOffset = 3; what = (len = 0); + } else if (code === 18) { + bitsLength = 7; bitsOffset = 11; what = (len = 0); + } else { + codeLengths[i++] = len = code; + continue; + } + + var repeatLength = this.getBits(bitsLength) + bitsOffset; + while (repeatLength-- > 0) { + codeLengths[i++] = what; + } + } + + litCodeTable = + this.generateHuffmanTable(codeLengths.subarray(0, numLitCodes)); + distCodeTable = + this.generateHuffmanTable(codeLengths.subarray(numLitCodes, codes)); + } else { + error('Unknown block type in flate stream'); + } + + buffer = this.buffer; + var limit = buffer ? buffer.length : 0; + var pos = this.bufferLength; + while (true) { + var code1 = this.getCode(litCodeTable); + if (code1 < 256) { + if (pos + 1 >= limit) { + buffer = this.ensureBuffer(pos + 1); + limit = buffer.length; + } + buffer[pos++] = code1; + continue; + } + if (code1 === 256) { + this.bufferLength = pos; + return; + } + code1 -= 257; + code1 = lengthDecode[code1]; + var code2 = code1 >> 16; + if (code2 > 0) { + code2 = this.getBits(code2); + } + len = (code1 & 0xffff) + code2; + code1 = this.getCode(distCodeTable); + code1 = distDecode[code1]; + code2 = code1 >> 16; + if (code2 > 0) { + code2 = this.getBits(code2); + } + var dist = (code1 & 0xffff) + code2; + if (pos + len >= limit) { + buffer = this.ensureBuffer(pos + len); + limit = buffer.length; + } + for (var k = 0; k < len; ++k, ++pos) { + buffer[pos] = buffer[pos - dist]; + } + } + }; + + return FlateStream; +})(); + +var PredictorStream = (function PredictorStreamClosure() { + function PredictorStream(str, maybeLength, params) { + var predictor = this.predictor = params.get('Predictor') || 1; + + if (predictor <= 1) { + return str; // no prediction + } + if (predictor !== 2 && (predictor < 10 || predictor > 15)) { + error('Unsupported predictor: ' + predictor); + } + + if (predictor === 2) { + this.readBlock = this.readBlockTiff; + } else { + this.readBlock = this.readBlockPng; + } + + this.str = str; + this.dict = str.dict; + + var colors = this.colors = params.get('Colors') || 1; + var bits = this.bits = params.get('BitsPerComponent') || 8; + var columns = this.columns = params.get('Columns') || 1; + + this.pixBytes = (colors * bits + 7) >> 3; + this.rowBytes = (columns * colors * bits + 7) >> 3; + + DecodeStream.call(this, maybeLength); + return this; + } + + PredictorStream.prototype = Object.create(DecodeStream.prototype); + + PredictorStream.prototype.readBlockTiff = + function predictorStreamReadBlockTiff() { + var rowBytes = this.rowBytes; + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + rowBytes); + + var bits = this.bits; + var colors = this.colors; + + var rawBytes = this.str.getBytes(rowBytes); + this.eof = !rawBytes.length; + if (this.eof) { + return; + } + + var inbuf = 0, outbuf = 0; + var inbits = 0, outbits = 0; + var pos = bufferLength; + var i; + + if (bits === 1) { + for (i = 0; i < rowBytes; ++i) { + var c = rawBytes[i]; + inbuf = (inbuf << 8) | c; + // bitwise addition is exclusive or + // first shift inbuf and then add + buffer[pos++] = (c ^ (inbuf >> colors)) & 0xFF; + // truncate inbuf (assumes colors < 16) + inbuf &= 0xFFFF; + } + } else if (bits === 8) { + for (i = 0; i < colors; ++i) { + buffer[pos++] = rawBytes[i]; + } + for (; i < rowBytes; ++i) { + buffer[pos] = buffer[pos - colors] + rawBytes[i]; + pos++; + } + } else { + var compArray = new Uint8Array(colors + 1); + var bitMask = (1 << bits) - 1; + var j = 0, k = bufferLength; + var columns = this.columns; + for (i = 0; i < columns; ++i) { + for (var kk = 0; kk < colors; ++kk) { + if (inbits < bits) { + inbuf = (inbuf << 8) | (rawBytes[j++] & 0xFF); + inbits += 8; + } + compArray[kk] = (compArray[kk] + + (inbuf >> (inbits - bits))) & bitMask; + inbits -= bits; + outbuf = (outbuf << bits) | compArray[kk]; + outbits += bits; + if (outbits >= 8) { + buffer[k++] = (outbuf >> (outbits - 8)) & 0xFF; + outbits -= 8; + } + } + } + if (outbits > 0) { + buffer[k++] = (outbuf << (8 - outbits)) + + (inbuf & ((1 << (8 - outbits)) - 1)); + } + } + this.bufferLength += rowBytes; + }; + + PredictorStream.prototype.readBlockPng = + function predictorStreamReadBlockPng() { + + var rowBytes = this.rowBytes; + var pixBytes = this.pixBytes; + + var predictor = this.str.getByte(); + var rawBytes = this.str.getBytes(rowBytes); + this.eof = !rawBytes.length; + if (this.eof) { + return; + } + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + rowBytes); + + var prevRow = buffer.subarray(bufferLength - rowBytes, bufferLength); + if (prevRow.length === 0) { + prevRow = new Uint8Array(rowBytes); + } + + var i, j = bufferLength, up, c; + switch (predictor) { + case 0: + for (i = 0; i < rowBytes; ++i) { + buffer[j++] = rawBytes[i]; + } + break; + case 1: + for (i = 0; i < pixBytes; ++i) { + buffer[j++] = rawBytes[i]; + } + for (; i < rowBytes; ++i) { + buffer[j] = (buffer[j - pixBytes] + rawBytes[i]) & 0xFF; + j++; + } + break; + case 2: + for (i = 0; i < rowBytes; ++i) { + buffer[j++] = (prevRow[i] + rawBytes[i]) & 0xFF; + } + break; + case 3: + for (i = 0; i < pixBytes; ++i) { + buffer[j++] = (prevRow[i] >> 1) + rawBytes[i]; + } + for (; i < rowBytes; ++i) { + buffer[j] = (((prevRow[i] + buffer[j - pixBytes]) >> 1) + + rawBytes[i]) & 0xFF; + j++; + } + break; + case 4: + // we need to save the up left pixels values. the simplest way + // is to create a new buffer + for (i = 0; i < pixBytes; ++i) { + up = prevRow[i]; + c = rawBytes[i]; + buffer[j++] = up + c; + } + for (; i < rowBytes; ++i) { + up = prevRow[i]; + var upLeft = prevRow[i - pixBytes]; + var left = buffer[j - pixBytes]; + var p = left + up - upLeft; + + var pa = p - left; + if (pa < 0) { + pa = -pa; + } + var pb = p - up; + if (pb < 0) { + pb = -pb; + } + var pc = p - upLeft; + if (pc < 0) { + pc = -pc; + } + + c = rawBytes[i]; + if (pa <= pb && pa <= pc) { + buffer[j++] = left + c; + } else if (pb <= pc) { + buffer[j++] = up + c; + } else { + buffer[j++] = upLeft + c; + } + } + break; + default: + error('Unsupported predictor: ' + predictor); + } + this.bufferLength += rowBytes; + }; + + return PredictorStream; +})(); + +/** + * Depending on the type of JPEG a JpegStream is handled in different ways. For + * JPEG's that are supported natively such as DeviceGray and DeviceRGB the image + * data is stored and then loaded by the browser. For unsupported JPEG's we use + * a library to decode these images and the stream behaves like all the other + * DecodeStreams. + */ +var JpegStream = (function JpegStreamClosure() { + function JpegStream(stream, maybeLength, dict, xref) { + // Some images may contain 'junk' before the SOI (start-of-image) marker. + // Note: this seems to mainly affect inline images. + var ch; + while ((ch = stream.getByte()) !== -1) { + if (ch === 0xFF) { // Find the first byte of the SOI marker (0xFFD8). + stream.skip(-1); // Reset the stream position to the SOI. + break; + } + } + this.stream = stream; + this.maybeLength = maybeLength; + this.dict = dict; + + DecodeStream.call(this, maybeLength); + } + + JpegStream.prototype = Object.create(DecodeStream.prototype); + + Object.defineProperty(JpegStream.prototype, 'bytes', { + get: function JpegStream_bytes() { + // If this.maybeLength is null, we'll get the entire stream. + return shadow(this, 'bytes', this.stream.getBytes(this.maybeLength)); + }, + configurable: true + }); + + JpegStream.prototype.ensureBuffer = function JpegStream_ensureBuffer(req) { + if (this.bufferLength) { + return; + } + try { + var jpegImage = new JpegImage(); + + // checking if values needs to be transformed before conversion + if (this.forceRGB && this.dict && isArray(this.dict.get('Decode'))) { + var decodeArr = this.dict.get('Decode'); + var bitsPerComponent = this.dict.get('BitsPerComponent') || 8; + var decodeArrLength = decodeArr.length; + var transform = new Int32Array(decodeArrLength); + var transformNeeded = false; + var maxValue = (1 << bitsPerComponent) - 1; + for (var i = 0; i < decodeArrLength; i += 2) { + transform[i] = ((decodeArr[i + 1] - decodeArr[i]) * 256) | 0; + transform[i + 1] = (decodeArr[i] * maxValue) | 0; + if (transform[i] !== 256 || transform[i + 1] !== 0) { + transformNeeded = true; + } + } + if (transformNeeded) { + jpegImage.decodeTransform = transform; + } + } + + jpegImage.parse(this.bytes); + var data = jpegImage.getData(this.drawWidth, this.drawHeight, + this.forceRGB); + this.buffer = data; + this.bufferLength = data.length; + this.eof = true; + } catch (e) { + error('JPEG error: ' + e); + } + }; + + JpegStream.prototype.getBytes = function JpegStream_getBytes(length) { + this.ensureBuffer(); + return this.buffer; + }; + + JpegStream.prototype.getIR = function JpegStream_getIR() { + return PDFJS.createObjectURL(this.bytes, 'image/jpeg'); + }; + /** + * Checks if the image can be decoded and displayed by the browser without any + * further processing such as color space conversions. + */ + JpegStream.prototype.isNativelySupported = + function JpegStream_isNativelySupported(xref, res) { + var cs = ColorSpace.parse(this.dict.get('ColorSpace', 'CS'), xref, res); + return cs.name === 'DeviceGray' || cs.name === 'DeviceRGB'; + }; + /** + * Checks if the image can be decoded by the browser. + */ + JpegStream.prototype.isNativelyDecodable = + function JpegStream_isNativelyDecodable(xref, res) { + var cs = ColorSpace.parse(this.dict.get('ColorSpace', 'CS'), xref, res); + var numComps = cs.numComps; + return numComps === 1 || numComps === 3; + }; + + return JpegStream; +})(); + +/** + * For JPEG 2000's we use a library to decode these images and + * the stream behaves like all the other DecodeStreams. + */ +var JpxStream = (function JpxStreamClosure() { + function JpxStream(stream, maybeLength, dict) { + this.stream = stream; + this.maybeLength = maybeLength; + this.dict = dict; + + DecodeStream.call(this, maybeLength); + } + + JpxStream.prototype = Object.create(DecodeStream.prototype); + + Object.defineProperty(JpxStream.prototype, 'bytes', { + get: function JpxStream_bytes() { + // If this.maybeLength is null, we'll get the entire stream. + return shadow(this, 'bytes', this.stream.getBytes(this.maybeLength)); + }, + configurable: true + }); + + JpxStream.prototype.ensureBuffer = function JpxStream_ensureBuffer(req) { + if (this.bufferLength) { + return; + } + + var jpxImage = new JpxImage(); + jpxImage.parse(this.bytes); + + var width = jpxImage.width; + var height = jpxImage.height; + var componentsCount = jpxImage.componentsCount; + var tileCount = jpxImage.tiles.length; + if (tileCount === 1) { + this.buffer = jpxImage.tiles[0].items; + } else { + var data = new Uint8Array(width * height * componentsCount); + + for (var k = 0; k < tileCount; k++) { + var tileComponents = jpxImage.tiles[k]; + var tileWidth = tileComponents.width; + var tileHeight = tileComponents.height; + var tileLeft = tileComponents.left; + var tileTop = tileComponents.top; + + var src = tileComponents.items; + var srcPosition = 0; + var dataPosition = (width * tileTop + tileLeft) * componentsCount; + var imgRowSize = width * componentsCount; + var tileRowSize = tileWidth * componentsCount; + + for (var j = 0; j < tileHeight; j++) { + var rowBytes = src.subarray(srcPosition, srcPosition + tileRowSize); + data.set(rowBytes, dataPosition); + srcPosition += tileRowSize; + dataPosition += imgRowSize; + } + } + this.buffer = data; + } + this.bufferLength = this.buffer.length; + this.eof = true; + }; + + return JpxStream; +})(); + +/** + * For JBIG2's we use a library to decode these images and + * the stream behaves like all the other DecodeStreams. + */ +var Jbig2Stream = (function Jbig2StreamClosure() { + function Jbig2Stream(stream, maybeLength, dict) { + this.stream = stream; + this.maybeLength = maybeLength; + this.dict = dict; + + DecodeStream.call(this, maybeLength); + } + + Jbig2Stream.prototype = Object.create(DecodeStream.prototype); + + Object.defineProperty(Jbig2Stream.prototype, 'bytes', { + get: function Jbig2Stream_bytes() { + // If this.maybeLength is null, we'll get the entire stream. + return shadow(this, 'bytes', this.stream.getBytes(this.maybeLength)); + }, + configurable: true + }); + + Jbig2Stream.prototype.ensureBuffer = function Jbig2Stream_ensureBuffer(req) { + if (this.bufferLength) { + return; + } + + var jbig2Image = new Jbig2Image(); + + var chunks = [], xref = this.dict.xref; + var decodeParams = xref.fetchIfRef(this.dict.get('DecodeParms')); + + // According to the PDF specification, DecodeParms can be either + // a dictionary, or an array whose elements are dictionaries. + if (isArray(decodeParams)) { + if (decodeParams.length > 1) { + warn('JBIG2 - \'DecodeParms\' array with multiple elements ' + + 'not supported.'); + } + decodeParams = xref.fetchIfRef(decodeParams[0]); + } + if (decodeParams && decodeParams.has('JBIG2Globals')) { + var globalsStream = decodeParams.get('JBIG2Globals'); + var globals = globalsStream.getBytes(); + chunks.push({data: globals, start: 0, end: globals.length}); + } + chunks.push({data: this.bytes, start: 0, end: this.bytes.length}); + var data = jbig2Image.parseChunks(chunks); + var dataLength = data.length; + + // JBIG2 had black as 1 and white as 0, inverting the colors + for (var i = 0; i < dataLength; i++) { + data[i] ^= 0xFF; + } + + this.buffer = data; + this.bufferLength = dataLength; + this.eof = true; + }; + + return Jbig2Stream; +})(); + +var DecryptStream = (function DecryptStreamClosure() { + function DecryptStream(str, maybeLength, decrypt) { + this.str = str; + this.dict = str.dict; + this.decrypt = decrypt; + this.nextChunk = null; + this.initialized = false; + + DecodeStream.call(this, maybeLength); + } + + var chunkSize = 512; + + DecryptStream.prototype = Object.create(DecodeStream.prototype); + + DecryptStream.prototype.readBlock = function DecryptStream_readBlock() { + var chunk; + if (this.initialized) { + chunk = this.nextChunk; + } else { + chunk = this.str.getBytes(chunkSize); + this.initialized = true; + } + if (!chunk || chunk.length === 0) { + this.eof = true; + return; + } + this.nextChunk = this.str.getBytes(chunkSize); + var hasMoreData = this.nextChunk && this.nextChunk.length > 0; + + var decrypt = this.decrypt; + chunk = decrypt(chunk, !hasMoreData); + + var bufferLength = this.bufferLength; + var i, n = chunk.length; + var buffer = this.ensureBuffer(bufferLength + n); + for (i = 0; i < n; i++) { + buffer[bufferLength++] = chunk[i]; + } + this.bufferLength = bufferLength; + }; + + return DecryptStream; +})(); + +var Ascii85Stream = (function Ascii85StreamClosure() { + function Ascii85Stream(str, maybeLength) { + this.str = str; + this.dict = str.dict; + this.input = new Uint8Array(5); + + // Most streams increase in size when decoded, but Ascii85 streams + // typically shrink by ~20%. + if (maybeLength) { + maybeLength = 0.8 * maybeLength; + } + DecodeStream.call(this, maybeLength); + } + + Ascii85Stream.prototype = Object.create(DecodeStream.prototype); + + Ascii85Stream.prototype.readBlock = function Ascii85Stream_readBlock() { + var TILDA_CHAR = 0x7E; // '~' + var Z_LOWER_CHAR = 0x7A; // 'z' + var EOF = -1; + + var str = this.str; + + var c = str.getByte(); + while (Lexer.isSpace(c)) { + c = str.getByte(); + } + + if (c === EOF || c === TILDA_CHAR) { + this.eof = true; + return; + } + + var bufferLength = this.bufferLength, buffer; + var i; + + // special code for z + if (c === Z_LOWER_CHAR) { + buffer = this.ensureBuffer(bufferLength + 4); + for (i = 0; i < 4; ++i) { + buffer[bufferLength + i] = 0; + } + this.bufferLength += 4; + } else { + var input = this.input; + input[0] = c; + for (i = 1; i < 5; ++i) { + c = str.getByte(); + while (Lexer.isSpace(c)) { + c = str.getByte(); + } + + input[i] = c; + + if (c === EOF || c === TILDA_CHAR) { + break; + } + } + buffer = this.ensureBuffer(bufferLength + i - 1); + this.bufferLength += i - 1; + + // partial ending; + if (i < 5) { + for (; i < 5; ++i) { + input[i] = 0x21 + 84; + } + this.eof = true; + } + var t = 0; + for (i = 0; i < 5; ++i) { + t = t * 85 + (input[i] - 0x21); + } + + for (i = 3; i >= 0; --i) { + buffer[bufferLength + i] = t & 0xFF; + t >>= 8; + } + } + }; + + return Ascii85Stream; +})(); + +var AsciiHexStream = (function AsciiHexStreamClosure() { + function AsciiHexStream(str, maybeLength) { + this.str = str; + this.dict = str.dict; + + this.firstDigit = -1; + + // Most streams increase in size when decoded, but AsciiHex streams shrink + // by 50%. + if (maybeLength) { + maybeLength = 0.5 * maybeLength; + } + DecodeStream.call(this, maybeLength); + } + + AsciiHexStream.prototype = Object.create(DecodeStream.prototype); + + AsciiHexStream.prototype.readBlock = function AsciiHexStream_readBlock() { + var UPSTREAM_BLOCK_SIZE = 8000; + var bytes = this.str.getBytes(UPSTREAM_BLOCK_SIZE); + if (!bytes.length) { + this.eof = true; + return; + } + + var maxDecodeLength = (bytes.length + 1) >> 1; + var buffer = this.ensureBuffer(this.bufferLength + maxDecodeLength); + var bufferLength = this.bufferLength; + + var firstDigit = this.firstDigit; + for (var i = 0, ii = bytes.length; i < ii; i++) { + var ch = bytes[i], digit; + if (ch >= 0x30 && ch <= 0x39) { // '0'-'9' + digit = ch & 0x0F; + } else if ((ch >= 0x41 && ch <= 0x46) || (ch >= 0x61 && ch <= 0x66)) { + // 'A'-'Z', 'a'-'z' + digit = (ch & 0x0F) + 9; + } else if (ch === 0x3E) { // '>' + this.eof = true; + break; + } else { // probably whitespace + continue; // ignoring + } + if (firstDigit < 0) { + firstDigit = digit; + } else { + buffer[bufferLength++] = (firstDigit << 4) | digit; + firstDigit = -1; + } + } + if (firstDigit >= 0 && this.eof) { + // incomplete byte + buffer[bufferLength++] = (firstDigit << 4); + firstDigit = -1; + } + this.firstDigit = firstDigit; + this.bufferLength = bufferLength; + }; + + return AsciiHexStream; +})(); + +var RunLengthStream = (function RunLengthStreamClosure() { + function RunLengthStream(str, maybeLength) { + this.str = str; + this.dict = str.dict; + + DecodeStream.call(this, maybeLength); + } + + RunLengthStream.prototype = Object.create(DecodeStream.prototype); + + RunLengthStream.prototype.readBlock = function RunLengthStream_readBlock() { + // The repeatHeader has following format. The first byte defines type of run + // and amount of bytes to repeat/copy: n = 0 through 127 - copy next n bytes + // (in addition to the second byte from the header), n = 129 through 255 - + // duplicate the second byte from the header (257 - n) times, n = 128 - end. + var repeatHeader = this.str.getBytes(2); + if (!repeatHeader || repeatHeader.length < 2 || repeatHeader[0] === 128) { + this.eof = true; + return; + } + + var buffer; + var bufferLength = this.bufferLength; + var n = repeatHeader[0]; + if (n < 128) { + // copy n bytes + buffer = this.ensureBuffer(bufferLength + n + 1); + buffer[bufferLength++] = repeatHeader[1]; + if (n > 0) { + var source = this.str.getBytes(n); + buffer.set(source, bufferLength); + bufferLength += n; + } + } else { + n = 257 - n; + var b = repeatHeader[1]; + buffer = this.ensureBuffer(bufferLength + n + 1); + for (var i = 0; i < n; i++) { + buffer[bufferLength++] = b; + } + } + this.bufferLength = bufferLength; + }; + + return RunLengthStream; +})(); + +var CCITTFaxStream = (function CCITTFaxStreamClosure() { + + var ccittEOL = -2; + var twoDimPass = 0; + var twoDimHoriz = 1; + var twoDimVert0 = 2; + var twoDimVertR1 = 3; + var twoDimVertL1 = 4; + var twoDimVertR2 = 5; + var twoDimVertL2 = 6; + var twoDimVertR3 = 7; + var twoDimVertL3 = 8; + + var twoDimTable = [ + [-1, -1], [-1, -1], // 000000x + [7, twoDimVertL3], // 0000010 + [7, twoDimVertR3], // 0000011 + [6, twoDimVertL2], [6, twoDimVertL2], // 000010x + [6, twoDimVertR2], [6, twoDimVertR2], // 000011x + [4, twoDimPass], [4, twoDimPass], // 0001xxx + [4, twoDimPass], [4, twoDimPass], + [4, twoDimPass], [4, twoDimPass], + [4, twoDimPass], [4, twoDimPass], + [3, twoDimHoriz], [3, twoDimHoriz], // 001xxxx + [3, twoDimHoriz], [3, twoDimHoriz], + [3, twoDimHoriz], [3, twoDimHoriz], + [3, twoDimHoriz], [3, twoDimHoriz], + [3, twoDimHoriz], [3, twoDimHoriz], + [3, twoDimHoriz], [3, twoDimHoriz], + [3, twoDimHoriz], [3, twoDimHoriz], + [3, twoDimHoriz], [3, twoDimHoriz], + [3, twoDimVertL1], [3, twoDimVertL1], // 010xxxx + [3, twoDimVertL1], [3, twoDimVertL1], + [3, twoDimVertL1], [3, twoDimVertL1], + [3, twoDimVertL1], [3, twoDimVertL1], + [3, twoDimVertL1], [3, twoDimVertL1], + [3, twoDimVertL1], [3, twoDimVertL1], + [3, twoDimVertL1], [3, twoDimVertL1], + [3, twoDimVertL1], [3, twoDimVertL1], + [3, twoDimVertR1], [3, twoDimVertR1], // 011xxxx + [3, twoDimVertR1], [3, twoDimVertR1], + [3, twoDimVertR1], [3, twoDimVertR1], + [3, twoDimVertR1], [3, twoDimVertR1], + [3, twoDimVertR1], [3, twoDimVertR1], + [3, twoDimVertR1], [3, twoDimVertR1], + [3, twoDimVertR1], [3, twoDimVertR1], + [3, twoDimVertR1], [3, twoDimVertR1], + [1, twoDimVert0], [1, twoDimVert0], // 1xxxxxx + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0], + [1, twoDimVert0], [1, twoDimVert0] + ]; + + var whiteTable1 = [ + [-1, -1], // 00000 + [12, ccittEOL], // 00001 + [-1, -1], [-1, -1], // 0001x + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 001xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 010xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 011xx + [11, 1792], [11, 1792], // 1000x + [12, 1984], // 10010 + [12, 2048], // 10011 + [12, 2112], // 10100 + [12, 2176], // 10101 + [12, 2240], // 10110 + [12, 2304], // 10111 + [11, 1856], [11, 1856], // 1100x + [11, 1920], [11, 1920], // 1101x + [12, 2368], // 11100 + [12, 2432], // 11101 + [12, 2496], // 11110 + [12, 2560] // 11111 + ]; + + var whiteTable2 = [ + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 0000000xx + [8, 29], [8, 29], // 00000010x + [8, 30], [8, 30], // 00000011x + [8, 45], [8, 45], // 00000100x + [8, 46], [8, 46], // 00000101x + [7, 22], [7, 22], [7, 22], [7, 22], // 0000011xx + [7, 23], [7, 23], [7, 23], [7, 23], // 0000100xx + [8, 47], [8, 47], // 00001010x + [8, 48], [8, 48], // 00001011x + [6, 13], [6, 13], [6, 13], [6, 13], // 000011xxx + [6, 13], [6, 13], [6, 13], [6, 13], + [7, 20], [7, 20], [7, 20], [7, 20], // 0001000xx + [8, 33], [8, 33], // 00010010x + [8, 34], [8, 34], // 00010011x + [8, 35], [8, 35], // 00010100x + [8, 36], [8, 36], // 00010101x + [8, 37], [8, 37], // 00010110x + [8, 38], [8, 38], // 00010111x + [7, 19], [7, 19], [7, 19], [7, 19], // 0001100xx + [8, 31], [8, 31], // 00011010x + [8, 32], [8, 32], // 00011011x + [6, 1], [6, 1], [6, 1], [6, 1], // 000111xxx + [6, 1], [6, 1], [6, 1], [6, 1], + [6, 12], [6, 12], [6, 12], [6, 12], // 001000xxx + [6, 12], [6, 12], [6, 12], [6, 12], + [8, 53], [8, 53], // 00100100x + [8, 54], [8, 54], // 00100101x + [7, 26], [7, 26], [7, 26], [7, 26], // 0010011xx + [8, 39], [8, 39], // 00101000x + [8, 40], [8, 40], // 00101001x + [8, 41], [8, 41], // 00101010x + [8, 42], [8, 42], // 00101011x + [8, 43], [8, 43], // 00101100x + [8, 44], [8, 44], // 00101101x + [7, 21], [7, 21], [7, 21], [7, 21], // 0010111xx + [7, 28], [7, 28], [7, 28], [7, 28], // 0011000xx + [8, 61], [8, 61], // 00110010x + [8, 62], [8, 62], // 00110011x + [8, 63], [8, 63], // 00110100x + [8, 0], [8, 0], // 00110101x + [8, 320], [8, 320], // 00110110x + [8, 384], [8, 384], // 00110111x + [5, 10], [5, 10], [5, 10], [5, 10], // 00111xxxx + [5, 10], [5, 10], [5, 10], [5, 10], + [5, 10], [5, 10], [5, 10], [5, 10], + [5, 10], [5, 10], [5, 10], [5, 10], + [5, 11], [5, 11], [5, 11], [5, 11], // 01000xxxx + [5, 11], [5, 11], [5, 11], [5, 11], + [5, 11], [5, 11], [5, 11], [5, 11], + [5, 11], [5, 11], [5, 11], [5, 11], + [7, 27], [7, 27], [7, 27], [7, 27], // 0100100xx + [8, 59], [8, 59], // 01001010x + [8, 60], [8, 60], // 01001011x + [9, 1472], // 010011000 + [9, 1536], // 010011001 + [9, 1600], // 010011010 + [9, 1728], // 010011011 + [7, 18], [7, 18], [7, 18], [7, 18], // 0100111xx + [7, 24], [7, 24], [7, 24], [7, 24], // 0101000xx + [8, 49], [8, 49], // 01010010x + [8, 50], [8, 50], // 01010011x + [8, 51], [8, 51], // 01010100x + [8, 52], [8, 52], // 01010101x + [7, 25], [7, 25], [7, 25], [7, 25], // 0101011xx + [8, 55], [8, 55], // 01011000x + [8, 56], [8, 56], // 01011001x + [8, 57], [8, 57], // 01011010x + [8, 58], [8, 58], // 01011011x + [6, 192], [6, 192], [6, 192], [6, 192], // 010111xxx + [6, 192], [6, 192], [6, 192], [6, 192], + [6, 1664], [6, 1664], [6, 1664], [6, 1664], // 011000xxx + [6, 1664], [6, 1664], [6, 1664], [6, 1664], + [8, 448], [8, 448], // 01100100x + [8, 512], [8, 512], // 01100101x + [9, 704], // 011001100 + [9, 768], // 011001101 + [8, 640], [8, 640], // 01100111x + [8, 576], [8, 576], // 01101000x + [9, 832], // 011010010 + [9, 896], // 011010011 + [9, 960], // 011010100 + [9, 1024], // 011010101 + [9, 1088], // 011010110 + [9, 1152], // 011010111 + [9, 1216], // 011011000 + [9, 1280], // 011011001 + [9, 1344], // 011011010 + [9, 1408], // 011011011 + [7, 256], [7, 256], [7, 256], [7, 256], // 0110111xx + [4, 2], [4, 2], [4, 2], [4, 2], // 0111xxxxx + [4, 2], [4, 2], [4, 2], [4, 2], + [4, 2], [4, 2], [4, 2], [4, 2], + [4, 2], [4, 2], [4, 2], [4, 2], + [4, 2], [4, 2], [4, 2], [4, 2], + [4, 2], [4, 2], [4, 2], [4, 2], + [4, 2], [4, 2], [4, 2], [4, 2], + [4, 2], [4, 2], [4, 2], [4, 2], + [4, 3], [4, 3], [4, 3], [4, 3], // 1000xxxxx + [4, 3], [4, 3], [4, 3], [4, 3], + [4, 3], [4, 3], [4, 3], [4, 3], + [4, 3], [4, 3], [4, 3], [4, 3], + [4, 3], [4, 3], [4, 3], [4, 3], + [4, 3], [4, 3], [4, 3], [4, 3], + [4, 3], [4, 3], [4, 3], [4, 3], + [4, 3], [4, 3], [4, 3], [4, 3], + [5, 128], [5, 128], [5, 128], [5, 128], // 10010xxxx + [5, 128], [5, 128], [5, 128], [5, 128], + [5, 128], [5, 128], [5, 128], [5, 128], + [5, 128], [5, 128], [5, 128], [5, 128], + [5, 8], [5, 8], [5, 8], [5, 8], // 10011xxxx + [5, 8], [5, 8], [5, 8], [5, 8], + [5, 8], [5, 8], [5, 8], [5, 8], + [5, 8], [5, 8], [5, 8], [5, 8], + [5, 9], [5, 9], [5, 9], [5, 9], // 10100xxxx + [5, 9], [5, 9], [5, 9], [5, 9], + [5, 9], [5, 9], [5, 9], [5, 9], + [5, 9], [5, 9], [5, 9], [5, 9], + [6, 16], [6, 16], [6, 16], [6, 16], // 101010xxx + [6, 16], [6, 16], [6, 16], [6, 16], + [6, 17], [6, 17], [6, 17], [6, 17], // 101011xxx + [6, 17], [6, 17], [6, 17], [6, 17], + [4, 4], [4, 4], [4, 4], [4, 4], // 1011xxxxx + [4, 4], [4, 4], [4, 4], [4, 4], + [4, 4], [4, 4], [4, 4], [4, 4], + [4, 4], [4, 4], [4, 4], [4, 4], + [4, 4], [4, 4], [4, 4], [4, 4], + [4, 4], [4, 4], [4, 4], [4, 4], + [4, 4], [4, 4], [4, 4], [4, 4], + [4, 4], [4, 4], [4, 4], [4, 4], + [4, 5], [4, 5], [4, 5], [4, 5], // 1100xxxxx + [4, 5], [4, 5], [4, 5], [4, 5], + [4, 5], [4, 5], [4, 5], [4, 5], + [4, 5], [4, 5], [4, 5], [4, 5], + [4, 5], [4, 5], [4, 5], [4, 5], + [4, 5], [4, 5], [4, 5], [4, 5], + [4, 5], [4, 5], [4, 5], [4, 5], + [4, 5], [4, 5], [4, 5], [4, 5], + [6, 14], [6, 14], [6, 14], [6, 14], // 110100xxx + [6, 14], [6, 14], [6, 14], [6, 14], + [6, 15], [6, 15], [6, 15], [6, 15], // 110101xxx + [6, 15], [6, 15], [6, 15], [6, 15], + [5, 64], [5, 64], [5, 64], [5, 64], // 11011xxxx + [5, 64], [5, 64], [5, 64], [5, 64], + [5, 64], [5, 64], [5, 64], [5, 64], + [5, 64], [5, 64], [5, 64], [5, 64], + [4, 6], [4, 6], [4, 6], [4, 6], // 1110xxxxx + [4, 6], [4, 6], [4, 6], [4, 6], + [4, 6], [4, 6], [4, 6], [4, 6], + [4, 6], [4, 6], [4, 6], [4, 6], + [4, 6], [4, 6], [4, 6], [4, 6], + [4, 6], [4, 6], [4, 6], [4, 6], + [4, 6], [4, 6], [4, 6], [4, 6], + [4, 6], [4, 6], [4, 6], [4, 6], + [4, 7], [4, 7], [4, 7], [4, 7], // 1111xxxxx + [4, 7], [4, 7], [4, 7], [4, 7], + [4, 7], [4, 7], [4, 7], [4, 7], + [4, 7], [4, 7], [4, 7], [4, 7], + [4, 7], [4, 7], [4, 7], [4, 7], + [4, 7], [4, 7], [4, 7], [4, 7], + [4, 7], [4, 7], [4, 7], [4, 7], + [4, 7], [4, 7], [4, 7], [4, 7] + ]; + + var blackTable1 = [ + [-1, -1], [-1, -1], // 000000000000x + [12, ccittEOL], [12, ccittEOL], // 000000000001x + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 00000000001xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 00000000010xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 00000000011xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 00000000100xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 00000000101xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 00000000110xx + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 00000000111xx + [11, 1792], [11, 1792], [11, 1792], [11, 1792], // 00000001000xx + [12, 1984], [12, 1984], // 000000010010x + [12, 2048], [12, 2048], // 000000010011x + [12, 2112], [12, 2112], // 000000010100x + [12, 2176], [12, 2176], // 000000010101x + [12, 2240], [12, 2240], // 000000010110x + [12, 2304], [12, 2304], // 000000010111x + [11, 1856], [11, 1856], [11, 1856], [11, 1856], // 00000001100xx + [11, 1920], [11, 1920], [11, 1920], [11, 1920], // 00000001101xx + [12, 2368], [12, 2368], // 000000011100x + [12, 2432], [12, 2432], // 000000011101x + [12, 2496], [12, 2496], // 000000011110x + [12, 2560], [12, 2560], // 000000011111x + [10, 18], [10, 18], [10, 18], [10, 18], // 0000001000xxx + [10, 18], [10, 18], [10, 18], [10, 18], + [12, 52], [12, 52], // 000000100100x + [13, 640], // 0000001001010 + [13, 704], // 0000001001011 + [13, 768], // 0000001001100 + [13, 832], // 0000001001101 + [12, 55], [12, 55], // 000000100111x + [12, 56], [12, 56], // 000000101000x + [13, 1280], // 0000001010010 + [13, 1344], // 0000001010011 + [13, 1408], // 0000001010100 + [13, 1472], // 0000001010101 + [12, 59], [12, 59], // 000000101011x + [12, 60], [12, 60], // 000000101100x + [13, 1536], // 0000001011010 + [13, 1600], // 0000001011011 + [11, 24], [11, 24], [11, 24], [11, 24], // 00000010111xx + [11, 25], [11, 25], [11, 25], [11, 25], // 00000011000xx + [13, 1664], // 0000001100100 + [13, 1728], // 0000001100101 + [12, 320], [12, 320], // 000000110011x + [12, 384], [12, 384], // 000000110100x + [12, 448], [12, 448], // 000000110101x + [13, 512], // 0000001101100 + [13, 576], // 0000001101101 + [12, 53], [12, 53], // 000000110111x + [12, 54], [12, 54], // 000000111000x + [13, 896], // 0000001110010 + [13, 960], // 0000001110011 + [13, 1024], // 0000001110100 + [13, 1088], // 0000001110101 + [13, 1152], // 0000001110110 + [13, 1216], // 0000001110111 + [10, 64], [10, 64], [10, 64], [10, 64], // 0000001111xxx + [10, 64], [10, 64], [10, 64], [10, 64] + ]; + + var blackTable2 = [ + [8, 13], [8, 13], [8, 13], [8, 13], // 00000100xxxx + [8, 13], [8, 13], [8, 13], [8, 13], + [8, 13], [8, 13], [8, 13], [8, 13], + [8, 13], [8, 13], [8, 13], [8, 13], + [11, 23], [11, 23], // 00000101000x + [12, 50], // 000001010010 + [12, 51], // 000001010011 + [12, 44], // 000001010100 + [12, 45], // 000001010101 + [12, 46], // 000001010110 + [12, 47], // 000001010111 + [12, 57], // 000001011000 + [12, 58], // 000001011001 + [12, 61], // 000001011010 + [12, 256], // 000001011011 + [10, 16], [10, 16], [10, 16], [10, 16], // 0000010111xx + [10, 17], [10, 17], [10, 17], [10, 17], // 0000011000xx + [12, 48], // 000001100100 + [12, 49], // 000001100101 + [12, 62], // 000001100110 + [12, 63], // 000001100111 + [12, 30], // 000001101000 + [12, 31], // 000001101001 + [12, 32], // 000001101010 + [12, 33], // 000001101011 + [12, 40], // 000001101100 + [12, 41], // 000001101101 + [11, 22], [11, 22], // 00000110111x + [8, 14], [8, 14], [8, 14], [8, 14], // 00000111xxxx + [8, 14], [8, 14], [8, 14], [8, 14], + [8, 14], [8, 14], [8, 14], [8, 14], + [8, 14], [8, 14], [8, 14], [8, 14], + [7, 10], [7, 10], [7, 10], [7, 10], // 0000100xxxxx + [7, 10], [7, 10], [7, 10], [7, 10], + [7, 10], [7, 10], [7, 10], [7, 10], + [7, 10], [7, 10], [7, 10], [7, 10], + [7, 10], [7, 10], [7, 10], [7, 10], + [7, 10], [7, 10], [7, 10], [7, 10], + [7, 10], [7, 10], [7, 10], [7, 10], + [7, 10], [7, 10], [7, 10], [7, 10], + [7, 11], [7, 11], [7, 11], [7, 11], // 0000101xxxxx + [7, 11], [7, 11], [7, 11], [7, 11], + [7, 11], [7, 11], [7, 11], [7, 11], + [7, 11], [7, 11], [7, 11], [7, 11], + [7, 11], [7, 11], [7, 11], [7, 11], + [7, 11], [7, 11], [7, 11], [7, 11], + [7, 11], [7, 11], [7, 11], [7, 11], + [7, 11], [7, 11], [7, 11], [7, 11], + [9, 15], [9, 15], [9, 15], [9, 15], // 000011000xxx + [9, 15], [9, 15], [9, 15], [9, 15], + [12, 128], // 000011001000 + [12, 192], // 000011001001 + [12, 26], // 000011001010 + [12, 27], // 000011001011 + [12, 28], // 000011001100 + [12, 29], // 000011001101 + [11, 19], [11, 19], // 00001100111x + [11, 20], [11, 20], // 00001101000x + [12, 34], // 000011010010 + [12, 35], // 000011010011 + [12, 36], // 000011010100 + [12, 37], // 000011010101 + [12, 38], // 000011010110 + [12, 39], // 000011010111 + [11, 21], [11, 21], // 00001101100x + [12, 42], // 000011011010 + [12, 43], // 000011011011 + [10, 0], [10, 0], [10, 0], [10, 0], // 0000110111xx + [7, 12], [7, 12], [7, 12], [7, 12], // 0000111xxxxx + [7, 12], [7, 12], [7, 12], [7, 12], + [7, 12], [7, 12], [7, 12], [7, 12], + [7, 12], [7, 12], [7, 12], [7, 12], + [7, 12], [7, 12], [7, 12], [7, 12], + [7, 12], [7, 12], [7, 12], [7, 12], + [7, 12], [7, 12], [7, 12], [7, 12], + [7, 12], [7, 12], [7, 12], [7, 12] + ]; + + var blackTable3 = [ + [-1, -1], [-1, -1], [-1, -1], [-1, -1], // 0000xx + [6, 9], // 000100 + [6, 8], // 000101 + [5, 7], [5, 7], // 00011x + [4, 6], [4, 6], [4, 6], [4, 6], // 0010xx + [4, 5], [4, 5], [4, 5], [4, 5], // 0011xx + [3, 1], [3, 1], [3, 1], [3, 1], // 010xxx + [3, 1], [3, 1], [3, 1], [3, 1], + [3, 4], [3, 4], [3, 4], [3, 4], // 011xxx + [3, 4], [3, 4], [3, 4], [3, 4], + [2, 3], [2, 3], [2, 3], [2, 3], // 10xxxx + [2, 3], [2, 3], [2, 3], [2, 3], + [2, 3], [2, 3], [2, 3], [2, 3], + [2, 3], [2, 3], [2, 3], [2, 3], + [2, 2], [2, 2], [2, 2], [2, 2], // 11xxxx + [2, 2], [2, 2], [2, 2], [2, 2], + [2, 2], [2, 2], [2, 2], [2, 2], + [2, 2], [2, 2], [2, 2], [2, 2] + ]; + + function CCITTFaxStream(str, maybeLength, params) { + this.str = str; + this.dict = str.dict; + + params = params || Dict.empty; + + this.encoding = params.get('K') || 0; + this.eoline = params.get('EndOfLine') || false; + this.byteAlign = params.get('EncodedByteAlign') || false; + this.columns = params.get('Columns') || 1728; + this.rows = params.get('Rows') || 0; + var eoblock = params.get('EndOfBlock'); + if (eoblock === null || eoblock === undefined) { + eoblock = true; + } + this.eoblock = eoblock; + this.black = params.get('BlackIs1') || false; + + this.codingLine = new Uint32Array(this.columns + 1); + this.refLine = new Uint32Array(this.columns + 2); + + this.codingLine[0] = this.columns; + this.codingPos = 0; + + this.row = 0; + this.nextLine2D = this.encoding < 0; + this.inputBits = 0; + this.inputBuf = 0; + this.outputBits = 0; + + var code1; + while ((code1 = this.lookBits(12)) === 0) { + this.eatBits(1); + } + if (code1 === 1) { + this.eatBits(12); + } + if (this.encoding > 0) { + this.nextLine2D = !this.lookBits(1); + this.eatBits(1); + } + + DecodeStream.call(this, maybeLength); + } + + CCITTFaxStream.prototype = Object.create(DecodeStream.prototype); + + CCITTFaxStream.prototype.readBlock = function CCITTFaxStream_readBlock() { + while (!this.eof) { + var c = this.lookChar(); + this.ensureBuffer(this.bufferLength + 1); + this.buffer[this.bufferLength++] = c; + } + }; + + CCITTFaxStream.prototype.addPixels = + function ccittFaxStreamAddPixels(a1, blackPixels) { + var codingLine = this.codingLine; + var codingPos = this.codingPos; + + if (a1 > codingLine[codingPos]) { + if (a1 > this.columns) { + info('row is wrong length'); + this.err = true; + a1 = this.columns; + } + if ((codingPos & 1) ^ blackPixels) { + ++codingPos; + } + + codingLine[codingPos] = a1; + } + this.codingPos = codingPos; + }; + + CCITTFaxStream.prototype.addPixelsNeg = + function ccittFaxStreamAddPixelsNeg(a1, blackPixels) { + var codingLine = this.codingLine; + var codingPos = this.codingPos; + + if (a1 > codingLine[codingPos]) { + if (a1 > this.columns) { + info('row is wrong length'); + this.err = true; + a1 = this.columns; + } + if ((codingPos & 1) ^ blackPixels) { + ++codingPos; + } + + codingLine[codingPos] = a1; + } else if (a1 < codingLine[codingPos]) { + if (a1 < 0) { + info('invalid code'); + this.err = true; + a1 = 0; + } + while (codingPos > 0 && a1 < codingLine[codingPos - 1]) { + --codingPos; + } + codingLine[codingPos] = a1; + } + + this.codingPos = codingPos; + }; + + CCITTFaxStream.prototype.lookChar = function CCITTFaxStream_lookChar() { + var refLine = this.refLine; + var codingLine = this.codingLine; + var columns = this.columns; + + var refPos, blackPixels, bits, i; + + if (this.outputBits === 0) { + if (this.eof) { + return null; + } + this.err = false; + + var code1, code2, code3; + if (this.nextLine2D) { + for (i = 0; codingLine[i] < columns; ++i) { + refLine[i] = codingLine[i]; + } + refLine[i++] = columns; + refLine[i] = columns; + codingLine[0] = 0; + this.codingPos = 0; + refPos = 0; + blackPixels = 0; + + while (codingLine[this.codingPos] < columns) { + code1 = this.getTwoDimCode(); + switch (code1) { + case twoDimPass: + this.addPixels(refLine[refPos + 1], blackPixels); + if (refLine[refPos + 1] < columns) { + refPos += 2; + } + break; + case twoDimHoriz: + code1 = code2 = 0; + if (blackPixels) { + do { + code1 += (code3 = this.getBlackCode()); + } while (code3 >= 64); + do { + code2 += (code3 = this.getWhiteCode()); + } while (code3 >= 64); + } else { + do { + code1 += (code3 = this.getWhiteCode()); + } while (code3 >= 64); + do { + code2 += (code3 = this.getBlackCode()); + } while (code3 >= 64); + } + this.addPixels(codingLine[this.codingPos] + + code1, blackPixels); + if (codingLine[this.codingPos] < columns) { + this.addPixels(codingLine[this.codingPos] + code2, + blackPixels ^ 1); + } + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + break; + case twoDimVertR3: + this.addPixels(refLine[refPos] + 3, blackPixels); + blackPixels ^= 1; + if (codingLine[this.codingPos] < columns) { + ++refPos; + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + } + break; + case twoDimVertR2: + this.addPixels(refLine[refPos] + 2, blackPixels); + blackPixels ^= 1; + if (codingLine[this.codingPos] < columns) { + ++refPos; + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + } + break; + case twoDimVertR1: + this.addPixels(refLine[refPos] + 1, blackPixels); + blackPixels ^= 1; + if (codingLine[this.codingPos] < columns) { + ++refPos; + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + } + break; + case twoDimVert0: + this.addPixels(refLine[refPos], blackPixels); + blackPixels ^= 1; + if (codingLine[this.codingPos] < columns) { + ++refPos; + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + } + break; + case twoDimVertL3: + this.addPixelsNeg(refLine[refPos] - 3, blackPixels); + blackPixels ^= 1; + if (codingLine[this.codingPos] < columns) { + if (refPos > 0) { + --refPos; + } else { + ++refPos; + } + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + } + break; + case twoDimVertL2: + this.addPixelsNeg(refLine[refPos] - 2, blackPixels); + blackPixels ^= 1; + if (codingLine[this.codingPos] < columns) { + if (refPos > 0) { + --refPos; + } else { + ++refPos; + } + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + } + break; + case twoDimVertL1: + this.addPixelsNeg(refLine[refPos] - 1, blackPixels); + blackPixels ^= 1; + if (codingLine[this.codingPos] < columns) { + if (refPos > 0) { + --refPos; + } else { + ++refPos; + } + while (refLine[refPos] <= codingLine[this.codingPos] && + refLine[refPos] < columns) { + refPos += 2; + } + } + break; + case EOF: + this.addPixels(columns, 0); + this.eof = true; + break; + default: + info('bad 2d code'); + this.addPixels(columns, 0); + this.err = true; + } + } + } else { + codingLine[0] = 0; + this.codingPos = 0; + blackPixels = 0; + while (codingLine[this.codingPos] < columns) { + code1 = 0; + if (blackPixels) { + do { + code1 += (code3 = this.getBlackCode()); + } while (code3 >= 64); + } else { + do { + code1 += (code3 = this.getWhiteCode()); + } while (code3 >= 64); + } + this.addPixels(codingLine[this.codingPos] + code1, blackPixels); + blackPixels ^= 1; + } + } + + var gotEOL = false; + + if (this.byteAlign) { + this.inputBits &= ~7; + } + + if (!this.eoblock && this.row === this.rows - 1) { + this.eof = true; + } else { + code1 = this.lookBits(12); + if (this.eoline) { + while (code1 !== EOF && code1 !== 1) { + this.eatBits(1); + code1 = this.lookBits(12); + } + } else { + while (code1 === 0) { + this.eatBits(1); + code1 = this.lookBits(12); + } + } + if (code1 === 1) { + this.eatBits(12); + gotEOL = true; + } else if (code1 === EOF) { + this.eof = true; + } + } + + if (!this.eof && this.encoding > 0) { + this.nextLine2D = !this.lookBits(1); + this.eatBits(1); + } + + if (this.eoblock && gotEOL && this.byteAlign) { + code1 = this.lookBits(12); + if (code1 === 1) { + this.eatBits(12); + if (this.encoding > 0) { + this.lookBits(1); + this.eatBits(1); + } + if (this.encoding >= 0) { + for (i = 0; i < 4; ++i) { + code1 = this.lookBits(12); + if (code1 !== 1) { + info('bad rtc code: ' + code1); + } + this.eatBits(12); + if (this.encoding > 0) { + this.lookBits(1); + this.eatBits(1); + } + } + } + this.eof = true; + } + } else if (this.err && this.eoline) { + while (true) { + code1 = this.lookBits(13); + if (code1 === EOF) { + this.eof = true; + return null; + } + if ((code1 >> 1) === 1) { + break; + } + this.eatBits(1); + } + this.eatBits(12); + if (this.encoding > 0) { + this.eatBits(1); + this.nextLine2D = !(code1 & 1); + } + } + + if (codingLine[0] > 0) { + this.outputBits = codingLine[this.codingPos = 0]; + } else { + this.outputBits = codingLine[this.codingPos = 1]; + } + this.row++; + } + + var c; + if (this.outputBits >= 8) { + c = (this.codingPos & 1) ? 0 : 0xFF; + this.outputBits -= 8; + if (this.outputBits === 0 && codingLine[this.codingPos] < columns) { + this.codingPos++; + this.outputBits = (codingLine[this.codingPos] - + codingLine[this.codingPos - 1]); + } + } else { + bits = 8; + c = 0; + do { + if (this.outputBits > bits) { + c <<= bits; + if (!(this.codingPos & 1)) { + c |= 0xFF >> (8 - bits); + } + this.outputBits -= bits; + bits = 0; + } else { + c <<= this.outputBits; + if (!(this.codingPos & 1)) { + c |= 0xFF >> (8 - this.outputBits); + } + bits -= this.outputBits; + this.outputBits = 0; + if (codingLine[this.codingPos] < columns) { + this.codingPos++; + this.outputBits = (codingLine[this.codingPos] - + codingLine[this.codingPos - 1]); + } else if (bits > 0) { + c <<= bits; + bits = 0; + } + } + } while (bits); + } + if (this.black) { + c ^= 0xFF; + } + return c; + }; + + // This functions returns the code found from the table. + // The start and end parameters set the boundaries for searching the table. + // The limit parameter is optional. Function returns an array with three + // values. The first array element indicates whether a valid code is being + // returned. The second array element is the actual code. The third array + // element indicates whether EOF was reached. + CCITTFaxStream.prototype.findTableCode = + function ccittFaxStreamFindTableCode(start, end, table, limit) { + + var limitValue = limit || 0; + for (var i = start; i <= end; ++i) { + var code = this.lookBits(i); + if (code === EOF) { + return [true, 1, false]; + } + if (i < end) { + code <<= end - i; + } + if (!limitValue || code >= limitValue) { + var p = table[code - limitValue]; + if (p[0] === i) { + this.eatBits(i); + return [true, p[1], true]; + } + } + } + return [false, 0, false]; + }; + + CCITTFaxStream.prototype.getTwoDimCode = + function ccittFaxStreamGetTwoDimCode() { + + var code = 0; + var p; + if (this.eoblock) { + code = this.lookBits(7); + p = twoDimTable[code]; + if (p && p[0] > 0) { + this.eatBits(p[0]); + return p[1]; + } + } else { + var result = this.findTableCode(1, 7, twoDimTable); + if (result[0] && result[2]) { + return result[1]; + } + } + info('Bad two dim code'); + return EOF; + }; + + CCITTFaxStream.prototype.getWhiteCode = + function ccittFaxStreamGetWhiteCode() { + + var code = 0; + var p; + if (this.eoblock) { + code = this.lookBits(12); + if (code === EOF) { + return 1; + } + + if ((code >> 5) === 0) { + p = whiteTable1[code]; + } else { + p = whiteTable2[code >> 3]; + } + + if (p[0] > 0) { + this.eatBits(p[0]); + return p[1]; + } + } else { + var result = this.findTableCode(1, 9, whiteTable2); + if (result[0]) { + return result[1]; + } + + result = this.findTableCode(11, 12, whiteTable1); + if (result[0]) { + return result[1]; + } + } + info('bad white code'); + this.eatBits(1); + return 1; + }; + + CCITTFaxStream.prototype.getBlackCode = + function ccittFaxStreamGetBlackCode() { + + var code, p; + if (this.eoblock) { + code = this.lookBits(13); + if (code === EOF) { + return 1; + } + if ((code >> 7) === 0) { + p = blackTable1[code]; + } else if ((code >> 9) === 0 && (code >> 7) !== 0) { + p = blackTable2[(code >> 1) - 64]; + } else { + p = blackTable3[code >> 7]; + } + + if (p[0] > 0) { + this.eatBits(p[0]); + return p[1]; + } + } else { + var result = this.findTableCode(2, 6, blackTable3); + if (result[0]) { + return result[1]; + } + + result = this.findTableCode(7, 12, blackTable2, 64); + if (result[0]) { + return result[1]; + } + + result = this.findTableCode(10, 13, blackTable1); + if (result[0]) { + return result[1]; + } + } + info('bad black code'); + this.eatBits(1); + return 1; + }; + + CCITTFaxStream.prototype.lookBits = function CCITTFaxStream_lookBits(n) { + var c; + while (this.inputBits < n) { + if ((c = this.str.getByte()) === -1) { + if (this.inputBits === 0) { + return EOF; + } + return ((this.inputBuf << (n - this.inputBits)) & + (0xFFFF >> (16 - n))); + } + this.inputBuf = (this.inputBuf << 8) + c; + this.inputBits += 8; + } + return (this.inputBuf >> (this.inputBits - n)) & (0xFFFF >> (16 - n)); + }; + + CCITTFaxStream.prototype.eatBits = function CCITTFaxStream_eatBits(n) { + if ((this.inputBits -= n) < 0) { + this.inputBits = 0; + } + }; + + return CCITTFaxStream; +})(); + +var LZWStream = (function LZWStreamClosure() { + function LZWStream(str, maybeLength, earlyChange) { + this.str = str; + this.dict = str.dict; + this.cachedData = 0; + this.bitsCached = 0; + + var maxLzwDictionarySize = 4096; + var lzwState = { + earlyChange: earlyChange, + codeLength: 9, + nextCode: 258, + dictionaryValues: new Uint8Array(maxLzwDictionarySize), + dictionaryLengths: new Uint16Array(maxLzwDictionarySize), + dictionaryPrevCodes: new Uint16Array(maxLzwDictionarySize), + currentSequence: new Uint8Array(maxLzwDictionarySize), + currentSequenceLength: 0 + }; + for (var i = 0; i < 256; ++i) { + lzwState.dictionaryValues[i] = i; + lzwState.dictionaryLengths[i] = 1; + } + this.lzwState = lzwState; + + DecodeStream.call(this, maybeLength); + } + + LZWStream.prototype = Object.create(DecodeStream.prototype); + + LZWStream.prototype.readBits = function LZWStream_readBits(n) { + var bitsCached = this.bitsCached; + var cachedData = this.cachedData; + while (bitsCached < n) { + var c = this.str.getByte(); + if (c === -1) { + this.eof = true; + return null; + } + cachedData = (cachedData << 8) | c; + bitsCached += 8; + } + this.bitsCached = (bitsCached -= n); + this.cachedData = cachedData; + this.lastCode = null; + return (cachedData >>> bitsCached) & ((1 << n) - 1); + }; + + LZWStream.prototype.readBlock = function LZWStream_readBlock() { + var blockSize = 512; + var estimatedDecodedSize = blockSize * 2, decodedSizeDelta = blockSize; + var i, j, q; + + var lzwState = this.lzwState; + if (!lzwState) { + return; // eof was found + } + + var earlyChange = lzwState.earlyChange; + var nextCode = lzwState.nextCode; + var dictionaryValues = lzwState.dictionaryValues; + var dictionaryLengths = lzwState.dictionaryLengths; + var dictionaryPrevCodes = lzwState.dictionaryPrevCodes; + var codeLength = lzwState.codeLength; + var prevCode = lzwState.prevCode; + var currentSequence = lzwState.currentSequence; + var currentSequenceLength = lzwState.currentSequenceLength; + + var decodedLength = 0; + var currentBufferLength = this.bufferLength; + var buffer = this.ensureBuffer(this.bufferLength + estimatedDecodedSize); + + for (i = 0; i < blockSize; i++) { + var code = this.readBits(codeLength); + var hasPrev = currentSequenceLength > 0; + if (code < 256) { + currentSequence[0] = code; + currentSequenceLength = 1; + } else if (code >= 258) { + if (code < nextCode) { + currentSequenceLength = dictionaryLengths[code]; + for (j = currentSequenceLength - 1, q = code; j >= 0; j--) { + currentSequence[j] = dictionaryValues[q]; + q = dictionaryPrevCodes[q]; + } + } else { + currentSequence[currentSequenceLength++] = currentSequence[0]; + } + } else if (code === 256) { + codeLength = 9; + nextCode = 258; + currentSequenceLength = 0; + continue; + } else { + this.eof = true; + delete this.lzwState; + break; + } + + if (hasPrev) { + dictionaryPrevCodes[nextCode] = prevCode; + dictionaryLengths[nextCode] = dictionaryLengths[prevCode] + 1; + dictionaryValues[nextCode] = currentSequence[0]; + nextCode++; + codeLength = (nextCode + earlyChange) & (nextCode + earlyChange - 1) ? + codeLength : Math.min(Math.log(nextCode + earlyChange) / + 0.6931471805599453 + 1, 12) | 0; + } + prevCode = code; + + decodedLength += currentSequenceLength; + if (estimatedDecodedSize < decodedLength) { + do { + estimatedDecodedSize += decodedSizeDelta; + } while (estimatedDecodedSize < decodedLength); + buffer = this.ensureBuffer(this.bufferLength + estimatedDecodedSize); + } + for (j = 0; j < currentSequenceLength; j++) { + buffer[currentBufferLength++] = currentSequence[j]; + } + } + lzwState.nextCode = nextCode; + lzwState.codeLength = codeLength; + lzwState.prevCode = prevCode; + lzwState.currentSequenceLength = currentSequenceLength; + + this.bufferLength = currentBufferLength; + }; + + return LZWStream; +})(); + +var NullStream = (function NullStreamClosure() { + function NullStream() { + Stream.call(this, new Uint8Array(0)); + } + + NullStream.prototype = Stream.prototype; + + return NullStream; +})(); + + +var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { + setup: function wphSetup(handler) { + var pdfManager; + + function loadDocument(recoveryMode) { + var loadDocumentCapability = createPromiseCapability(); + + var parseSuccess = function parseSuccess() { + var numPagesPromise = pdfManager.ensureDoc('numPages'); + var fingerprintPromise = pdfManager.ensureDoc('fingerprint'); + var encryptedPromise = pdfManager.ensureXRef('encrypt'); + Promise.all([numPagesPromise, fingerprintPromise, + encryptedPromise]).then(function onDocReady(results) { + var doc = { + numPages: results[0], + fingerprint: results[1], + encrypted: !!results[2], + }; + loadDocumentCapability.resolve(doc); + }, + parseFailure); + }; + + var parseFailure = function parseFailure(e) { + loadDocumentCapability.reject(e); + }; + + pdfManager.ensureDoc('checkHeader', []).then(function() { + pdfManager.ensureDoc('parseStartXRef', []).then(function() { + pdfManager.ensureDoc('parse', [recoveryMode]).then( + parseSuccess, parseFailure); + }, parseFailure); + }, parseFailure); + + return loadDocumentCapability.promise; + } + + function getPdfManager(data) { + var pdfManagerCapability = createPromiseCapability(); + + var source = data.source; + var disableRange = data.disableRange; + if (source.data) { + try { + pdfManager = new LocalPdfManager(source.data, source.password); + pdfManagerCapability.resolve(); + } catch (ex) { + pdfManagerCapability.reject(ex); + } + return pdfManagerCapability.promise; + } else if (source.chunkedViewerLoading) { + try { + pdfManager = new NetworkPdfManager(source, handler); + pdfManagerCapability.resolve(); + } catch (ex) { + pdfManagerCapability.reject(ex); + } + return pdfManagerCapability.promise; + } + + var networkManager = new NetworkManager(source.url, { + httpHeaders: source.httpHeaders, + withCredentials: source.withCredentials + }); + var cachedChunks = []; + var fullRequestXhrId = networkManager.requestFull({ + onHeadersReceived: function onHeadersReceived() { + if (disableRange) { + return; + } + + var fullRequestXhr = networkManager.getRequestXhr(fullRequestXhrId); + if (fullRequestXhr.getResponseHeader('Accept-Ranges') !== 'bytes') { + return; + } + + var contentEncoding = + fullRequestXhr.getResponseHeader('Content-Encoding') || 'identity'; + if (contentEncoding !== 'identity') { + return; + } + + var length = fullRequestXhr.getResponseHeader('Content-Length'); + length = parseInt(length, 10); + if (!isInt(length)) { + return; + } + source.length = length; + if (length <= 2 * RANGE_CHUNK_SIZE) { + // The file size is smaller than the size of two chunks, so it does + // not make any sense to abort the request and retry with a range + // request. + return; + } + + if (networkManager.isStreamingRequest(fullRequestXhrId)) { + // We can continue fetching when progressive loading is enabled, + // and we don't need the autoFetch feature. + source.disableAutoFetch = true; + } else { + // NOTE: by cancelling the full request, and then issuing range + // requests, there will be an issue for sites where you can only + // request the pdf once. However, if this is the case, then the + // server should not be returning that it can support range + // requests. + networkManager.abortRequest(fullRequestXhrId); + } + + try { + pdfManager = new NetworkPdfManager(source, handler); + pdfManagerCapability.resolve(pdfManager); + } catch (ex) { + pdfManagerCapability.reject(ex); + } + }, + + onProgressiveData: source.disableStream ? null : + function onProgressiveData(chunk) { + if (!pdfManager) { + cachedChunks.push(chunk); + return; + } + pdfManager.sendProgressiveData(chunk); + }, + + onDone: function onDone(args) { + if (pdfManager) { + return; // already processed + } + + var pdfFile; + if (args === null) { + // TODO add some streaming manager, e.g. for unknown length files. + // The data was returned in the onProgressiveData, combining... + var pdfFileLength = 0, pos = 0; + cachedChunks.forEach(function (chunk) { + pdfFileLength += chunk.byteLength; + }); + if (source.length && pdfFileLength !== source.length) { + warn('reported HTTP length is different from actual'); + } + var pdfFileArray = new Uint8Array(pdfFileLength); + cachedChunks.forEach(function (chunk) { + pdfFileArray.set(new Uint8Array(chunk), pos); + pos += chunk.byteLength; + }); + pdfFile = pdfFileArray.buffer; + } else { + pdfFile = args.chunk; + } + + // the data is array, instantiating directly from it + try { + pdfManager = new LocalPdfManager(pdfFile, source.password); + pdfManagerCapability.resolve(); + } catch (ex) { + pdfManagerCapability.reject(ex); + } + }, + + onError: function onError(status) { + var exception; + if (status === 404) { + exception = new MissingPDFException('Missing PDF "' + + source.url + '".'); + handler.send('MissingPDF', exception); + } else { + exception = new UnexpectedResponseException( + 'Unexpected server response (' + status + + ') while retrieving PDF "' + source.url + '".', status); + handler.send('UnexpectedResponse', exception); + } + }, + + onProgress: function onProgress(evt) { + handler.send('DocProgress', { + loaded: evt.loaded, + total: evt.lengthComputable ? evt.total : source.length + }); + } + }); + + return pdfManagerCapability.promise; + } + + handler.on('test', function wphSetupTest(data) { + // check if Uint8Array can be sent to worker + if (!(data instanceof Uint8Array)) { + handler.send('test', false); + return; + } + // making sure postMessage transfers are working + var supportTransfers = data[0] === 255; + handler.postMessageTransfers = supportTransfers; + // check if the response property is supported by xhr + var xhr = new XMLHttpRequest(); + var responseExists = 'response' in xhr; + // check if the property is actually implemented + try { + var dummy = xhr.responseType; + } catch (e) { + responseExists = false; + } + if (!responseExists) { + handler.send('test', false); + return; + } + handler.send('test', { + supportTypedArray: true, + supportTransfers: supportTransfers + }); + }); + + handler.on('GetDocRequest', function wphSetupDoc(data) { + + var onSuccess = function(doc) { + handler.send('GetDoc', { pdfInfo: doc }); + }; + + var onFailure = function(e) { + if (e instanceof PasswordException) { + if (e.code === PasswordResponses.NEED_PASSWORD) { + handler.send('NeedPassword', e); + } else if (e.code === PasswordResponses.INCORRECT_PASSWORD) { + handler.send('IncorrectPassword', e); + } + } else if (e instanceof InvalidPDFException) { + handler.send('InvalidPDF', e); + } else if (e instanceof MissingPDFException) { + handler.send('MissingPDF', e); + } else if (e instanceof UnexpectedResponseException) { + handler.send('UnexpectedResponse', e); + } else { + handler.send('UnknownError', + new UnknownErrorException(e.message, e.toString())); + } + }; + + PDFJS.maxImageSize = data.maxImageSize === undefined ? + -1 : data.maxImageSize; + PDFJS.disableFontFace = data.disableFontFace; + PDFJS.disableCreateObjectURL = data.disableCreateObjectURL; + PDFJS.verbosity = data.verbosity; + PDFJS.cMapUrl = data.cMapUrl === undefined ? + null : data.cMapUrl; + PDFJS.cMapPacked = data.cMapPacked === true; + + getPdfManager(data).then(function () { + handler.send('PDFManagerReady', null); + pdfManager.onLoadedStream().then(function(stream) { + handler.send('DataLoaded', { length: stream.bytes.byteLength }); + }); + }).then(function pdfManagerReady() { + loadDocument(false).then(onSuccess, function loadFailure(ex) { + // Try again with recoveryMode == true + if (!(ex instanceof XRefParseException)) { + if (ex instanceof PasswordException) { + // after password exception prepare to receive a new password + // to repeat loading + pdfManager.passwordChanged().then(pdfManagerReady); + } + + onFailure(ex); + return; + } + + pdfManager.requestLoadedStream(); + pdfManager.onLoadedStream().then(function() { + loadDocument(true).then(onSuccess, onFailure); + }); + }, onFailure); + }, onFailure); + }); + + handler.on('GetPage', function wphSetupGetPage(data) { + return pdfManager.getPage(data.pageIndex).then(function(page) { + var rotatePromise = pdfManager.ensure(page, 'rotate'); + var refPromise = pdfManager.ensure(page, 'ref'); + var viewPromise = pdfManager.ensure(page, 'view'); + + return Promise.all([rotatePromise, refPromise, viewPromise]).then( + function(results) { + return { + rotate: results[0], + ref: results[1], + view: results[2] + }; + }); + }); + }); + + handler.on('GetPageIndex', function wphSetupGetPageIndex(data) { + var ref = new Ref(data.ref.num, data.ref.gen); + var catalog = pdfManager.pdfDocument.catalog; + return catalog.getPageIndex(ref); + }); + + handler.on('GetDestinations', + function wphSetupGetDestinations(data) { + return pdfManager.ensureCatalog('destinations'); + } + ); + + handler.on('GetDestination', + function wphSetupGetDestination(data) { + return pdfManager.ensureCatalog('getDestination', [ data.id ]); + } + ); + + handler.on('GetAttachments', + function wphSetupGetAttachments(data) { + return pdfManager.ensureCatalog('attachments'); + } + ); + + handler.on('GetJavaScript', + function wphSetupGetJavaScript(data) { + return pdfManager.ensureCatalog('javaScript'); + } + ); + + handler.on('GetOutline', + function wphSetupGetOutline(data) { + return pdfManager.ensureCatalog('documentOutline'); + } + ); + + handler.on('GetMetadata', + function wphSetupGetMetadata(data) { + return Promise.all([pdfManager.ensureDoc('documentInfo'), + pdfManager.ensureCatalog('metadata')]); + } + ); + + handler.on('GetData', function wphSetupGetData(data) { + pdfManager.requestLoadedStream(); + return pdfManager.onLoadedStream().then(function(stream) { + return stream.bytes; + }); + }); + + handler.on('GetStats', + function wphSetupGetStats(data) { + return pdfManager.pdfDocument.xref.stats; + } + ); + + handler.on('UpdatePassword', function wphSetupUpdatePassword(data) { + pdfManager.updatePassword(data); + }); + + handler.on('GetAnnotations', function wphSetupGetAnnotations(data) { + return pdfManager.getPage(data.pageIndex).then(function(page) { + return pdfManager.ensure(page, 'getAnnotationsData', []); + }); + }); + + handler.on('RenderPageRequest', function wphSetupRenderPage(data) { + pdfManager.getPage(data.pageIndex).then(function(page) { + + var pageNum = data.pageIndex + 1; + var start = Date.now(); + // Pre compile the pdf page and fetch the fonts/images. + page.getOperatorList(handler, data.intent).then(function(operatorList) { + + info('page=' + pageNum + ' - getOperatorList: time=' + + (Date.now() - start) + 'ms, len=' + operatorList.fnArray.length); + + }, function(e) { + + var minimumStackMessage = + 'worker.js: while trying to getPage() and getOperatorList()'; + + var wrappedException; + + // Turn the error into an obj that can be serialized + if (typeof e === 'string') { + wrappedException = { + message: e, + stack: minimumStackMessage + }; + } else if (typeof e === 'object') { + wrappedException = { + message: e.message || e.toString(), + stack: e.stack || minimumStackMessage + }; + } else { + wrappedException = { + message: 'Unknown exception type: ' + (typeof e), + stack: minimumStackMessage + }; + } + + handler.send('PageError', { + pageNum: pageNum, + error: wrappedException, + intent: data.intent + }); + }); + }); + }, this); + + handler.on('GetTextContent', function wphExtractText(data) { + return pdfManager.getPage(data.pageIndex).then(function(page) { + var pageNum = data.pageIndex + 1; + var start = Date.now(); + return page.extractTextContent().then(function(textContent) { + info('text indexing: page=' + pageNum + ' - time=' + + (Date.now() - start) + 'ms'); + return textContent; + }); + }); + }); + + handler.on('Cleanup', function wphCleanup(data) { + return pdfManager.cleanup(); + }); + + handler.on('Terminate', function wphTerminate(data) { + pdfManager.terminate(); + }); + } +}; + +var consoleTimer = {}; + +var workerConsole = { + log: function log() { + var args = Array.prototype.slice.call(arguments); + globalScope.postMessage({ + action: 'console_log', + data: args + }); + }, + + error: function error() { + var args = Array.prototype.slice.call(arguments); + globalScope.postMessage({ + action: 'console_error', + data: args + }); + throw 'pdf.js execution error'; + }, + + time: function time(name) { + consoleTimer[name] = Date.now(); + }, + + timeEnd: function timeEnd(name) { + var time = consoleTimer[name]; + if (!time) { + error('Unknown timer name ' + name); + } + this.log('Timer:', name, Date.now() - time); + } +}; + + +// Worker thread? +if (typeof window === 'undefined') { + if (!('console' in globalScope)) { + globalScope.console = workerConsole; + } + + // Listen for unsupported features so we can pass them on to the main thread. + PDFJS.UnsupportedManager.listen(function (msg) { + globalScope.postMessage({ + action: '_unsupported_feature', + data: msg + }); + }); + + var handler = new MessageHandler('worker_processor', this); + WorkerMessageHandler.setup(handler); +} + + +/* This class implements the QM Coder decoding as defined in + * JPEG 2000 Part I Final Committee Draft Version 1.0 + * Annex C.3 Arithmetic decoding procedure + * available at http://www.jpeg.org/public/fcd15444-1.pdf + * + * The arithmetic decoder is used in conjunction with context models to decode + * JPEG2000 and JBIG2 streams. + */ +var ArithmeticDecoder = (function ArithmeticDecoderClosure() { + // Table C-2 + var QeTable = [ + {qe: 0x5601, nmps: 1, nlps: 1, switchFlag: 1}, + {qe: 0x3401, nmps: 2, nlps: 6, switchFlag: 0}, + {qe: 0x1801, nmps: 3, nlps: 9, switchFlag: 0}, + {qe: 0x0AC1, nmps: 4, nlps: 12, switchFlag: 0}, + {qe: 0x0521, nmps: 5, nlps: 29, switchFlag: 0}, + {qe: 0x0221, nmps: 38, nlps: 33, switchFlag: 0}, + {qe: 0x5601, nmps: 7, nlps: 6, switchFlag: 1}, + {qe: 0x5401, nmps: 8, nlps: 14, switchFlag: 0}, + {qe: 0x4801, nmps: 9, nlps: 14, switchFlag: 0}, + {qe: 0x3801, nmps: 10, nlps: 14, switchFlag: 0}, + {qe: 0x3001, nmps: 11, nlps: 17, switchFlag: 0}, + {qe: 0x2401, nmps: 12, nlps: 18, switchFlag: 0}, + {qe: 0x1C01, nmps: 13, nlps: 20, switchFlag: 0}, + {qe: 0x1601, nmps: 29, nlps: 21, switchFlag: 0}, + {qe: 0x5601, nmps: 15, nlps: 14, switchFlag: 1}, + {qe: 0x5401, nmps: 16, nlps: 14, switchFlag: 0}, + {qe: 0x5101, nmps: 17, nlps: 15, switchFlag: 0}, + {qe: 0x4801, nmps: 18, nlps: 16, switchFlag: 0}, + {qe: 0x3801, nmps: 19, nlps: 17, switchFlag: 0}, + {qe: 0x3401, nmps: 20, nlps: 18, switchFlag: 0}, + {qe: 0x3001, nmps: 21, nlps: 19, switchFlag: 0}, + {qe: 0x2801, nmps: 22, nlps: 19, switchFlag: 0}, + {qe: 0x2401, nmps: 23, nlps: 20, switchFlag: 0}, + {qe: 0x2201, nmps: 24, nlps: 21, switchFlag: 0}, + {qe: 0x1C01, nmps: 25, nlps: 22, switchFlag: 0}, + {qe: 0x1801, nmps: 26, nlps: 23, switchFlag: 0}, + {qe: 0x1601, nmps: 27, nlps: 24, switchFlag: 0}, + {qe: 0x1401, nmps: 28, nlps: 25, switchFlag: 0}, + {qe: 0x1201, nmps: 29, nlps: 26, switchFlag: 0}, + {qe: 0x1101, nmps: 30, nlps: 27, switchFlag: 0}, + {qe: 0x0AC1, nmps: 31, nlps: 28, switchFlag: 0}, + {qe: 0x09C1, nmps: 32, nlps: 29, switchFlag: 0}, + {qe: 0x08A1, nmps: 33, nlps: 30, switchFlag: 0}, + {qe: 0x0521, nmps: 34, nlps: 31, switchFlag: 0}, + {qe: 0x0441, nmps: 35, nlps: 32, switchFlag: 0}, + {qe: 0x02A1, nmps: 36, nlps: 33, switchFlag: 0}, + {qe: 0x0221, nmps: 37, nlps: 34, switchFlag: 0}, + {qe: 0x0141, nmps: 38, nlps: 35, switchFlag: 0}, + {qe: 0x0111, nmps: 39, nlps: 36, switchFlag: 0}, + {qe: 0x0085, nmps: 40, nlps: 37, switchFlag: 0}, + {qe: 0x0049, nmps: 41, nlps: 38, switchFlag: 0}, + {qe: 0x0025, nmps: 42, nlps: 39, switchFlag: 0}, + {qe: 0x0015, nmps: 43, nlps: 40, switchFlag: 0}, + {qe: 0x0009, nmps: 44, nlps: 41, switchFlag: 0}, + {qe: 0x0005, nmps: 45, nlps: 42, switchFlag: 0}, + {qe: 0x0001, nmps: 45, nlps: 43, switchFlag: 0}, + {qe: 0x5601, nmps: 46, nlps: 46, switchFlag: 0} + ]; + + // C.3.5 Initialisation of the decoder (INITDEC) + function ArithmeticDecoder(data, start, end) { + this.data = data; + this.bp = start; + this.dataEnd = end; + + this.chigh = data[start]; + this.clow = 0; + + this.byteIn(); + + this.chigh = ((this.chigh << 7) & 0xFFFF) | ((this.clow >> 9) & 0x7F); + this.clow = (this.clow << 7) & 0xFFFF; + this.ct -= 7; + this.a = 0x8000; + } + + ArithmeticDecoder.prototype = { + // C.3.4 Compressed data input (BYTEIN) + byteIn: function ArithmeticDecoder_byteIn() { + var data = this.data; + var bp = this.bp; + if (data[bp] === 0xFF) { + var b1 = data[bp + 1]; + if (b1 > 0x8F) { + this.clow += 0xFF00; + this.ct = 8; + } else { + bp++; + this.clow += (data[bp] << 9); + this.ct = 7; + this.bp = bp; + } + } else { + bp++; + this.clow += bp < this.dataEnd ? (data[bp] << 8) : 0xFF00; + this.ct = 8; + this.bp = bp; + } + if (this.clow > 0xFFFF) { + this.chigh += (this.clow >> 16); + this.clow &= 0xFFFF; + } + }, + // C.3.2 Decoding a decision (DECODE) + readBit: function ArithmeticDecoder_readBit(contexts, pos) { + // contexts are packed into 1 byte: + // highest 7 bits carry cx.index, lowest bit carries cx.mps + var cx_index = contexts[pos] >> 1, cx_mps = contexts[pos] & 1; + var qeTableIcx = QeTable[cx_index]; + var qeIcx = qeTableIcx.qe; + var d; + var a = this.a - qeIcx; + + if (this.chigh < qeIcx) { + // exchangeLps + if (a < qeIcx) { + a = qeIcx; + d = cx_mps; + cx_index = qeTableIcx.nmps; + } else { + a = qeIcx; + d = 1 ^ cx_mps; + if (qeTableIcx.switchFlag === 1) { + cx_mps = d; + } + cx_index = qeTableIcx.nlps; + } + } else { + this.chigh -= qeIcx; + if ((a & 0x8000) !== 0) { + this.a = a; + return cx_mps; + } + // exchangeMps + if (a < qeIcx) { + d = 1 ^ cx_mps; + if (qeTableIcx.switchFlag === 1) { + cx_mps = d; + } + cx_index = qeTableIcx.nlps; + } else { + d = cx_mps; + cx_index = qeTableIcx.nmps; + } + } + // C.3.3 renormD; + do { + if (this.ct === 0) { + this.byteIn(); + } + + a <<= 1; + this.chigh = ((this.chigh << 1) & 0xFFFF) | ((this.clow >> 15) & 1); + this.clow = (this.clow << 1) & 0xFFFF; + this.ct--; + } while ((a & 0x8000) === 0); + this.a = a; + + contexts[pos] = cx_index << 1 | cx_mps; + return d; + } + }; + + return ArithmeticDecoder; +})(); + + +var JpegImage = (function jpegImage() { + var dctZigZag = new Uint8Array([ + 0, + 1, 8, + 16, 9, 2, + 3, 10, 17, 24, + 32, 25, 18, 11, 4, + 5, 12, 19, 26, 33, 40, + 48, 41, 34, 27, 20, 13, 6, + 7, 14, 21, 28, 35, 42, 49, 56, + 57, 50, 43, 36, 29, 22, 15, + 23, 30, 37, 44, 51, 58, + 59, 52, 45, 38, 31, + 39, 46, 53, 60, + 61, 54, 47, + 55, 62, + 63 + ]); + + var dctCos1 = 4017; // cos(pi/16) + var dctSin1 = 799; // sin(pi/16) + var dctCos3 = 3406; // cos(3*pi/16) + var dctSin3 = 2276; // sin(3*pi/16) + var dctCos6 = 1567; // cos(6*pi/16) + var dctSin6 = 3784; // sin(6*pi/16) + var dctSqrt2 = 5793; // sqrt(2) + var dctSqrt1d2 = 2896; // sqrt(2) / 2 + + function constructor() { + } + + function buildHuffmanTable(codeLengths, values) { + var k = 0, code = [], i, j, length = 16; + while (length > 0 && !codeLengths[length - 1]) { + length--; + } + code.push({children: [], index: 0}); + var p = code[0], q; + for (i = 0; i < length; i++) { + for (j = 0; j < codeLengths[i]; j++) { + p = code.pop(); + p.children[p.index] = values[k]; + while (p.index > 0) { + p = code.pop(); + } + p.index++; + code.push(p); + while (code.length <= i) { + code.push(q = {children: [], index: 0}); + p.children[p.index] = q.children; + p = q; + } + k++; + } + if (i + 1 < length) { + // p here points to last code + code.push(q = {children: [], index: 0}); + p.children[p.index] = q.children; + p = q; + } + } + return code[0].children; + } + + function getBlockBufferOffset(component, row, col) { + return 64 * ((component.blocksPerLine + 1) * row + col); + } + + function decodeScan(data, offset, frame, components, resetInterval, + spectralStart, spectralEnd, successivePrev, successive) { + var precision = frame.precision; + var samplesPerLine = frame.samplesPerLine; + var scanLines = frame.scanLines; + var mcusPerLine = frame.mcusPerLine; + var progressive = frame.progressive; + var maxH = frame.maxH, maxV = frame.maxV; + + var startOffset = offset, bitsData = 0, bitsCount = 0; + + function readBit() { + if (bitsCount > 0) { + bitsCount--; + return (bitsData >> bitsCount) & 1; + } + bitsData = data[offset++]; + if (bitsData === 0xFF) { + var nextByte = data[offset++]; + if (nextByte) { + throw 'unexpected marker: ' + + ((bitsData << 8) | nextByte).toString(16); + } + // unstuff 0 + } + bitsCount = 7; + return bitsData >>> 7; + } + + function decodeHuffman(tree) { + var node = tree; + while (true) { + node = node[readBit()]; + if (typeof node === 'number') { + return node; + } + if (typeof node !== 'object') { + throw 'invalid huffman sequence'; + } + } + } + + function receive(length) { + var n = 0; + while (length > 0) { + n = (n << 1) | readBit(); + length--; + } + return n; + } + + function receiveAndExtend(length) { + if (length === 1) { + return readBit() === 1 ? 1 : -1; + } + var n = receive(length); + if (n >= 1 << (length - 1)) { + return n; + } + return n + (-1 << length) + 1; + } + + function decodeBaseline(component, offset) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : receiveAndExtend(t); + component.blockData[offset] = (component.pred += diff); + var k = 1; + while (k < 64) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + break; + } + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + component.blockData[offset + z] = receiveAndExtend(s); + k++; + } + } + + function decodeDCFirst(component, offset) { + var t = decodeHuffman(component.huffmanTableDC); + var diff = t === 0 ? 0 : (receiveAndExtend(t) << successive); + component.blockData[offset] = (component.pred += diff); + } + + function decodeDCSuccessive(component, offset) { + component.blockData[offset] |= readBit() << successive; + } + + var eobrun = 0; + function decodeACFirst(component, offset) { + if (eobrun > 0) { + eobrun--; + return; + } + var k = spectralStart, e = spectralEnd; + while (k <= e) { + var rs = decodeHuffman(component.huffmanTableAC); + var s = rs & 15, r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r) - 1; + break; + } + k += 16; + continue; + } + k += r; + var z = dctZigZag[k]; + component.blockData[offset + z] = + receiveAndExtend(s) * (1 << successive); + k++; + } + } + + var successiveACState = 0, successiveACNextValue; + function decodeACSuccessive(component, offset) { + var k = spectralStart; + var e = spectralEnd; + var r = 0; + var s; + var rs; + while (k <= e) { + var z = dctZigZag[k]; + switch (successiveACState) { + case 0: // initial state + rs = decodeHuffman(component.huffmanTableAC); + s = rs & 15; + r = rs >> 4; + if (s === 0) { + if (r < 15) { + eobrun = receive(r) + (1 << r); + successiveACState = 4; + } else { + r = 16; + successiveACState = 1; + } + } else { + if (s !== 1) { + throw 'invalid ACn encoding'; + } + successiveACNextValue = receiveAndExtend(s); + successiveACState = r ? 2 : 3; + } + continue; + case 1: // skipping r zero items + case 2: + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } else { + r--; + if (r === 0) { + successiveACState = successiveACState === 2 ? 3 : 0; + } + } + break; + case 3: // set value for a zero item + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } else { + component.blockData[offset + z] = + successiveACNextValue << successive; + successiveACState = 0; + } + break; + case 4: // eob + if (component.blockData[offset + z]) { + component.blockData[offset + z] += (readBit() << successive); + } + break; + } + k++; + } + if (successiveACState === 4) { + eobrun--; + if (eobrun === 0) { + successiveACState = 0; + } + } + } + + function decodeMcu(component, decode, mcu, row, col) { + var mcuRow = (mcu / mcusPerLine) | 0; + var mcuCol = mcu % mcusPerLine; + var blockRow = mcuRow * component.v + row; + var blockCol = mcuCol * component.h + col; + var offset = getBlockBufferOffset(component, blockRow, blockCol); + decode(component, offset); + } + + function decodeBlock(component, decode, mcu) { + var blockRow = (mcu / component.blocksPerLine) | 0; + var blockCol = mcu % component.blocksPerLine; + var offset = getBlockBufferOffset(component, blockRow, blockCol); + decode(component, offset); + } + + var componentsLength = components.length; + var component, i, j, k, n; + var decodeFn; + if (progressive) { + if (spectralStart === 0) { + decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive; + } else { + decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive; + } + } else { + decodeFn = decodeBaseline; + } + + var mcu = 0, marker; + var mcuExpected; + if (componentsLength === 1) { + mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn; + } else { + mcuExpected = mcusPerLine * frame.mcusPerColumn; + } + if (!resetInterval) { + resetInterval = mcuExpected; + } + + var h, v; + while (mcu < mcuExpected) { + // reset interval stuff + for (i = 0; i < componentsLength; i++) { + components[i].pred = 0; + } + eobrun = 0; + + if (componentsLength === 1) { + component = components[0]; + for (n = 0; n < resetInterval; n++) { + decodeBlock(component, decodeFn, mcu); + mcu++; + } + } else { + for (n = 0; n < resetInterval; n++) { + for (i = 0; i < componentsLength; i++) { + component = components[i]; + h = component.h; + v = component.v; + for (j = 0; j < v; j++) { + for (k = 0; k < h; k++) { + decodeMcu(component, decodeFn, mcu, j, k); + } + } + } + mcu++; + } + } + + // find marker + bitsCount = 0; + marker = (data[offset] << 8) | data[offset + 1]; + if (marker <= 0xFF00) { + throw 'marker was not found'; + } + + if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx + offset += 2; + } else { + break; + } + } + + return offset - startOffset; + } + + // A port of poppler's IDCT method which in turn is taken from: + // Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz, + // 'Practical Fast 1-D DCT Algorithms with 11 Multiplications', + // IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989, + // 988-991. + function quantizeAndInverse(component, blockBufferOffset, p) { + var qt = component.quantizationTable, blockData = component.blockData; + var v0, v1, v2, v3, v4, v5, v6, v7; + var p0, p1, p2, p3, p4, p5, p6, p7; + var t; + + // inverse DCT on rows + for (var row = 0; row < 64; row += 8) { + // gather block data + p0 = blockData[blockBufferOffset + row]; + p1 = blockData[blockBufferOffset + row + 1]; + p2 = blockData[blockBufferOffset + row + 2]; + p3 = blockData[blockBufferOffset + row + 3]; + p4 = blockData[blockBufferOffset + row + 4]; + p5 = blockData[blockBufferOffset + row + 5]; + p6 = blockData[blockBufferOffset + row + 6]; + p7 = blockData[blockBufferOffset + row + 7]; + + // dequant p0 + p0 *= qt[row]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) { + t = (dctSqrt2 * p0 + 512) >> 10; + p[row] = t; + p[row + 1] = t; + p[row + 2] = t; + p[row + 3] = t; + p[row + 4] = t; + p[row + 5] = t; + p[row + 6] = t; + p[row + 7] = t; + continue; + } + // dequant p1 ... p7 + p1 *= qt[row + 1]; + p2 *= qt[row + 2]; + p3 *= qt[row + 3]; + p4 *= qt[row + 4]; + p5 *= qt[row + 5]; + p6 *= qt[row + 6]; + p7 *= qt[row + 7]; + + // stage 4 + v0 = (dctSqrt2 * p0 + 128) >> 8; + v1 = (dctSqrt2 * p4 + 128) >> 8; + v2 = p2; + v3 = p6; + v4 = (dctSqrt1d2 * (p1 - p7) + 128) >> 8; + v7 = (dctSqrt1d2 * (p1 + p7) + 128) >> 8; + v5 = p3 << 4; + v6 = p5 << 4; + + // stage 3 + v0 = (v0 + v1 + 1) >> 1; + v1 = v0 - v1; + t = (v2 * dctSin6 + v3 * dctCos6 + 128) >> 8; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 128) >> 8; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p[row] = v0 + v7; + p[row + 7] = v0 - v7; + p[row + 1] = v1 + v6; + p[row + 6] = v1 - v6; + p[row + 2] = v2 + v5; + p[row + 5] = v2 - v5; + p[row + 3] = v3 + v4; + p[row + 4] = v3 - v4; + } + + // inverse DCT on columns + for (var col = 0; col < 8; ++col) { + p0 = p[col]; + p1 = p[col + 8]; + p2 = p[col + 16]; + p3 = p[col + 24]; + p4 = p[col + 32]; + p5 = p[col + 40]; + p6 = p[col + 48]; + p7 = p[col + 56]; + + // check for all-zero AC coefficients + if ((p1 | p2 | p3 | p4 | p5 | p6 | p7) === 0) { + t = (dctSqrt2 * p0 + 8192) >> 14; + // convert to 8 bit + t = (t < -2040) ? 0 : (t >= 2024) ? 255 : (t + 2056) >> 4; + blockData[blockBufferOffset + col] = t; + blockData[blockBufferOffset + col + 8] = t; + blockData[blockBufferOffset + col + 16] = t; + blockData[blockBufferOffset + col + 24] = t; + blockData[blockBufferOffset + col + 32] = t; + blockData[blockBufferOffset + col + 40] = t; + blockData[blockBufferOffset + col + 48] = t; + blockData[blockBufferOffset + col + 56] = t; + continue; + } + + // stage 4 + v0 = (dctSqrt2 * p0 + 2048) >> 12; + v1 = (dctSqrt2 * p4 + 2048) >> 12; + v2 = p2; + v3 = p6; + v4 = (dctSqrt1d2 * (p1 - p7) + 2048) >> 12; + v7 = (dctSqrt1d2 * (p1 + p7) + 2048) >> 12; + v5 = p3; + v6 = p5; + + // stage 3 + // Shift v0 by 128.5 << 5 here, so we don't need to shift p0...p7 when + // converting to UInt8 range later. + v0 = ((v0 + v1 + 1) >> 1) + 4112; + v1 = v0 - v1; + t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12; + v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12; + v3 = t; + v4 = (v4 + v6 + 1) >> 1; + v6 = v4 - v6; + v7 = (v7 + v5 + 1) >> 1; + v5 = v7 - v5; + + // stage 2 + v0 = (v0 + v3 + 1) >> 1; + v3 = v0 - v3; + v1 = (v1 + v2 + 1) >> 1; + v2 = v1 - v2; + t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12; + v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12; + v7 = t; + t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12; + v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12; + v6 = t; + + // stage 1 + p0 = v0 + v7; + p7 = v0 - v7; + p1 = v1 + v6; + p6 = v1 - v6; + p2 = v2 + v5; + p5 = v2 - v5; + p3 = v3 + v4; + p4 = v3 - v4; + + // convert to 8-bit integers + p0 = (p0 < 16) ? 0 : (p0 >= 4080) ? 255 : p0 >> 4; + p1 = (p1 < 16) ? 0 : (p1 >= 4080) ? 255 : p1 >> 4; + p2 = (p2 < 16) ? 0 : (p2 >= 4080) ? 255 : p2 >> 4; + p3 = (p3 < 16) ? 0 : (p3 >= 4080) ? 255 : p3 >> 4; + p4 = (p4 < 16) ? 0 : (p4 >= 4080) ? 255 : p4 >> 4; + p5 = (p5 < 16) ? 0 : (p5 >= 4080) ? 255 : p5 >> 4; + p6 = (p6 < 16) ? 0 : (p6 >= 4080) ? 255 : p6 >> 4; + p7 = (p7 < 16) ? 0 : (p7 >= 4080) ? 255 : p7 >> 4; + + // store block data + blockData[blockBufferOffset + col] = p0; + blockData[blockBufferOffset + col + 8] = p1; + blockData[blockBufferOffset + col + 16] = p2; + blockData[blockBufferOffset + col + 24] = p3; + blockData[blockBufferOffset + col + 32] = p4; + blockData[blockBufferOffset + col + 40] = p5; + blockData[blockBufferOffset + col + 48] = p6; + blockData[blockBufferOffset + col + 56] = p7; + } + } + + function buildComponentData(frame, component) { + var blocksPerLine = component.blocksPerLine; + var blocksPerColumn = component.blocksPerColumn; + var computationBuffer = new Int16Array(64); + + for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) { + for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) { + var offset = getBlockBufferOffset(component, blockRow, blockCol); + quantizeAndInverse(component, offset, computationBuffer); + } + } + return component.blockData; + } + + function clamp0to255(a) { + return a <= 0 ? 0 : a >= 255 ? 255 : a; + } + + constructor.prototype = { + parse: function parse(data) { + + function readUint16() { + var value = (data[offset] << 8) | data[offset + 1]; + offset += 2; + return value; + } + + function readDataBlock() { + var length = readUint16(); + var array = data.subarray(offset, offset + length - 2); + offset += array.length; + return array; + } + + function prepareComponents(frame) { + var mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / frame.maxH); + var mcusPerColumn = Math.ceil(frame.scanLines / 8 / frame.maxV); + for (var i = 0; i < frame.components.length; i++) { + component = frame.components[i]; + var blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * + component.h / frame.maxH); + var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * + component.v / frame.maxV); + var blocksPerLineForMcu = mcusPerLine * component.h; + var blocksPerColumnForMcu = mcusPerColumn * component.v; + + var blocksBufferSize = 64 * blocksPerColumnForMcu * + (blocksPerLineForMcu + 1); + component.blockData = new Int16Array(blocksBufferSize); + component.blocksPerLine = blocksPerLine; + component.blocksPerColumn = blocksPerColumn; + } + frame.mcusPerLine = mcusPerLine; + frame.mcusPerColumn = mcusPerColumn; + } + + var offset = 0, length = data.length; + var jfif = null; + var adobe = null; + var pixels = null; + var frame, resetInterval; + var quantizationTables = []; + var huffmanTablesAC = [], huffmanTablesDC = []; + var fileMarker = readUint16(); + if (fileMarker !== 0xFFD8) { // SOI (Start of Image) + throw 'SOI not found'; + } + + fileMarker = readUint16(); + while (fileMarker !== 0xFFD9) { // EOI (End of image) + var i, j, l; + switch(fileMarker) { + case 0xFFE0: // APP0 (Application Specific) + case 0xFFE1: // APP1 + case 0xFFE2: // APP2 + case 0xFFE3: // APP3 + case 0xFFE4: // APP4 + case 0xFFE5: // APP5 + case 0xFFE6: // APP6 + case 0xFFE7: // APP7 + case 0xFFE8: // APP8 + case 0xFFE9: // APP9 + case 0xFFEA: // APP10 + case 0xFFEB: // APP11 + case 0xFFEC: // APP12 + case 0xFFED: // APP13 + case 0xFFEE: // APP14 + case 0xFFEF: // APP15 + case 0xFFFE: // COM (Comment) + var appData = readDataBlock(); + + if (fileMarker === 0xFFE0) { + if (appData[0] === 0x4A && appData[1] === 0x46 && + appData[2] === 0x49 && appData[3] === 0x46 && + appData[4] === 0) { // 'JFIF\x00' + jfif = { + version: { major: appData[5], minor: appData[6] }, + densityUnits: appData[7], + xDensity: (appData[8] << 8) | appData[9], + yDensity: (appData[10] << 8) | appData[11], + thumbWidth: appData[12], + thumbHeight: appData[13], + thumbData: appData.subarray(14, 14 + + 3 * appData[12] * appData[13]) + }; + } + } + // TODO APP1 - Exif + if (fileMarker === 0xFFEE) { + if (appData[0] === 0x41 && appData[1] === 0x64 && + appData[2] === 0x6F && appData[3] === 0x62 && + appData[4] === 0x65) { // 'Adobe' + adobe = { + version: (appData[5] << 8) | appData[6], + flags0: (appData[7] << 8) | appData[8], + flags1: (appData[9] << 8) | appData[10], + transformCode: appData[11] + }; + } + } + break; + + case 0xFFDB: // DQT (Define Quantization Tables) + var quantizationTablesLength = readUint16(); + var quantizationTablesEnd = quantizationTablesLength + offset - 2; + var z; + while (offset < quantizationTablesEnd) { + var quantizationTableSpec = data[offset++]; + var tableData = new Uint16Array(64); + if ((quantizationTableSpec >> 4) === 0) { // 8 bit values + for (j = 0; j < 64; j++) { + z = dctZigZag[j]; + tableData[z] = data[offset++]; + } + } else if ((quantizationTableSpec >> 4) === 1) { //16 bit + for (j = 0; j < 64; j++) { + z = dctZigZag[j]; + tableData[z] = readUint16(); + } + } else { + throw 'DQT: invalid table spec'; + } + quantizationTables[quantizationTableSpec & 15] = tableData; + } + break; + + case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT) + case 0xFFC1: // SOF1 (Start of Frame, Extended DCT) + case 0xFFC2: // SOF2 (Start of Frame, Progressive DCT) + if (frame) { + throw 'Only single frame JPEGs supported'; + } + readUint16(); // skip data length + frame = {}; + frame.extended = (fileMarker === 0xFFC1); + frame.progressive = (fileMarker === 0xFFC2); + frame.precision = data[offset++]; + frame.scanLines = readUint16(); + frame.samplesPerLine = readUint16(); + frame.components = []; + frame.componentIds = {}; + var componentsCount = data[offset++], componentId; + var maxH = 0, maxV = 0; + for (i = 0; i < componentsCount; i++) { + componentId = data[offset]; + var h = data[offset + 1] >> 4; + var v = data[offset + 1] & 15; + if (maxH < h) { + maxH = h; + } + if (maxV < v) { + maxV = v; + } + var qId = data[offset + 2]; + l = frame.components.push({ + h: h, + v: v, + quantizationTable: quantizationTables[qId] + }); + frame.componentIds[componentId] = l - 1; + offset += 3; + } + frame.maxH = maxH; + frame.maxV = maxV; + prepareComponents(frame); + break; + + case 0xFFC4: // DHT (Define Huffman Tables) + var huffmanLength = readUint16(); + for (i = 2; i < huffmanLength;) { + var huffmanTableSpec = data[offset++]; + var codeLengths = new Uint8Array(16); + var codeLengthSum = 0; + for (j = 0; j < 16; j++, offset++) { + codeLengthSum += (codeLengths[j] = data[offset]); + } + var huffmanValues = new Uint8Array(codeLengthSum); + for (j = 0; j < codeLengthSum; j++, offset++) { + huffmanValues[j] = data[offset]; + } + i += 17 + codeLengthSum; + + ((huffmanTableSpec >> 4) === 0 ? + huffmanTablesDC : huffmanTablesAC)[huffmanTableSpec & 15] = + buildHuffmanTable(codeLengths, huffmanValues); + } + break; + + case 0xFFDD: // DRI (Define Restart Interval) + readUint16(); // skip data length + resetInterval = readUint16(); + break; + + case 0xFFDA: // SOS (Start of Scan) + var scanLength = readUint16(); + var selectorsCount = data[offset++]; + var components = [], component; + for (i = 0; i < selectorsCount; i++) { + var componentIndex = frame.componentIds[data[offset++]]; + component = frame.components[componentIndex]; + var tableSpec = data[offset++]; + component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4]; + component.huffmanTableAC = huffmanTablesAC[tableSpec & 15]; + components.push(component); + } + var spectralStart = data[offset++]; + var spectralEnd = data[offset++]; + var successiveApproximation = data[offset++]; + var processed = decodeScan(data, offset, + frame, components, resetInterval, + spectralStart, spectralEnd, + successiveApproximation >> 4, successiveApproximation & 15); + offset += processed; + break; + + case 0xFFFF: // Fill bytes + if (data[offset] !== 0xFF) { // Avoid skipping a valid marker. + offset--; + } + break; + + default: + if (data[offset - 3] === 0xFF && + data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) { + // could be incorrect encoding -- last 0xFF byte of the previous + // block was eaten by the encoder + offset -= 3; + break; + } + throw 'unknown JPEG marker ' + fileMarker.toString(16); + } + fileMarker = readUint16(); + } + + this.width = frame.samplesPerLine; + this.height = frame.scanLines; + this.jfif = jfif; + this.adobe = adobe; + this.components = []; + for (i = 0; i < frame.components.length; i++) { + component = frame.components[i]; + this.components.push({ + output: buildComponentData(frame, component), + scaleX: component.h / frame.maxH, + scaleY: component.v / frame.maxV, + blocksPerLine: component.blocksPerLine, + blocksPerColumn: component.blocksPerColumn + }); + } + this.numComponents = this.components.length; + }, + + _getLinearizedBlockData: function getLinearizedBlockData(width, height) { + var scaleX = this.width / width, scaleY = this.height / height; + + var component, componentScaleX, componentScaleY, blocksPerScanline; + var x, y, i, j, k; + var index; + var offset = 0; + var output; + var numComponents = this.components.length; + var dataLength = width * height * numComponents; + var data = new Uint8Array(dataLength); + var xScaleBlockOffset = new Uint32Array(width); + var mask3LSB = 0xfffffff8; // used to clear the 3 LSBs + + for (i = 0; i < numComponents; i++) { + component = this.components[i]; + componentScaleX = component.scaleX * scaleX; + componentScaleY = component.scaleY * scaleY; + offset = i; + output = component.output; + blocksPerScanline = (component.blocksPerLine + 1) << 3; + // precalculate the xScaleBlockOffset + for (x = 0; x < width; x++) { + j = 0 | (x * componentScaleX); + xScaleBlockOffset[x] = ((j & mask3LSB) << 3) | (j & 7); + } + // linearize the blocks of the component + for (y = 0; y < height; y++) { + j = 0 | (y * componentScaleY); + index = blocksPerScanline * (j & mask3LSB) | ((j & 7) << 3); + for (x = 0; x < width; x++) { + data[offset] = output[index + xScaleBlockOffset[x]]; + offset += numComponents; + } + } + } + + // decodeTransform contains pairs of multiplier (-256..256) and additive + var transform = this.decodeTransform; + if (transform) { + for (i = 0; i < dataLength;) { + for (j = 0, k = 0; j < numComponents; j++, i++, k += 2) { + data[i] = ((data[i] * transform[k]) >> 8) + transform[k + 1]; + } + } + } + return data; + }, + + _isColorConversionNeeded: function isColorConversionNeeded() { + if (this.adobe && this.adobe.transformCode) { + // The adobe transform marker overrides any previous setting + return true; + } else if (this.numComponents === 3) { + return true; + } else { + return false; + } + }, + + _convertYccToRgb: function convertYccToRgb(data) { + var Y, Cb, Cr; + for (var i = 0, length = data.length; i < length; i += 3) { + Y = data[i ]; + Cb = data[i + 1]; + Cr = data[i + 2]; + data[i ] = clamp0to255(Y - 179.456 + 1.402 * Cr); + data[i + 1] = clamp0to255(Y + 135.459 - 0.344 * Cb - 0.714 * Cr); + data[i + 2] = clamp0to255(Y - 226.816 + 1.772 * Cb); + } + return data; + }, + + _convertYcckToRgb: function convertYcckToRgb(data) { + var Y, Cb, Cr, k; + var offset = 0; + for (var i = 0, length = data.length; i < length; i += 4) { + Y = data[i]; + Cb = data[i + 1]; + Cr = data[i + 2]; + k = data[i + 3]; + + var r = -122.67195406894 + + Cb * (-6.60635669420364e-5 * Cb + 0.000437130475926232 * Cr - + 5.4080610064599e-5 * Y + 0.00048449797120281 * k - + 0.154362151871126) + + Cr * (-0.000957964378445773 * Cr + 0.000817076911346625 * Y - + 0.00477271405408747 * k + 1.53380253221734) + + Y * (0.000961250184130688 * Y - 0.00266257332283933 * k + + 0.48357088451265) + + k * (-0.000336197177618394 * k + 0.484791561490776); + + var g = 107.268039397724 + + Cb * (2.19927104525741e-5 * Cb - 0.000640992018297945 * Cr + + 0.000659397001245577 * Y + 0.000426105652938837 * k - + 0.176491792462875) + + Cr * (-0.000778269941513683 * Cr + 0.00130872261408275 * Y + + 0.000770482631801132 * k - 0.151051492775562) + + Y * (0.00126935368114843 * Y - 0.00265090189010898 * k + + 0.25802910206845) + + k * (-0.000318913117588328 * k - 0.213742400323665); + + var b = -20.810012546947 + + Cb * (-0.000570115196973677 * Cb - 2.63409051004589e-5 * Cr + + 0.0020741088115012 * Y - 0.00288260236853442 * k + + 0.814272968359295) + + Cr * (-1.53496057440975e-5 * Cr - 0.000132689043961446 * Y + + 0.000560833691242812 * k - 0.195152027534049) + + Y * (0.00174418132927582 * Y - 0.00255243321439347 * k + + 0.116935020465145) + + k * (-0.000343531996510555 * k + 0.24165260232407); + + data[offset++] = clamp0to255(r); + data[offset++] = clamp0to255(g); + data[offset++] = clamp0to255(b); + } + return data; + }, + + _convertYcckToCmyk: function convertYcckToCmyk(data) { + var Y, Cb, Cr; + for (var i = 0, length = data.length; i < length; i += 4) { + Y = data[i]; + Cb = data[i + 1]; + Cr = data[i + 2]; + data[i ] = clamp0to255(434.456 - Y - 1.402 * Cr); + data[i + 1] = clamp0to255(119.541 - Y + 0.344 * Cb + 0.714 * Cr); + data[i + 2] = clamp0to255(481.816 - Y - 1.772 * Cb); + // K in data[i + 3] is unchanged + } + return data; + }, + + _convertCmykToRgb: function convertCmykToRgb(data) { + var c, m, y, k; + var offset = 0; + var min = -255 * 255 * 255; + var scale = 1 / 255 / 255; + for (var i = 0, length = data.length; i < length; i += 4) { + c = data[i]; + m = data[i + 1]; + y = data[i + 2]; + k = data[i + 3]; + + var r = + c * (-4.387332384609988 * c + 54.48615194189176 * m + + 18.82290502165302 * y + 212.25662451639585 * k - + 72734.4411664936) + + m * (1.7149763477362134 * m - 5.6096736904047315 * y - + 17.873870861415444 * k - 1401.7366389350734) + + y * (-2.5217340131683033 * y - 21.248923337353073 * k + + 4465.541406466231) - + k * (21.86122147463605 * k + 48317.86113160301); + var g = + c * (8.841041422036149 * c + 60.118027045597366 * m + + 6.871425592049007 * y + 31.159100130055922 * k - + 20220.756542821975) + + m * (-15.310361306967817 * m + 17.575251261109482 * y + + 131.35250912493976 * k - 48691.05921601825) + + y * (4.444339102852739 * y + 9.8632861493405 * k - + 6341.191035517494) - + k * (20.737325471181034 * k + 47890.15695978492); + var b = + c * (0.8842522430003296 * c + 8.078677503112928 * m + + 30.89978309703729 * y - 0.23883238689178934 * k - + 3616.812083916688) + + m * (10.49593273432072 * m + 63.02378494754052 * y + + 50.606957656360734 * k - 28620.90484698408) + + y * (0.03296041114873217 * y + 115.60384449646641 * k - + 49363.43385999684) - + k * (22.33816807309886 * k + 45932.16563550634); + + data[offset++] = r >= 0 ? 255 : r <= min ? 0 : 255 + r * scale | 0; + data[offset++] = g >= 0 ? 255 : g <= min ? 0 : 255 + g * scale | 0; + data[offset++] = b >= 0 ? 255 : b <= min ? 0 : 255 + b * scale | 0; + } + return data; + }, + + getData: function getData(width, height, forceRGBoutput) { + if (this.numComponents > 4) { + throw 'Unsupported color mode'; + } + // type of data: Uint8Array(width * height * numComponents) + var data = this._getLinearizedBlockData(width, height); + + if (this.numComponents === 3) { + return this._convertYccToRgb(data); + } else if (this.numComponents === 4) { + if (this._isColorConversionNeeded()) { + if (forceRGBoutput) { + return this._convertYcckToRgb(data); + } else { + return this._convertYcckToCmyk(data); + } + } else if (forceRGBoutput) { + return this._convertCmykToRgb(data); + } + } + return data; + } + }; + + return constructor; +})(); + + +var JpxImage = (function JpxImageClosure() { + // Table E.1 + var SubbandsGainLog2 = { + 'LL': 0, + 'LH': 1, + 'HL': 1, + 'HH': 2 + }; + function JpxImage() { + this.failOnCorruptedImage = false; + } + JpxImage.prototype = { + parse: function JpxImage_parse(data) { + + var head = readUint16(data, 0); + // No box header, immediate start of codestream (SOC) + if (head === 0xFF4F) { + this.parseCodestream(data, 0, data.length); + return; + } + + var position = 0, length = data.length; + while (position < length) { + var headerSize = 8; + var lbox = readUint32(data, position); + var tbox = readUint32(data, position + 4); + position += headerSize; + if (lbox === 1) { + // XLBox: read UInt64 according to spec. + // JavaScript's int precision of 53 bit should be sufficient here. + lbox = readUint32(data, position) * 4294967296 + + readUint32(data, position + 4); + position += 8; + headerSize += 8; + } + if (lbox === 0) { + lbox = length - position + headerSize; + } + if (lbox < headerSize) { + throw new Error('JPX Error: Invalid box field size'); + } + var dataLength = lbox - headerSize; + var jumpDataLength = true; + switch (tbox) { + case 0x6A703268: // 'jp2h' + jumpDataLength = false; // parsing child boxes + break; + case 0x636F6C72: // 'colr' + // Colorspaces are not used, the CS from the PDF is used. + var method = data[position]; + var precedence = data[position + 1]; + var approximation = data[position + 2]; + if (method === 1) { + // enumerated colorspace + var colorspace = readUint32(data, position + 3); + switch (colorspace) { + case 16: // this indicates a sRGB colorspace + case 17: // this indicates a grayscale colorspace + case 18: // this indicates a YUV colorspace + break; + default: + warn('Unknown colorspace ' + colorspace); + break; + } + } else if (method === 2) { + info('ICC profile not supported'); + } + break; + case 0x6A703263: // 'jp2c' + this.parseCodestream(data, position, position + dataLength); + break; + case 0x6A502020: // 'jP\024\024' + if (0x0d0a870a !== readUint32(data, position)) { + warn('Invalid JP2 signature'); + } + break; + // The following header types are valid but currently not used: + case 0x6A501A1A: // 'jP\032\032' + case 0x66747970: // 'ftyp' + case 0x72726571: // 'rreq' + case 0x72657320: // 'res ' + case 0x69686472: // 'ihdr' + break; + default: + var headerType = String.fromCharCode((tbox >> 24) & 0xFF, + (tbox >> 16) & 0xFF, + (tbox >> 8) & 0xFF, + tbox & 0xFF); + warn('Unsupported header type ' + tbox + ' (' + headerType + ')'); + break; + } + if (jumpDataLength) { + position += dataLength; + } + } + }, + parseImageProperties: function JpxImage_parseImageProperties(stream) { + var newByte = stream.getByte(); + while (newByte >= 0) { + var oldByte = newByte; + newByte = stream.getByte(); + var code = (oldByte << 8) | newByte; + // Image and tile size (SIZ) + if (code === 0xFF51) { + stream.skip(4); + var Xsiz = stream.getInt32() >>> 0; // Byte 4 + var Ysiz = stream.getInt32() >>> 0; // Byte 8 + var XOsiz = stream.getInt32() >>> 0; // Byte 12 + var YOsiz = stream.getInt32() >>> 0; // Byte 16 + stream.skip(16); + var Csiz = stream.getUint16(); // Byte 36 + this.width = Xsiz - XOsiz; + this.height = Ysiz - YOsiz; + this.componentsCount = Csiz; + // Results are always returned as Uint8Arrays + this.bitsPerComponent = 8; + return; + } + } + throw new Error('JPX Error: No size marker found in JPX stream'); + }, + parseCodestream: function JpxImage_parseCodestream(data, start, end) { + var context = {}; + try { + var doNotRecover = false; + var position = start; + while (position + 1 < end) { + var code = readUint16(data, position); + position += 2; + + var length = 0, j, sqcd, spqcds, spqcdSize, scalarExpounded, tile; + switch (code) { + case 0xFF4F: // Start of codestream (SOC) + context.mainHeader = true; + break; + case 0xFFD9: // End of codestream (EOC) + break; + case 0xFF51: // Image and tile size (SIZ) + length = readUint16(data, position); + var siz = {}; + siz.Xsiz = readUint32(data, position + 4); + siz.Ysiz = readUint32(data, position + 8); + siz.XOsiz = readUint32(data, position + 12); + siz.YOsiz = readUint32(data, position + 16); + siz.XTsiz = readUint32(data, position + 20); + siz.YTsiz = readUint32(data, position + 24); + siz.XTOsiz = readUint32(data, position + 28); + siz.YTOsiz = readUint32(data, position + 32); + var componentsCount = readUint16(data, position + 36); + siz.Csiz = componentsCount; + var components = []; + j = position + 38; + for (var i = 0; i < componentsCount; i++) { + var component = { + precision: (data[j] & 0x7F) + 1, + isSigned: !!(data[j] & 0x80), + XRsiz: data[j + 1], + YRsiz: data[j + 1] + }; + calculateComponentDimensions(component, siz); + components.push(component); + } + context.SIZ = siz; + context.components = components; + calculateTileGrids(context, components); + context.QCC = []; + context.COC = []; + break; + case 0xFF5C: // Quantization default (QCD) + length = readUint16(data, position); + var qcd = {}; + j = position + 2; + sqcd = data[j++]; + switch (sqcd & 0x1F) { + case 0: + spqcdSize = 8; + scalarExpounded = true; + break; + case 1: + spqcdSize = 16; + scalarExpounded = false; + break; + case 2: + spqcdSize = 16; + scalarExpounded = true; + break; + default: + throw new Error('JPX Error: Invalid SQcd value ' + sqcd); + } + qcd.noQuantization = (spqcdSize === 8); + qcd.scalarExpounded = scalarExpounded; + qcd.guardBits = sqcd >> 5; + spqcds = []; + while (j < length + position) { + var spqcd = {}; + if (spqcdSize === 8) { + spqcd.epsilon = data[j++] >> 3; + spqcd.mu = 0; + } else { + spqcd.epsilon = data[j] >> 3; + spqcd.mu = ((data[j] & 0x7) << 8) | data[j + 1]; + j += 2; + } + spqcds.push(spqcd); + } + qcd.SPqcds = spqcds; + if (context.mainHeader) { + context.QCD = qcd; + } else { + context.currentTile.QCD = qcd; + context.currentTile.QCC = []; + } + break; + case 0xFF5D: // Quantization component (QCC) + length = readUint16(data, position); + var qcc = {}; + j = position + 2; + var cqcc; + if (context.SIZ.Csiz < 257) { + cqcc = data[j++]; + } else { + cqcc = readUint16(data, j); + j += 2; + } + sqcd = data[j++]; + switch (sqcd & 0x1F) { + case 0: + spqcdSize = 8; + scalarExpounded = true; + break; + case 1: + spqcdSize = 16; + scalarExpounded = false; + break; + case 2: + spqcdSize = 16; + scalarExpounded = true; + break; + default: + throw new Error('JPX Error: Invalid SQcd value ' + sqcd); + } + qcc.noQuantization = (spqcdSize === 8); + qcc.scalarExpounded = scalarExpounded; + qcc.guardBits = sqcd >> 5; + spqcds = []; + while (j < (length + position)) { + spqcd = {}; + if (spqcdSize === 8) { + spqcd.epsilon = data[j++] >> 3; + spqcd.mu = 0; + } else { + spqcd.epsilon = data[j] >> 3; + spqcd.mu = ((data[j] & 0x7) << 8) | data[j + 1]; + j += 2; + } + spqcds.push(spqcd); + } + qcc.SPqcds = spqcds; + if (context.mainHeader) { + context.QCC[cqcc] = qcc; + } else { + context.currentTile.QCC[cqcc] = qcc; + } + break; + case 0xFF52: // Coding style default (COD) + length = readUint16(data, position); + var cod = {}; + j = position + 2; + var scod = data[j++]; + cod.entropyCoderWithCustomPrecincts = !!(scod & 1); + cod.sopMarkerUsed = !!(scod & 2); + cod.ephMarkerUsed = !!(scod & 4); + cod.progressionOrder = data[j++]; + cod.layersCount = readUint16(data, j); + j += 2; + cod.multipleComponentTransform = data[j++]; + + cod.decompositionLevelsCount = data[j++]; + cod.xcb = (data[j++] & 0xF) + 2; + cod.ycb = (data[j++] & 0xF) + 2; + var blockStyle = data[j++]; + cod.selectiveArithmeticCodingBypass = !!(blockStyle & 1); + cod.resetContextProbabilities = !!(blockStyle & 2); + cod.terminationOnEachCodingPass = !!(blockStyle & 4); + cod.verticalyStripe = !!(blockStyle & 8); + cod.predictableTermination = !!(blockStyle & 16); + cod.segmentationSymbolUsed = !!(blockStyle & 32); + cod.reversibleTransformation = data[j++]; + if (cod.entropyCoderWithCustomPrecincts) { + var precinctsSizes = []; + while (j < length + position) { + var precinctsSize = data[j++]; + precinctsSizes.push({ + PPx: precinctsSize & 0xF, + PPy: precinctsSize >> 4 + }); + } + cod.precinctsSizes = precinctsSizes; + } + var unsupported = []; + if (cod.selectiveArithmeticCodingBypass) { + unsupported.push('selectiveArithmeticCodingBypass'); + } + if (cod.resetContextProbabilities) { + unsupported.push('resetContextProbabilities'); + } + if (cod.terminationOnEachCodingPass) { + unsupported.push('terminationOnEachCodingPass'); + } + if (cod.verticalyStripe) { + unsupported.push('verticalyStripe'); + } + if (cod.predictableTermination) { + unsupported.push('predictableTermination'); + } + if (unsupported.length > 0) { + doNotRecover = true; + throw new Error('JPX Error: Unsupported COD options (' + + unsupported.join(', ') + ')'); + } + if (context.mainHeader) { + context.COD = cod; + } else { + context.currentTile.COD = cod; + context.currentTile.COC = []; + } + break; + case 0xFF90: // Start of tile-part (SOT) + length = readUint16(data, position); + tile = {}; + tile.index = readUint16(data, position + 2); + tile.length = readUint32(data, position + 4); + tile.dataEnd = tile.length + position - 2; + tile.partIndex = data[position + 8]; + tile.partsCount = data[position + 9]; + + context.mainHeader = false; + if (tile.partIndex === 0) { + // reset component specific settings + tile.COD = context.COD; + tile.COC = context.COC.slice(0); // clone of the global COC + tile.QCD = context.QCD; + tile.QCC = context.QCC.slice(0); // clone of the global COC + } + context.currentTile = tile; + break; + case 0xFF93: // Start of data (SOD) + tile = context.currentTile; + if (tile.partIndex === 0) { + initializeTile(context, tile.index); + buildPackets(context); + } + + // moving to the end of the data + length = tile.dataEnd - position; + parseTilePackets(context, data, position, length); + break; + case 0xFF55: // Tile-part lengths, main header (TLM) + case 0xFF57: // Packet length, main header (PLM) + case 0xFF58: // Packet length, tile-part header (PLT) + case 0xFF64: // Comment (COM) + length = readUint16(data, position); + // skipping content + break; + case 0xFF53: // Coding style component (COC) + throw new Error('JPX Error: Codestream code 0xFF53 (COC) is ' + + 'not implemented'); + default: + throw new Error('JPX Error: Unknown codestream code: ' + + code.toString(16)); + } + position += length; + } + } catch (e) { + if (doNotRecover || this.failOnCorruptedImage) { + throw e; + } else { + warn('Trying to recover from ' + e.message); + } + } + this.tiles = transformComponents(context); + this.width = context.SIZ.Xsiz - context.SIZ.XOsiz; + this.height = context.SIZ.Ysiz - context.SIZ.YOsiz; + this.componentsCount = context.SIZ.Csiz; + } + }; + function calculateComponentDimensions(component, siz) { + // Section B.2 Component mapping + component.x0 = Math.ceil(siz.XOsiz / component.XRsiz); + component.x1 = Math.ceil(siz.Xsiz / component.XRsiz); + component.y0 = Math.ceil(siz.YOsiz / component.YRsiz); + component.y1 = Math.ceil(siz.Ysiz / component.YRsiz); + component.width = component.x1 - component.x0; + component.height = component.y1 - component.y0; + } + function calculateTileGrids(context, components) { + var siz = context.SIZ; + // Section B.3 Division into tile and tile-components + var tile, tiles = []; + var numXtiles = Math.ceil((siz.Xsiz - siz.XTOsiz) / siz.XTsiz); + var numYtiles = Math.ceil((siz.Ysiz - siz.YTOsiz) / siz.YTsiz); + for (var q = 0; q < numYtiles; q++) { + for (var p = 0; p < numXtiles; p++) { + tile = {}; + tile.tx0 = Math.max(siz.XTOsiz + p * siz.XTsiz, siz.XOsiz); + tile.ty0 = Math.max(siz.YTOsiz + q * siz.YTsiz, siz.YOsiz); + tile.tx1 = Math.min(siz.XTOsiz + (p + 1) * siz.XTsiz, siz.Xsiz); + tile.ty1 = Math.min(siz.YTOsiz + (q + 1) * siz.YTsiz, siz.Ysiz); + tile.width = tile.tx1 - tile.tx0; + tile.height = tile.ty1 - tile.ty0; + tile.components = []; + tiles.push(tile); + } + } + context.tiles = tiles; + + var componentsCount = siz.Csiz; + for (var i = 0, ii = componentsCount; i < ii; i++) { + var component = components[i]; + for (var j = 0, jj = tiles.length; j < jj; j++) { + var tileComponent = {}; + tile = tiles[j]; + tileComponent.tcx0 = Math.ceil(tile.tx0 / component.XRsiz); + tileComponent.tcy0 = Math.ceil(tile.ty0 / component.YRsiz); + tileComponent.tcx1 = Math.ceil(tile.tx1 / component.XRsiz); + tileComponent.tcy1 = Math.ceil(tile.ty1 / component.YRsiz); + tileComponent.width = tileComponent.tcx1 - tileComponent.tcx0; + tileComponent.height = tileComponent.tcy1 - tileComponent.tcy0; + tile.components[i] = tileComponent; + } + } + } + function getBlocksDimensions(context, component, r) { + var codOrCoc = component.codingStyleParameters; + var result = {}; + if (!codOrCoc.entropyCoderWithCustomPrecincts) { + result.PPx = 15; + result.PPy = 15; + } else { + result.PPx = codOrCoc.precinctsSizes[r].PPx; + result.PPy = codOrCoc.precinctsSizes[r].PPy; + } + // calculate codeblock size as described in section B.7 + result.xcb_ = (r > 0 ? Math.min(codOrCoc.xcb, result.PPx - 1) : + Math.min(codOrCoc.xcb, result.PPx)); + result.ycb_ = (r > 0 ? Math.min(codOrCoc.ycb, result.PPy - 1) : + Math.min(codOrCoc.ycb, result.PPy)); + return result; + } + function buildPrecincts(context, resolution, dimensions) { + // Section B.6 Division resolution to precincts + var precinctWidth = 1 << dimensions.PPx; + var precinctHeight = 1 << dimensions.PPy; + // Jasper introduces codeblock groups for mapping each subband codeblocks + // to precincts. Precinct partition divides a resolution according to width + // and height parameters. The subband that belongs to the resolution level + // has a different size than the level, unless it is the zero resolution. + + // From Jasper documentation: jpeg2000.pdf, section K: Tier-2 coding: + // The precinct partitioning for a particular subband is derived from a + // partitioning of its parent LL band (i.e., the LL band at the next higher + // resolution level)... The LL band associated with each resolution level is + // divided into precincts... Each of the resulting precinct regions is then + // mapped into its child subbands (if any) at the next lower resolution + // level. This is accomplished by using the coordinate transformation + // (u, v) = (ceil(x/2), ceil(y/2)) where (x, y) and (u, v) are the + // coordinates of a point in the LL band and child subband, respectively. + var isZeroRes = resolution.resLevel === 0; + var precinctWidthInSubband = 1 << (dimensions.PPx + (isZeroRes ? 0 : -1)); + var precinctHeightInSubband = 1 << (dimensions.PPy + (isZeroRes ? 0 : -1)); + var numprecinctswide = (resolution.trx1 > resolution.trx0 ? + Math.ceil(resolution.trx1 / precinctWidth) - + Math.floor(resolution.trx0 / precinctWidth) : 0); + var numprecinctshigh = (resolution.try1 > resolution.try0 ? + Math.ceil(resolution.try1 / precinctHeight) - + Math.floor(resolution.try0 / precinctHeight) : 0); + var numprecincts = numprecinctswide * numprecinctshigh; + + resolution.precinctParameters = { + precinctWidth: precinctWidth, + precinctHeight: precinctHeight, + numprecinctswide: numprecinctswide, + numprecinctshigh: numprecinctshigh, + numprecincts: numprecincts, + precinctWidthInSubband: precinctWidthInSubband, + precinctHeightInSubband: precinctHeightInSubband + }; + } + function buildCodeblocks(context, subband, dimensions) { + // Section B.7 Division sub-band into code-blocks + var xcb_ = dimensions.xcb_; + var ycb_ = dimensions.ycb_; + var codeblockWidth = 1 << xcb_; + var codeblockHeight = 1 << ycb_; + var cbx0 = subband.tbx0 >> xcb_; + var cby0 = subband.tby0 >> ycb_; + var cbx1 = (subband.tbx1 + codeblockWidth - 1) >> xcb_; + var cby1 = (subband.tby1 + codeblockHeight - 1) >> ycb_; + var precinctParameters = subband.resolution.precinctParameters; + var codeblocks = []; + var precincts = []; + var i, j, codeblock, precinctNumber; + for (j = cby0; j < cby1; j++) { + for (i = cbx0; i < cbx1; i++) { + codeblock = { + cbx: i, + cby: j, + tbx0: codeblockWidth * i, + tby0: codeblockHeight * j, + tbx1: codeblockWidth * (i + 1), + tby1: codeblockHeight * (j + 1) + }; + + codeblock.tbx0_ = Math.max(subband.tbx0, codeblock.tbx0); + codeblock.tby0_ = Math.max(subband.tby0, codeblock.tby0); + codeblock.tbx1_ = Math.min(subband.tbx1, codeblock.tbx1); + codeblock.tby1_ = Math.min(subband.tby1, codeblock.tby1); + + // Calculate precinct number for this codeblock, codeblock position + // should be relative to its subband, use actual dimension and position + // See comment about codeblock group width and height + var pi = Math.floor((codeblock.tbx0_ - subband.tbx0) / + precinctParameters.precinctWidthInSubband); + var pj = Math.floor((codeblock.tby0_ - subband.tby0) / + precinctParameters.precinctHeightInSubband); + precinctNumber = pi + (pj * precinctParameters.numprecinctswide); + + codeblock.precinctNumber = precinctNumber; + codeblock.subbandType = subband.type; + codeblock.Lblock = 3; + + if (codeblock.tbx1_ <= codeblock.tbx0_ || + codeblock.tby1_ <= codeblock.tby0_) { + continue; + } + codeblocks.push(codeblock); + // building precinct for the sub-band + var precinct = precincts[precinctNumber]; + if (precinct !== undefined) { + if (i < precinct.cbxMin) { + precinct.cbxMin = i; + } else if (i > precinct.cbxMax) { + precinct.cbxMax = i; + } + if (j < precinct.cbyMin) { + precinct.cbxMin = j; + } else if (j > precinct.cbyMax) { + precinct.cbyMax = j; + } + } else { + precincts[precinctNumber] = precinct = { + cbxMin: i, + cbyMin: j, + cbxMax: i, + cbyMax: j + }; + } + codeblock.precinct = precinct; + } + } + subband.codeblockParameters = { + codeblockWidth: xcb_, + codeblockHeight: ycb_, + numcodeblockwide: cbx1 - cbx0 + 1, + numcodeblockhigh: cby1 - cby0 + 1 + }; + subband.codeblocks = codeblocks; + subband.precincts = precincts; + } + function createPacket(resolution, precinctNumber, layerNumber) { + var precinctCodeblocks = []; + // Section B.10.8 Order of info in packet + var subbands = resolution.subbands; + // sub-bands already ordered in 'LL', 'HL', 'LH', and 'HH' sequence + for (var i = 0, ii = subbands.length; i < ii; i++) { + var subband = subbands[i]; + var codeblocks = subband.codeblocks; + for (var j = 0, jj = codeblocks.length; j < jj; j++) { + var codeblock = codeblocks[j]; + if (codeblock.precinctNumber !== precinctNumber) { + continue; + } + precinctCodeblocks.push(codeblock); + } + } + return { + layerNumber: layerNumber, + codeblocks: precinctCodeblocks + }; + } + function LayerResolutionComponentPositionIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var maxDecompositionLevelsCount = 0; + for (var q = 0; q < componentsCount; q++) { + maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, + tile.components[q].codingStyleParameters.decompositionLevelsCount); + } + + var l = 0, r = 0, i = 0, k = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.1 Layer-resolution-component-position + for (; l < layersCount; l++) { + for (; r <= maxDecompositionLevelsCount; r++) { + for (; i < componentsCount; i++) { + var component = tile.components[i]; + if (r > component.codingStyleParameters.decompositionLevelsCount) { + continue; + } + + var resolution = component.resolutions[r]; + var numprecincts = resolution.precinctParameters.numprecincts; + for (; k < numprecincts;) { + var packet = createPacket(resolution, k, l); + k++; + return packet; + } + k = 0; + } + i = 0; + } + r = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function ResolutionLayerComponentPositionIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var maxDecompositionLevelsCount = 0; + for (var q = 0; q < componentsCount; q++) { + maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, + tile.components[q].codingStyleParameters.decompositionLevelsCount); + } + + var r = 0, l = 0, i = 0, k = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.2 Resolution-layer-component-position + for (; r <= maxDecompositionLevelsCount; r++) { + for (; l < layersCount; l++) { + for (; i < componentsCount; i++) { + var component = tile.components[i]; + if (r > component.codingStyleParameters.decompositionLevelsCount) { + continue; + } + + var resolution = component.resolutions[r]; + var numprecincts = resolution.precinctParameters.numprecincts; + for (; k < numprecincts;) { + var packet = createPacket(resolution, k, l); + k++; + return packet; + } + k = 0; + } + i = 0; + } + l = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function ResolutionPositionComponentLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var l, r, c, p; + var maxDecompositionLevelsCount = 0; + for (c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, + component.codingStyleParameters.decompositionLevelsCount); + } + var maxNumPrecinctsInLevel = new Int32Array( + maxDecompositionLevelsCount + 1); + for (r = 0; r <= maxDecompositionLevelsCount; ++r) { + var maxNumPrecincts = 0; + for (c = 0; c < componentsCount; ++c) { + var resolutions = tile.components[c].resolutions; + if (r < resolutions.length) { + maxNumPrecincts = Math.max(maxNumPrecincts, + resolutions[r].precinctParameters.numprecincts); + } + } + maxNumPrecinctsInLevel[r] = maxNumPrecincts; + } + l = 0; + r = 0; + c = 0; + p = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.3 Resolution-position-component-layer + for (; r <= maxDecompositionLevelsCount; r++) { + for (; p < maxNumPrecinctsInLevel[r]; p++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + if (r > component.codingStyleParameters.decompositionLevelsCount) { + continue; + } + var resolution = component.resolutions[r]; + var numprecincts = resolution.precinctParameters.numprecincts; + if (p >= numprecincts) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, p, l); + l++; + return packet; + } + l = 0; + } + c = 0; + } + p = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function PositionComponentResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var precinctsIterationSizes = precinctsSizes; + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.4 Position-component-resolution-layer + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = + precinctsSizes.components[c].resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + c = 0; + } + px = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function ComponentPositionResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.5 Component-position-resolution-layer + for (; c < componentsCount; ++c) { + var component = tile.components[c]; + var precinctsIterationSizes = precinctsSizes.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = precinctsIterationSizes.resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + px = 0; + } + py = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function getPrecinctIndexIfExist( + pxIndex, pyIndex, sizeInImageScale, precinctIterationSizes, resolution) { + var posX = pxIndex * precinctIterationSizes.minWidth; + var posY = pyIndex * precinctIterationSizes.minHeight; + if (posX % sizeInImageScale.width !== 0 || + posY % sizeInImageScale.height !== 0) { + return null; + } + var startPrecinctRowIndex = + (posY / sizeInImageScale.width) * + resolution.precinctParameters.numprecinctswide; + return (posX / sizeInImageScale.height) + startPrecinctRowIndex; + } + function getPrecinctSizesInImageScale(tile) { + var componentsCount = tile.components.length; + var minWidth = Number.MAX_VALUE; + var minHeight = Number.MAX_VALUE; + var maxNumWide = 0; + var maxNumHigh = 0; + var sizePerComponent = new Array(componentsCount); + for (var c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + var sizePerResolution = new Array(decompositionLevelsCount + 1); + var minWidthCurrentComponent = Number.MAX_VALUE; + var minHeightCurrentComponent = Number.MAX_VALUE; + var maxNumWideCurrentComponent = 0; + var maxNumHighCurrentComponent = 0; + var scale = 1; + for (var r = decompositionLevelsCount; r >= 0; --r) { + var resolution = component.resolutions[r]; + var widthCurrentResolution = + scale * resolution.precinctParameters.precinctWidth; + var heightCurrentResolution = + scale * resolution.precinctParameters.precinctHeight; + minWidthCurrentComponent = Math.min( + minWidthCurrentComponent, + widthCurrentResolution); + minHeightCurrentComponent = Math.min( + minHeightCurrentComponent, + heightCurrentResolution); + maxNumWideCurrentComponent = Math.max(maxNumWideCurrentComponent, + resolution.precinctParameters.numprecinctswide); + maxNumHighCurrentComponent = Math.max(maxNumHighCurrentComponent, + resolution.precinctParameters.numprecinctshigh); + sizePerResolution[r] = { + width: widthCurrentResolution, + height: heightCurrentResolution + }; + scale <<= 1; + } + minWidth = Math.min(minWidth, minWidthCurrentComponent); + minHeight = Math.min(minHeight, minHeightCurrentComponent); + maxNumWide = Math.max(maxNumWide, maxNumWideCurrentComponent); + maxNumHigh = Math.max(maxNumHigh, maxNumHighCurrentComponent); + sizePerComponent[c] = { + resolutions: sizePerResolution, + minWidth: minWidthCurrentComponent, + minHeight: minHeightCurrentComponent, + maxNumWide: maxNumWideCurrentComponent, + maxNumHigh: maxNumHighCurrentComponent + }; + } + return { + components: sizePerComponent, + minWidth: minWidth, + minHeight: minHeight, + maxNumWide: maxNumWide, + maxNumHigh: maxNumHigh + }; + } + function buildPackets(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var componentsCount = siz.Csiz; + // Creating resolutions and sub-bands for each component + for (var c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + // Section B.5 Resolution levels and sub-bands + var resolutions = []; + var subbands = []; + for (var r = 0; r <= decompositionLevelsCount; r++) { + var blocksDimensions = getBlocksDimensions(context, component, r); + var resolution = {}; + var scale = 1 << (decompositionLevelsCount - r); + resolution.trx0 = Math.ceil(component.tcx0 / scale); + resolution.try0 = Math.ceil(component.tcy0 / scale); + resolution.trx1 = Math.ceil(component.tcx1 / scale); + resolution.try1 = Math.ceil(component.tcy1 / scale); + resolution.resLevel = r; + buildPrecincts(context, resolution, blocksDimensions); + resolutions.push(resolution); + + var subband; + if (r === 0) { + // one sub-band (LL) with last decomposition + subband = {}; + subband.type = 'LL'; + subband.tbx0 = Math.ceil(component.tcx0 / scale); + subband.tby0 = Math.ceil(component.tcy0 / scale); + subband.tbx1 = Math.ceil(component.tcx1 / scale); + subband.tby1 = Math.ceil(component.tcy1 / scale); + subband.resolution = resolution; + buildCodeblocks(context, subband, blocksDimensions); + subbands.push(subband); + resolution.subbands = [subband]; + } else { + var bscale = 1 << (decompositionLevelsCount - r + 1); + var resolutionSubbands = []; + // three sub-bands (HL, LH and HH) with rest of decompositions + subband = {}; + subband.type = 'HL'; + subband.tbx0 = Math.ceil(component.tcx0 / bscale - 0.5); + subband.tby0 = Math.ceil(component.tcy0 / bscale); + subband.tbx1 = Math.ceil(component.tcx1 / bscale - 0.5); + subband.tby1 = Math.ceil(component.tcy1 / bscale); + subband.resolution = resolution; + buildCodeblocks(context, subband, blocksDimensions); + subbands.push(subband); + resolutionSubbands.push(subband); + + subband = {}; + subband.type = 'LH'; + subband.tbx0 = Math.ceil(component.tcx0 / bscale); + subband.tby0 = Math.ceil(component.tcy0 / bscale - 0.5); + subband.tbx1 = Math.ceil(component.tcx1 / bscale); + subband.tby1 = Math.ceil(component.tcy1 / bscale - 0.5); + subband.resolution = resolution; + buildCodeblocks(context, subband, blocksDimensions); + subbands.push(subband); + resolutionSubbands.push(subband); + + subband = {}; + subband.type = 'HH'; + subband.tbx0 = Math.ceil(component.tcx0 / bscale - 0.5); + subband.tby0 = Math.ceil(component.tcy0 / bscale - 0.5); + subband.tbx1 = Math.ceil(component.tcx1 / bscale - 0.5); + subband.tby1 = Math.ceil(component.tcy1 / bscale - 0.5); + subband.resolution = resolution; + buildCodeblocks(context, subband, blocksDimensions); + subbands.push(subband); + resolutionSubbands.push(subband); + + resolution.subbands = resolutionSubbands; + } + } + component.resolutions = resolutions; + component.subbands = subbands; + } + // Generate the packets sequence + var progressionOrder = tile.codingStyleDefaultParameters.progressionOrder; + switch (progressionOrder) { + case 0: + tile.packetsIterator = + new LayerResolutionComponentPositionIterator(context); + break; + case 1: + tile.packetsIterator = + new ResolutionLayerComponentPositionIterator(context); + break; + case 2: + tile.packetsIterator = + new ResolutionPositionComponentLayerIterator(context); + break; + case 3: + tile.packetsIterator = + new PositionComponentResolutionLayerIterator(context); + break; + case 4: + tile.packetsIterator = + new ComponentPositionResolutionLayerIterator(context); + break; + default: + throw new Error('JPX Error: Unsupported progression order ' + + progressionOrder); + } + } + function parseTilePackets(context, data, offset, dataLength) { + var position = 0; + var buffer, bufferSize = 0, skipNextBit = false; + function readBits(count) { + while (bufferSize < count) { + var b = data[offset + position]; + position++; + if (skipNextBit) { + buffer = (buffer << 7) | b; + bufferSize += 7; + skipNextBit = false; + } else { + buffer = (buffer << 8) | b; + bufferSize += 8; + } + if (b === 0xFF) { + skipNextBit = true; + } + } + bufferSize -= count; + return (buffer >>> bufferSize) & ((1 << count) - 1); + } + function skipMarkerIfEqual(value) { + if (data[offset + position - 1] === 0xFF && + data[offset + position] === value) { + skipBytes(1); + return true; + } else if (data[offset + position] === 0xFF && + data[offset + position + 1] === value) { + skipBytes(2); + return true; + } + return false; + } + function skipBytes(count) { + position += count; + } + function alignToByte() { + bufferSize = 0; + if (skipNextBit) { + position++; + skipNextBit = false; + } + } + function readCodingpasses() { + if (readBits(1) === 0) { + return 1; + } + if (readBits(1) === 0) { + return 2; + } + var value = readBits(2); + if (value < 3) { + return value + 3; + } + value = readBits(5); + if (value < 31) { + return value + 6; + } + value = readBits(7); + return value + 37; + } + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var sopMarkerUsed = context.COD.sopMarkerUsed; + var ephMarkerUsed = context.COD.ephMarkerUsed; + var packetsIterator = tile.packetsIterator; + while (position < dataLength) { + alignToByte(); + if (sopMarkerUsed && skipMarkerIfEqual(0x91)) { + // Skip also marker segment length and packet sequence ID + skipBytes(4); + } + var packet = packetsIterator.nextPacket(); + if (!readBits(1)) { + continue; + } + var layerNumber = packet.layerNumber; + var queue = [], codeblock; + for (var i = 0, ii = packet.codeblocks.length; i < ii; i++) { + codeblock = packet.codeblocks[i]; + var precinct = codeblock.precinct; + var codeblockColumn = codeblock.cbx - precinct.cbxMin; + var codeblockRow = codeblock.cby - precinct.cbyMin; + var codeblockIncluded = false; + var firstTimeInclusion = false; + var valueReady; + if (codeblock['included'] !== undefined) { + codeblockIncluded = !!readBits(1); + } else { + // reading inclusion tree + precinct = codeblock.precinct; + var inclusionTree, zeroBitPlanesTree; + if (precinct['inclusionTree'] !== undefined) { + inclusionTree = precinct.inclusionTree; + } else { + // building inclusion and zero bit-planes trees + var width = precinct.cbxMax - precinct.cbxMin + 1; + var height = precinct.cbyMax - precinct.cbyMin + 1; + inclusionTree = new InclusionTree(width, height, layerNumber); + zeroBitPlanesTree = new TagTree(width, height); + precinct.inclusionTree = inclusionTree; + precinct.zeroBitPlanesTree = zeroBitPlanesTree; + } + + if (inclusionTree.reset(codeblockColumn, codeblockRow, layerNumber)) { + while (true) { + if (readBits(1)) { + valueReady = !inclusionTree.nextLevel(); + if (valueReady) { + codeblock.included = true; + codeblockIncluded = firstTimeInclusion = true; + break; + } + } else { + inclusionTree.incrementValue(layerNumber); + break; + } + } + } + } + if (!codeblockIncluded) { + continue; + } + if (firstTimeInclusion) { + zeroBitPlanesTree = precinct.zeroBitPlanesTree; + zeroBitPlanesTree.reset(codeblockColumn, codeblockRow); + while (true) { + if (readBits(1)) { + valueReady = !zeroBitPlanesTree.nextLevel(); + if (valueReady) { + break; + } + } else { + zeroBitPlanesTree.incrementValue(); + } + } + codeblock.zeroBitPlanes = zeroBitPlanesTree.value; + } + var codingpasses = readCodingpasses(); + while (readBits(1)) { + codeblock.Lblock++; + } + var codingpassesLog2 = log2(codingpasses); + // rounding down log2 + var bits = ((codingpasses < (1 << codingpassesLog2)) ? + codingpassesLog2 - 1 : codingpassesLog2) + codeblock.Lblock; + var codedDataLength = readBits(bits); + queue.push({ + codeblock: codeblock, + codingpasses: codingpasses, + dataLength: codedDataLength + }); + } + alignToByte(); + if (ephMarkerUsed) { + skipMarkerIfEqual(0x92); + } + while (queue.length > 0) { + var packetItem = queue.shift(); + codeblock = packetItem.codeblock; + if (codeblock['data'] === undefined) { + codeblock.data = []; + } + codeblock.data.push({ + data: data, + start: offset + position, + end: offset + position + packetItem.dataLength, + codingpasses: packetItem.codingpasses + }); + position += packetItem.dataLength; + } + } + return position; + } + function copyCoefficients(coefficients, levelWidth, levelHeight, subband, + delta, mb, reversible, segmentationSymbolUsed) { + var x0 = subband.tbx0; + var y0 = subband.tby0; + var width = subband.tbx1 - subband.tbx0; + var codeblocks = subband.codeblocks; + var right = subband.type.charAt(0) === 'H' ? 1 : 0; + var bottom = subband.type.charAt(1) === 'H' ? levelWidth : 0; + + for (var i = 0, ii = codeblocks.length; i < ii; ++i) { + var codeblock = codeblocks[i]; + var blockWidth = codeblock.tbx1_ - codeblock.tbx0_; + var blockHeight = codeblock.tby1_ - codeblock.tby0_; + if (blockWidth === 0 || blockHeight === 0) { + continue; + } + if (codeblock['data'] === undefined) { + continue; + } + + var bitModel, currentCodingpassType; + bitModel = new BitModel(blockWidth, blockHeight, codeblock.subbandType, + codeblock.zeroBitPlanes, mb); + currentCodingpassType = 2; // first bit plane starts from cleanup + + // collect data + var data = codeblock.data, totalLength = 0, codingpasses = 0; + var j, jj, dataItem; + for (j = 0, jj = data.length; j < jj; j++) { + dataItem = data[j]; + totalLength += dataItem.end - dataItem.start; + codingpasses += dataItem.codingpasses; + } + var encodedData = new Uint8Array(totalLength); + var position = 0; + for (j = 0, jj = data.length; j < jj; j++) { + dataItem = data[j]; + var chunk = dataItem.data.subarray(dataItem.start, dataItem.end); + encodedData.set(chunk, position); + position += chunk.length; + } + // decoding the item + var decoder = new ArithmeticDecoder(encodedData, 0, totalLength); + bitModel.setDecoder(decoder); + + for (j = 0; j < codingpasses; j++) { + switch (currentCodingpassType) { + case 0: + bitModel.runSignificancePropogationPass(); + break; + case 1: + bitModel.runMagnitudeRefinementPass(); + break; + case 2: + bitModel.runCleanupPass(); + if (segmentationSymbolUsed) { + bitModel.checkSegmentationSymbol(); + } + break; + } + currentCodingpassType = (currentCodingpassType + 1) % 3; + } + + var offset = (codeblock.tbx0_ - x0) + (codeblock.tby0_ - y0) * width; + var sign = bitModel.coefficentsSign; + var magnitude = bitModel.coefficentsMagnitude; + var bitsDecoded = bitModel.bitsDecoded; + var magnitudeCorrection = reversible ? 0 : 0.5; + var k, n, nb; + position = 0; + // Do the interleaving of Section F.3.3 here, so we do not need + // to copy later. LL level is not interleaved, just copied. + var interleave = (subband.type !== 'LL'); + for (j = 0; j < blockHeight; j++) { + var row = (offset / width) | 0; // row in the non-interleaved subband + var levelOffset = 2 * row * (levelWidth - width) + right + bottom; + for (k = 0; k < blockWidth; k++) { + n = magnitude[position]; + if (n !== 0) { + n = (n + magnitudeCorrection) * delta; + if (sign[position] !== 0) { + n = -n; + } + nb = bitsDecoded[position]; + var pos = interleave ? (levelOffset + (offset << 1)) : offset; + if (reversible && (nb >= mb)) { + coefficients[pos] = n; + } else { + coefficients[pos] = n * (1 << (mb - nb)); + } + } + offset++; + position++; + } + offset += width - blockWidth; + } + } + } + function transformTile(context, tile, c) { + var component = tile.components[c]; + var codingStyleParameters = component.codingStyleParameters; + var quantizationParameters = component.quantizationParameters; + var decompositionLevelsCount = + codingStyleParameters.decompositionLevelsCount; + var spqcds = quantizationParameters.SPqcds; + var scalarExpounded = quantizationParameters.scalarExpounded; + var guardBits = quantizationParameters.guardBits; + var segmentationSymbolUsed = codingStyleParameters.segmentationSymbolUsed; + var precision = context.components[c].precision; + + var reversible = codingStyleParameters.reversibleTransformation; + var transform = (reversible ? new ReversibleTransform() : + new IrreversibleTransform()); + + var subbandCoefficients = []; + var b = 0; + for (var i = 0; i <= decompositionLevelsCount; i++) { + var resolution = component.resolutions[i]; + + var width = resolution.trx1 - resolution.trx0; + var height = resolution.try1 - resolution.try0; + // Allocate space for the whole sublevel. + var coefficients = new Float32Array(width * height); + + for (var j = 0, jj = resolution.subbands.length; j < jj; j++) { + var mu, epsilon; + if (!scalarExpounded) { + // formula E-5 + mu = spqcds[0].mu; + epsilon = spqcds[0].epsilon + (i > 0 ? 1 - i : 0); + } else { + mu = spqcds[b].mu; + epsilon = spqcds[b].epsilon; + b++; + } + + var subband = resolution.subbands[j]; + var gainLog2 = SubbandsGainLog2[subband.type]; + + // calulate quantization coefficient (Section E.1.1.1) + var delta = (reversible ? 1 : + Math.pow(2, precision + gainLog2 - epsilon) * (1 + mu / 2048)); + var mb = (guardBits + epsilon - 1); + + // In the first resolution level, copyCoefficients will fill the + // whole array with coefficients. In the succeding passes, + // copyCoefficients will consecutively fill in the values that belong + // to the interleaved positions of the HL, LH, and HH coefficients. + // The LL coefficients will then be interleaved in Transform.iterate(). + copyCoefficients(coefficients, width, height, subband, delta, mb, + reversible, segmentationSymbolUsed); + } + subbandCoefficients.push({ + width: width, + height: height, + items: coefficients + }); + } + + var result = transform.calculate(subbandCoefficients, + component.tcx0, component.tcy0); + return { + left: component.tcx0, + top: component.tcy0, + width: result.width, + height: result.height, + items: result.items + }; + } + function transformComponents(context) { + var siz = context.SIZ; + var components = context.components; + var componentsCount = siz.Csiz; + var resultImages = []; + for (var i = 0, ii = context.tiles.length; i < ii; i++) { + var tile = context.tiles[i]; + var transformedTiles = []; + var c; + for (c = 0; c < componentsCount; c++) { + transformedTiles[c] = transformTile(context, tile, c); + } + var tile0 = transformedTiles[0]; + var out = new Uint8Array(tile0.items.length * componentsCount); + var result = { + left: tile0.left, + top: tile0.top, + width: tile0.width, + height: tile0.height, + items: out + }; + + // Section G.2.2 Inverse multi component transform + var shift, offset, max, min, maxK; + var pos = 0, j, jj, y0, y1, y2, r, g, b, k, val; + if (tile.codingStyleDefaultParameters.multipleComponentTransform) { + var fourComponents = componentsCount === 4; + var y0items = transformedTiles[0].items; + var y1items = transformedTiles[1].items; + var y2items = transformedTiles[2].items; + var y3items = fourComponents ? transformedTiles[3].items : null; + + // HACK: The multiple component transform formulas below assume that + // all components have the same precision. With this in mind, we + // compute shift and offset only once. + shift = components[0].precision - 8; + offset = (128 << shift) + 0.5; + max = 255 * (1 << shift); + maxK = max * 0.5; + min = -maxK; + + var component0 = tile.components[0]; + var alpha01 = componentsCount - 3; + jj = y0items.length; + if (!component0.codingStyleParameters.reversibleTransformation) { + // inverse irreversible multiple component transform + for (j = 0; j < jj; j++, pos += alpha01) { + y0 = y0items[j] + offset; + y1 = y1items[j]; + y2 = y2items[j]; + r = y0 + 1.402 * y2; + g = y0 - 0.34413 * y1 - 0.71414 * y2; + b = y0 + 1.772 * y1; + out[pos++] = r <= 0 ? 0 : r >= max ? 255 : r >> shift; + out[pos++] = g <= 0 ? 0 : g >= max ? 255 : g >> shift; + out[pos++] = b <= 0 ? 0 : b >= max ? 255 : b >> shift; + } + } else { + // inverse reversible multiple component transform + for (j = 0; j < jj; j++, pos += alpha01) { + y0 = y0items[j] + offset; + y1 = y1items[j]; + y2 = y2items[j]; + g = y0 - ((y2 + y1) >> 2); + r = g + y2; + b = g + y1; + out[pos++] = r <= 0 ? 0 : r >= max ? 255 : r >> shift; + out[pos++] = g <= 0 ? 0 : g >= max ? 255 : g >> shift; + out[pos++] = b <= 0 ? 0 : b >= max ? 255 : b >> shift; + } + } + if (fourComponents) { + for (j = 0, pos = 3; j < jj; j++, pos += 4) { + k = y3items[j]; + out[pos] = k <= min ? 0 : k >= maxK ? 255 : (k + offset) >> shift; + } + } + } else { // no multi-component transform + for (c = 0; c < componentsCount; c++) { + var items = transformedTiles[c].items; + shift = components[c].precision - 8; + offset = (128 << shift) + 0.5; + max = (127.5 * (1 << shift)); + min = -max; + for (pos = c, j = 0, jj = items.length; j < jj; j++) { + val = items[j]; + out[pos] = val <= min ? 0 : + val >= max ? 255 : (val + offset) >> shift; + pos += componentsCount; + } + } + } + resultImages.push(result); + } + return resultImages; + } + function initializeTile(context, tileIndex) { + var siz = context.SIZ; + var componentsCount = siz.Csiz; + var tile = context.tiles[tileIndex]; + for (var c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + var qcdOrQcc = (context.currentTile.QCC[c] !== undefined ? + context.currentTile.QCC[c] : context.currentTile.QCD); + component.quantizationParameters = qcdOrQcc; + var codOrCoc = (context.currentTile.COC[c] !== undefined ? + context.currentTile.COC[c] : context.currentTile.COD); + component.codingStyleParameters = codOrCoc; + } + tile.codingStyleDefaultParameters = context.currentTile.COD; + } + + // Section B.10.2 Tag trees + var TagTree = (function TagTreeClosure() { + function TagTree(width, height) { + var levelsLength = log2(Math.max(width, height)) + 1; + this.levels = []; + for (var i = 0; i < levelsLength; i++) { + var level = { + width: width, + height: height, + items: [] + }; + this.levels.push(level); + width = Math.ceil(width / 2); + height = Math.ceil(height / 2); + } + } + TagTree.prototype = { + reset: function TagTree_reset(i, j) { + var currentLevel = 0, value = 0, level; + while (currentLevel < this.levels.length) { + level = this.levels[currentLevel]; + var index = i + j * level.width; + if (level.items[index] !== undefined) { + value = level.items[index]; + break; + } + level.index = index; + i >>= 1; + j >>= 1; + currentLevel++; + } + currentLevel--; + level = this.levels[currentLevel]; + level.items[level.index] = value; + this.currentLevel = currentLevel; + delete this.value; + }, + incrementValue: function TagTree_incrementValue() { + var level = this.levels[this.currentLevel]; + level.items[level.index]++; + }, + nextLevel: function TagTree_nextLevel() { + var currentLevel = this.currentLevel; + var level = this.levels[currentLevel]; + var value = level.items[level.index]; + currentLevel--; + if (currentLevel < 0) { + this.value = value; + return false; + } + + this.currentLevel = currentLevel; + level = this.levels[currentLevel]; + level.items[level.index] = value; + return true; + } + }; + return TagTree; + })(); + + var InclusionTree = (function InclusionTreeClosure() { + function InclusionTree(width, height, defaultValue) { + var levelsLength = log2(Math.max(width, height)) + 1; + this.levels = []; + for (var i = 0; i < levelsLength; i++) { + var items = new Uint8Array(width * height); + for (var j = 0, jj = items.length; j < jj; j++) { + items[j] = defaultValue; + } + + var level = { + width: width, + height: height, + items: items + }; + this.levels.push(level); + + width = Math.ceil(width / 2); + height = Math.ceil(height / 2); + } + } + InclusionTree.prototype = { + reset: function InclusionTree_reset(i, j, stopValue) { + var currentLevel = 0; + while (currentLevel < this.levels.length) { + var level = this.levels[currentLevel]; + var index = i + j * level.width; + level.index = index; + var value = level.items[index]; + + if (value === 0xFF) { + break; + } + + if (value > stopValue) { + this.currentLevel = currentLevel; + // already know about this one, propagating the value to top levels + this.propagateValues(); + return false; + } + + i >>= 1; + j >>= 1; + currentLevel++; + } + this.currentLevel = currentLevel - 1; + return true; + }, + incrementValue: function InclusionTree_incrementValue(stopValue) { + var level = this.levels[this.currentLevel]; + level.items[level.index] = stopValue + 1; + this.propagateValues(); + }, + propagateValues: function InclusionTree_propagateValues() { + var levelIndex = this.currentLevel; + var level = this.levels[levelIndex]; + var currentValue = level.items[level.index]; + while (--levelIndex >= 0) { + level = this.levels[levelIndex]; + level.items[level.index] = currentValue; + } + }, + nextLevel: function InclusionTree_nextLevel() { + var currentLevel = this.currentLevel; + var level = this.levels[currentLevel]; + var value = level.items[level.index]; + level.items[level.index] = 0xFF; + currentLevel--; + if (currentLevel < 0) { + return false; + } + + this.currentLevel = currentLevel; + level = this.levels[currentLevel]; + level.items[level.index] = value; + return true; + } + }; + return InclusionTree; + })(); + + // Section D. Coefficient bit modeling + var BitModel = (function BitModelClosure() { + var UNIFORM_CONTEXT = 17; + var RUNLENGTH_CONTEXT = 18; + // Table D-1 + // The index is binary presentation: 0dddvvhh, ddd - sum of Di (0..4), + // vv - sum of Vi (0..2), and hh - sum of Hi (0..2) + var LLAndLHContextsLabel = new Uint8Array([ + 0, 5, 8, 0, 3, 7, 8, 0, 4, 7, 8, 0, 0, 0, 0, 0, 1, 6, 8, 0, 3, 7, 8, 0, 4, + 7, 8, 0, 0, 0, 0, 0, 2, 6, 8, 0, 3, 7, 8, 0, 4, 7, 8, 0, 0, 0, 0, 0, 2, 6, + 8, 0, 3, 7, 8, 0, 4, 7, 8, 0, 0, 0, 0, 0, 2, 6, 8, 0, 3, 7, 8, 0, 4, 7, 8 + ]); + var HLContextLabel = new Uint8Array([ + 0, 3, 4, 0, 5, 7, 7, 0, 8, 8, 8, 0, 0, 0, 0, 0, 1, 3, 4, 0, 6, 7, 7, 0, 8, + 8, 8, 0, 0, 0, 0, 0, 2, 3, 4, 0, 6, 7, 7, 0, 8, 8, 8, 0, 0, 0, 0, 0, 2, 3, + 4, 0, 6, 7, 7, 0, 8, 8, 8, 0, 0, 0, 0, 0, 2, 3, 4, 0, 6, 7, 7, 0, 8, 8, 8 + ]); + var HHContextLabel = new Uint8Array([ + 0, 1, 2, 0, 1, 2, 2, 0, 2, 2, 2, 0, 0, 0, 0, 0, 3, 4, 5, 0, 4, 5, 5, 0, 5, + 5, 5, 0, 0, 0, 0, 0, 6, 7, 7, 0, 7, 7, 7, 0, 7, 7, 7, 0, 0, 0, 0, 0, 8, 8, + 8, 0, 8, 8, 8, 0, 8, 8, 8, 0, 0, 0, 0, 0, 8, 8, 8, 0, 8, 8, 8, 0, 8, 8, 8 + ]); + + function BitModel(width, height, subband, zeroBitPlanes, mb) { + this.width = width; + this.height = height; + + this.contextLabelTable = (subband === 'HH' ? HHContextLabel : + (subband === 'HL' ? HLContextLabel : LLAndLHContextsLabel)); + + var coefficientCount = width * height; + + // coefficients outside the encoding region treated as insignificant + // add border state cells for significanceState + this.neighborsSignificance = new Uint8Array(coefficientCount); + this.coefficentsSign = new Uint8Array(coefficientCount); + this.coefficentsMagnitude = mb > 14 ? new Uint32Array(coefficientCount) : + mb > 6 ? new Uint16Array(coefficientCount) : + new Uint8Array(coefficientCount); + this.processingFlags = new Uint8Array(coefficientCount); + + var bitsDecoded = new Uint8Array(coefficientCount); + if (zeroBitPlanes !== 0) { + for (var i = 0; i < coefficientCount; i++) { + bitsDecoded[i] = zeroBitPlanes; + } + } + this.bitsDecoded = bitsDecoded; + + this.reset(); + } + + BitModel.prototype = { + setDecoder: function BitModel_setDecoder(decoder) { + this.decoder = decoder; + }, + reset: function BitModel_reset() { + // We have 17 contexts that are accessed via context labels, + // plus the uniform and runlength context. + this.contexts = new Int8Array(19); + + // Contexts are packed into 1 byte: + // highest 7 bits carry the index, lowest bit carries mps + this.contexts[0] = (4 << 1) | 0; + this.contexts[UNIFORM_CONTEXT] = (46 << 1) | 0; + this.contexts[RUNLENGTH_CONTEXT] = (3 << 1) | 0; + }, + setNeighborsSignificance: + function BitModel_setNeighborsSignificance(row, column, index) { + var neighborsSignificance = this.neighborsSignificance; + var width = this.width, height = this.height; + var left = (column > 0); + var right = (column + 1 < width); + var i; + + if (row > 0) { + i = index - width; + if (left) { + neighborsSignificance[i - 1] += 0x10; + } + if (right) { + neighborsSignificance[i + 1] += 0x10; + } + neighborsSignificance[i] += 0x04; + } + + if (row + 1 < height) { + i = index + width; + if (left) { + neighborsSignificance[i - 1] += 0x10; + } + if (right) { + neighborsSignificance[i + 1] += 0x10; + } + neighborsSignificance[i] += 0x04; + } + + if (left) { + neighborsSignificance[index - 1] += 0x01; + } + if (right) { + neighborsSignificance[index + 1] += 0x01; + } + neighborsSignificance[index] |= 0x80; + }, + runSignificancePropogationPass: + function BitModel_runSignificancePropogationPass() { + var decoder = this.decoder; + var width = this.width, height = this.height; + var coefficentsMagnitude = this.coefficentsMagnitude; + var coefficentsSign = this.coefficentsSign; + var neighborsSignificance = this.neighborsSignificance; + var processingFlags = this.processingFlags; + var contexts = this.contexts; + var labels = this.contextLabelTable; + var bitsDecoded = this.bitsDecoded; + var processedInverseMask = ~1; + var processedMask = 1; + var firstMagnitudeBitMask = 2; + + for (var i0 = 0; i0 < height; i0 += 4) { + for (var j = 0; j < width; j++) { + var index = i0 * width + j; + for (var i1 = 0; i1 < 4; i1++, index += width) { + var i = i0 + i1; + if (i >= height) { + break; + } + // clear processed flag first + processingFlags[index] &= processedInverseMask; + + if (coefficentsMagnitude[index] || + !neighborsSignificance[index]) { + continue; + } + + var contextLabel = labels[neighborsSignificance[index]]; + var decision = decoder.readBit(contexts, contextLabel); + if (decision) { + var sign = this.decodeSignBit(i, j, index); + coefficentsSign[index] = sign; + coefficentsMagnitude[index] = 1; + this.setNeighborsSignificance(i, j, index); + processingFlags[index] |= firstMagnitudeBitMask; + } + bitsDecoded[index]++; + processingFlags[index] |= processedMask; + } + } + } + }, + decodeSignBit: function BitModel_decodeSignBit(row, column, index) { + var width = this.width, height = this.height; + var coefficentsMagnitude = this.coefficentsMagnitude; + var coefficentsSign = this.coefficentsSign; + var contribution, sign0, sign1, significance1; + var contextLabel, decoded; + + // calculate horizontal contribution + significance1 = (column > 0 && coefficentsMagnitude[index - 1] !== 0); + if (column + 1 < width && coefficentsMagnitude[index + 1] !== 0) { + sign1 = coefficentsSign[index + 1]; + if (significance1) { + sign0 = coefficentsSign[index - 1]; + contribution = 1 - sign1 - sign0; + } else { + contribution = 1 - sign1 - sign1; + } + } else if (significance1) { + sign0 = coefficentsSign[index - 1]; + contribution = 1 - sign0 - sign0; + } else { + contribution = 0; + } + var horizontalContribution = 3 * contribution; + + // calculate vertical contribution and combine with the horizontal + significance1 = (row > 0 && coefficentsMagnitude[index - width] !== 0); + if (row + 1 < height && coefficentsMagnitude[index + width] !== 0) { + sign1 = coefficentsSign[index + width]; + if (significance1) { + sign0 = coefficentsSign[index - width]; + contribution = 1 - sign1 - sign0 + horizontalContribution; + } else { + contribution = 1 - sign1 - sign1 + horizontalContribution; + } + } else if (significance1) { + sign0 = coefficentsSign[index - width]; + contribution = 1 - sign0 - sign0 + horizontalContribution; + } else { + contribution = horizontalContribution; + } + + if (contribution >= 0) { + contextLabel = 9 + contribution; + decoded = this.decoder.readBit(this.contexts, contextLabel); + } else { + contextLabel = 9 - contribution; + decoded = this.decoder.readBit(this.contexts, contextLabel) ^ 1; + } + return decoded; + }, + runMagnitudeRefinementPass: + function BitModel_runMagnitudeRefinementPass() { + var decoder = this.decoder; + var width = this.width, height = this.height; + var coefficentsMagnitude = this.coefficentsMagnitude; + var neighborsSignificance = this.neighborsSignificance; + var contexts = this.contexts; + var bitsDecoded = this.bitsDecoded; + var processingFlags = this.processingFlags; + var processedMask = 1; + var firstMagnitudeBitMask = 2; + var length = width * height; + var width4 = width * 4; + + for (var index0 = 0, indexNext; index0 < length; index0 = indexNext) { + indexNext = Math.min(length, index0 + width4); + for (var j = 0; j < width; j++) { + for (var index = index0 + j; index < indexNext; index += width) { + + // significant but not those that have just become + if (!coefficentsMagnitude[index] || + (processingFlags[index] & processedMask) !== 0) { + continue; + } + + var contextLabel = 16; + if ((processingFlags[index] & firstMagnitudeBitMask) !== 0) { + processingFlags[index] ^= firstMagnitudeBitMask; + // first refinement + var significance = neighborsSignificance[index] & 127; + contextLabel = significance === 0 ? 15 : 14; + } + + var bit = decoder.readBit(contexts, contextLabel); + coefficentsMagnitude[index] = + (coefficentsMagnitude[index] << 1) | bit; + bitsDecoded[index]++; + processingFlags[index] |= processedMask; + } + } + } + }, + runCleanupPass: function BitModel_runCleanupPass() { + var decoder = this.decoder; + var width = this.width, height = this.height; + var neighborsSignificance = this.neighborsSignificance; + var coefficentsMagnitude = this.coefficentsMagnitude; + var coefficentsSign = this.coefficentsSign; + var contexts = this.contexts; + var labels = this.contextLabelTable; + var bitsDecoded = this.bitsDecoded; + var processingFlags = this.processingFlags; + var processedMask = 1; + var firstMagnitudeBitMask = 2; + var oneRowDown = width; + var twoRowsDown = width * 2; + var threeRowsDown = width * 3; + var iNext; + for (var i0 = 0; i0 < height; i0 = iNext) { + iNext = Math.min(i0 + 4, height); + var indexBase = i0 * width; + var checkAllEmpty = i0 + 3 < height; + for (var j = 0; j < width; j++) { + var index0 = indexBase + j; + // using the property: labels[neighborsSignificance[index]] === 0 + // when neighborsSignificance[index] === 0 + var allEmpty = (checkAllEmpty && + processingFlags[index0] === 0 && + processingFlags[index0 + oneRowDown] === 0 && + processingFlags[index0 + twoRowsDown] === 0 && + processingFlags[index0 + threeRowsDown] === 0 && + neighborsSignificance[index0] === 0 && + neighborsSignificance[index0 + oneRowDown] === 0 && + neighborsSignificance[index0 + twoRowsDown] === 0 && + neighborsSignificance[index0 + threeRowsDown] === 0); + var i1 = 0, index = index0; + var i = i0, sign; + if (allEmpty) { + var hasSignificantCoefficent = + decoder.readBit(contexts, RUNLENGTH_CONTEXT); + if (!hasSignificantCoefficent) { + bitsDecoded[index0]++; + bitsDecoded[index0 + oneRowDown]++; + bitsDecoded[index0 + twoRowsDown]++; + bitsDecoded[index0 + threeRowsDown]++; + continue; // next column + } + i1 = (decoder.readBit(contexts, UNIFORM_CONTEXT) << 1) | + decoder.readBit(contexts, UNIFORM_CONTEXT); + if (i1 !== 0) { + i = i0 + i1; + index += i1 * width; + } + + sign = this.decodeSignBit(i, j, index); + coefficentsSign[index] = sign; + coefficentsMagnitude[index] = 1; + this.setNeighborsSignificance(i, j, index); + processingFlags[index] |= firstMagnitudeBitMask; + + index = index0; + for (var i2 = i0; i2 <= i; i2++, index += width) { + bitsDecoded[index]++; + } + + i1++; + } + for (i = i0 + i1; i < iNext; i++, index += width) { + if (coefficentsMagnitude[index] || + (processingFlags[index] & processedMask) !== 0) { + continue; + } + + var contextLabel = labels[neighborsSignificance[index]]; + var decision = decoder.readBit(contexts, contextLabel); + if (decision === 1) { + sign = this.decodeSignBit(i, j, index); + coefficentsSign[index] = sign; + coefficentsMagnitude[index] = 1; + this.setNeighborsSignificance(i, j, index); + processingFlags[index] |= firstMagnitudeBitMask; + } + bitsDecoded[index]++; + } + } + } + }, + checkSegmentationSymbol: function BitModel_checkSegmentationSymbol() { + var decoder = this.decoder; + var contexts = this.contexts; + var symbol = (decoder.readBit(contexts, UNIFORM_CONTEXT) << 3) | + (decoder.readBit(contexts, UNIFORM_CONTEXT) << 2) | + (decoder.readBit(contexts, UNIFORM_CONTEXT) << 1) | + decoder.readBit(contexts, UNIFORM_CONTEXT); + if (symbol !== 0xA) { + throw new Error('JPX Error: Invalid segmentation symbol'); + } + } + }; + + return BitModel; + })(); + + // Section F, Discrete wavelet transformation + var Transform = (function TransformClosure() { + function Transform() {} + + Transform.prototype.calculate = + function transformCalculate(subbands, u0, v0) { + var ll = subbands[0]; + for (var i = 1, ii = subbands.length; i < ii; i++) { + ll = this.iterate(ll, subbands[i], u0, v0); + } + return ll; + }; + Transform.prototype.extend = function extend(buffer, offset, size) { + // Section F.3.7 extending... using max extension of 4 + var i1 = offset - 1, j1 = offset + 1; + var i2 = offset + size - 2, j2 = offset + size; + buffer[i1--] = buffer[j1++]; + buffer[j2++] = buffer[i2--]; + buffer[i1--] = buffer[j1++]; + buffer[j2++] = buffer[i2--]; + buffer[i1--] = buffer[j1++]; + buffer[j2++] = buffer[i2--]; + buffer[i1] = buffer[j1]; + buffer[j2] = buffer[i2]; + }; + Transform.prototype.iterate = function Transform_iterate(ll, hl_lh_hh, + u0, v0) { + var llWidth = ll.width, llHeight = ll.height, llItems = ll.items; + var width = hl_lh_hh.width; + var height = hl_lh_hh.height; + var items = hl_lh_hh.items; + var i, j, k, l, u, v; + + // Interleave LL according to Section F.3.3 + for (k = 0, i = 0; i < llHeight; i++) { + l = i * 2 * width; + for (j = 0; j < llWidth; j++, k++, l += 2) { + items[l] = llItems[k]; + } + } + // The LL band is not needed anymore. + llItems = ll.items = null; + + var bufferPadding = 4; + var rowBuffer = new Float32Array(width + 2 * bufferPadding); + + // Section F.3.4 HOR_SR + if (width === 1) { + // if width = 1, when u0 even keep items as is, when odd divide by 2 + if ((u0 & 1) !== 0) { + for (v = 0, k = 0; v < height; v++, k += width) { + items[k] *= 0.5; + } + } + } else { + for (v = 0, k = 0; v < height; v++, k += width) { + rowBuffer.set(items.subarray(k, k + width), bufferPadding); + + this.extend(rowBuffer, bufferPadding, width); + this.filter(rowBuffer, bufferPadding, width); + + items.set( + rowBuffer.subarray(bufferPadding, bufferPadding + width), + k); + } + } + + // Accesses to the items array can take long, because it may not fit into + // CPU cache and has to be fetched from main memory. Since subsequent + // accesses to the items array are not local when reading columns, we + // have a cache miss every time. To reduce cache misses, get up to + // 'numBuffers' items at a time and store them into the individual + // buffers. The colBuffers should be small enough to fit into CPU cache. + var numBuffers = 16; + var colBuffers = []; + for (i = 0; i < numBuffers; i++) { + colBuffers.push(new Float32Array(height + 2 * bufferPadding)); + } + var b, currentBuffer = 0; + ll = bufferPadding + height; + + // Section F.3.5 VER_SR + if (height === 1) { + // if height = 1, when v0 even keep items as is, when odd divide by 2 + if ((v0 & 1) !== 0) { + for (u = 0; u < width; u++) { + items[u] *= 0.5; + } + } + } else { + for (u = 0; u < width; u++) { + // if we ran out of buffers, copy several image columns at once + if (currentBuffer === 0) { + numBuffers = Math.min(width - u, numBuffers); + for (k = u, l = bufferPadding; l < ll; k += width, l++) { + for (b = 0; b < numBuffers; b++) { + colBuffers[b][l] = items[k + b]; + } + } + currentBuffer = numBuffers; + } + + currentBuffer--; + var buffer = colBuffers[currentBuffer]; + this.extend(buffer, bufferPadding, height); + this.filter(buffer, bufferPadding, height); + + // If this is last buffer in this group of buffers, flush all buffers. + if (currentBuffer === 0) { + k = u - numBuffers + 1; + for (l = bufferPadding; l < ll; k += width, l++) { + for (b = 0; b < numBuffers; b++) { + items[k + b] = colBuffers[b][l]; + } + } + } + } + } + + return { + width: width, + height: height, + items: items + }; + }; + return Transform; + })(); + + // Section 3.8.2 Irreversible 9-7 filter + var IrreversibleTransform = (function IrreversibleTransformClosure() { + function IrreversibleTransform() { + Transform.call(this); + } + + IrreversibleTransform.prototype = Object.create(Transform.prototype); + IrreversibleTransform.prototype.filter = + function irreversibleTransformFilter(x, offset, length) { + var len = length >> 1; + offset = offset | 0; + var j, n, current, next; + + var alpha = -1.586134342059924; + var beta = -0.052980118572961; + var gamma = 0.882911075530934; + var delta = 0.443506852043971; + var K = 1.230174104914001; + var K_ = 1 / K; + + // step 1 is combined with step 3 + + // step 2 + j = offset - 3; + for (n = len + 4; n--; j += 2) { + x[j] *= K_; + } + + // step 1 & 3 + j = offset - 2; + current = delta * x[j -1]; + for (n = len + 3; n--; j += 2) { + next = delta * x[j + 1]; + x[j] = K * x[j] - current - next; + if (n--) { + j += 2; + current = delta * x[j + 1]; + x[j] = K * x[j] - current - next; + } else { + break; + } + } + + // step 4 + j = offset - 1; + current = gamma * x[j - 1]; + for (n = len + 2; n--; j += 2) { + next = gamma * x[j + 1]; + x[j] -= current + next; + if (n--) { + j += 2; + current = gamma * x[j + 1]; + x[j] -= current + next; + } else { + break; + } + } + + // step 5 + j = offset; + current = beta * x[j - 1]; + for (n = len + 1; n--; j += 2) { + next = beta * x[j + 1]; + x[j] -= current + next; + if (n--) { + j += 2; + current = beta * x[j + 1]; + x[j] -= current + next; + } else { + break; + } + } + + // step 6 + if (len !== 0) { + j = offset + 1; + current = alpha * x[j - 1]; + for (n = len; n--; j += 2) { + next = alpha * x[j + 1]; + x[j] -= current + next; + if (n--) { + j += 2; + current = alpha * x[j + 1]; + x[j] -= current + next; + } else { + break; + } + } + } + }; + + return IrreversibleTransform; + })(); + + // Section 3.8.1 Reversible 5-3 filter + var ReversibleTransform = (function ReversibleTransformClosure() { + function ReversibleTransform() { + Transform.call(this); + } + + ReversibleTransform.prototype = Object.create(Transform.prototype); + ReversibleTransform.prototype.filter = + function reversibleTransformFilter(x, offset, length) { + var len = length >> 1; + offset = offset | 0; + var j, n; + + for (j = offset, n = len + 1; n--; j += 2) { + x[j] -= (x[j - 1] + x[j + 1] + 2) >> 2; + } + + for (j = offset + 1, n = len; n--; j += 2) { + x[j] += (x[j - 1] + x[j + 1]) >> 1; + } + }; + + return ReversibleTransform; + })(); + + return JpxImage; +})(); + + +var Jbig2Image = (function Jbig2ImageClosure() { + // Utility data structures + function ContextCache() {} + + ContextCache.prototype = { + getContexts: function(id) { + if (id in this) { + return this[id]; + } + return (this[id] = new Int8Array(1 << 16)); + } + }; + + function DecodingContext(data, start, end) { + this.data = data; + this.start = start; + this.end = end; + } + + DecodingContext.prototype = { + get decoder() { + var decoder = new ArithmeticDecoder(this.data, this.start, this.end); + return shadow(this, 'decoder', decoder); + }, + get contextCache() { + var cache = new ContextCache(); + return shadow(this, 'contextCache', cache); + } + }; + + // Annex A. Arithmetic Integer Decoding Procedure + // A.2 Procedure for decoding values + function decodeInteger(contextCache, procedure, decoder) { + var contexts = contextCache.getContexts(procedure); + var prev = 1; + + function readBits(length) { + var v = 0; + for (var i = 0; i < length; i++) { + var bit = decoder.readBit(contexts, prev); + prev = (prev < 256 ? (prev << 1) | bit : + (((prev << 1) | bit) & 511) | 256); + v = (v << 1) | bit; + } + return v >>> 0; + } + + var sign = readBits(1); + var value = readBits(1) ? + (readBits(1) ? + (readBits(1) ? + (readBits(1) ? + (readBits(1) ? + (readBits(32) + 4436) : + readBits(12) + 340) : + readBits(8) + 84) : + readBits(6) + 20) : + readBits(4) + 4) : + readBits(2); + return (sign === 0 ? value : (value > 0 ? -value : null)); + } + + // A.3 The IAID decoding procedure + function decodeIAID(contextCache, decoder, codeLength) { + var contexts = contextCache.getContexts('IAID'); + + var prev = 1; + for (var i = 0; i < codeLength; i++) { + var bit = decoder.readBit(contexts, prev); + prev = (prev << 1) | bit; + } + if (codeLength < 31) { + return prev & ((1 << codeLength) - 1); + } + return prev & 0x7FFFFFFF; + } + + // 7.3 Segment types + var SegmentTypes = [ + 'SymbolDictionary', null, null, null, 'IntermediateTextRegion', null, + 'ImmediateTextRegion', 'ImmediateLosslessTextRegion', null, null, null, + null, null, null, null, null, 'patternDictionary', null, null, null, + 'IntermediateHalftoneRegion', null, 'ImmediateHalftoneRegion', + 'ImmediateLosslessHalftoneRegion', null, null, null, null, null, null, null, + null, null, null, null, null, 'IntermediateGenericRegion', null, + 'ImmediateGenericRegion', 'ImmediateLosslessGenericRegion', + 'IntermediateGenericRefinementRegion', null, + 'ImmediateGenericRefinementRegion', + 'ImmediateLosslessGenericRefinementRegion', null, null, null, null, + 'PageInformation', 'EndOfPage', 'EndOfStripe', 'EndOfFile', 'Profiles', + 'Tables', null, null, null, null, null, null, null, null, + 'Extension' + ]; + + var CodingTemplates = [ + [{x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}, {x: -2, y: -1}, + {x: -1, y: -1}, {x: 0, y: -1}, {x: 1, y: -1}, {x: 2, y: -1}, + {x: -4, y: 0}, {x: -3, y: 0}, {x: -2, y: 0}, {x: -1, y: 0}], + [{x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}, {x: 2, y: -2}, + {x: -2, y: -1}, {x: -1, y: -1}, {x: 0, y: -1}, {x: 1, y: -1}, + {x: 2, y: -1}, {x: -3, y: 0}, {x: -2, y: 0}, {x: -1, y: 0}], + [{x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}, {x: -2, y: -1}, + {x: -1, y: -1}, {x: 0, y: -1}, {x: 1, y: -1}, {x: -2, y: 0}, + {x: -1, y: 0}], + [{x: -3, y: -1}, {x: -2, y: -1}, {x: -1, y: -1}, {x: 0, y: -1}, + {x: 1, y: -1}, {x: -4, y: 0}, {x: -3, y: 0}, {x: -2, y: 0}, {x: -1, y: 0}] + ]; + + var RefinementTemplates = [ + { + coding: [{x: 0, y: -1}, {x: 1, y: -1}, {x: -1, y: 0}], + reference: [{x: 0, y: -1}, {x: 1, y: -1}, {x: -1, y: 0}, {x: 0, y: 0}, + {x: 1, y: 0}, {x: -1, y: 1}, {x: 0, y: 1}, {x: 1, y: 1}] + }, + { + coding: [{x: -1, y: -1}, {x: 0, y: -1}, {x: 1, y: -1}, {x: -1, y: 0}], + reference: [{x: 0, y: -1}, {x: -1, y: 0}, {x: 0, y: 0}, {x: 1, y: 0}, + {x: 0, y: 1}, {x: 1, y: 1}] + } + ]; + + // See 6.2.5.7 Decoding the bitmap. + var ReusedContexts = [ + 0x9B25, // 10011 0110010 0101 + 0x0795, // 0011 110010 101 + 0x00E5, // 001 11001 01 + 0x0195 // 011001 0101 + ]; + + var RefinementReusedContexts = [ + 0x0020, // '000' + '0' (coding) + '00010000' + '0' (reference) + 0x0008 // '0000' + '001000' + ]; + + function decodeBitmapTemplate0(width, height, decodingContext) { + var decoder = decodingContext.decoder; + var contexts = decodingContext.contextCache.getContexts('GB'); + var contextLabel, i, j, pixel, row, row1, row2, bitmap = []; + + // ...ooooo.... + // ..ooooooo... Context template for current pixel (X) + // .ooooX...... (concatenate values of 'o'-pixels to get contextLabel) + var OLD_PIXEL_MASK = 0x7BF7; // 01111 0111111 0111 + + for (i = 0; i < height; i++) { + row = bitmap[i] = new Uint8Array(width); + row1 = (i < 1) ? row : bitmap[i - 1]; + row2 = (i < 2) ? row : bitmap[i - 2]; + + // At the beginning of each row: + // Fill contextLabel with pixels that are above/right of (X) + contextLabel = (row2[0] << 13) | (row2[1] << 12) | (row2[2] << 11) | + (row1[0] << 7) | (row1[1] << 6) | (row1[2] << 5) | + (row1[3] << 4); + + for (j = 0; j < width; j++) { + row[j] = pixel = decoder.readBit(contexts, contextLabel); + + // At each pixel: Clear contextLabel pixels that are shifted + // out of the context, then add new ones. + contextLabel = ((contextLabel & OLD_PIXEL_MASK) << 1) | + (j + 3 < width ? row2[j + 3] << 11 : 0) | + (j + 4 < width ? row1[j + 4] << 4 : 0) | pixel; + } + } + + return bitmap; + } + + // 6.2 Generic Region Decoding Procedure + function decodeBitmap(mmr, width, height, templateIndex, prediction, skip, at, + decodingContext) { + if (mmr) { + error('JBIG2 error: MMR encoding is not supported'); + } + + // Use optimized version for the most common case + if (templateIndex === 0 && !skip && !prediction && at.length === 4 && + at[0].x === 3 && at[0].y === -1 && at[1].x === -3 && at[1].y === -1 && + at[2].x === 2 && at[2].y === -2 && at[3].x === -2 && at[3].y === -2) { + return decodeBitmapTemplate0(width, height, decodingContext); + } + + var useskip = !!skip; + var template = CodingTemplates[templateIndex].concat(at); + + // Sorting is non-standard, and it is not required. But sorting increases + // the number of template bits that can be reused from the previous + // contextLabel in the main loop. + template.sort(function (a, b) { + return (a.y - b.y) || (a.x - b.x); + }); + + var templateLength = template.length; + var templateX = new Int8Array(templateLength); + var templateY = new Int8Array(templateLength); + var changingTemplateEntries = []; + var reuseMask = 0, minX = 0, maxX = 0, minY = 0; + var c, k; + + for (k = 0; k < templateLength; k++) { + templateX[k] = template[k].x; + templateY[k] = template[k].y; + minX = Math.min(minX, template[k].x); + maxX = Math.max(maxX, template[k].x); + minY = Math.min(minY, template[k].y); + // Check if the template pixel appears in two consecutive context labels, + // so it can be reused. Otherwise, we add it to the list of changing + // template entries. + if (k < templateLength - 1 && + template[k].y === template[k + 1].y && + template[k].x === template[k + 1].x - 1) { + reuseMask |= 1 << (templateLength - 1 - k); + } else { + changingTemplateEntries.push(k); + } + } + var changingEntriesLength = changingTemplateEntries.length; + + var changingTemplateX = new Int8Array(changingEntriesLength); + var changingTemplateY = new Int8Array(changingEntriesLength); + var changingTemplateBit = new Uint16Array(changingEntriesLength); + for (c = 0; c < changingEntriesLength; c++) { + k = changingTemplateEntries[c]; + changingTemplateX[c] = template[k].x; + changingTemplateY[c] = template[k].y; + changingTemplateBit[c] = 1 << (templateLength - 1 - k); + } + + // Get the safe bounding box edges from the width, height, minX, maxX, minY + var sbb_left = -minX; + var sbb_top = -minY; + var sbb_right = width - maxX; + + var pseudoPixelContext = ReusedContexts[templateIndex]; + var row = new Uint8Array(width); + var bitmap = []; + + var decoder = decodingContext.decoder; + var contexts = decodingContext.contextCache.getContexts('GB'); + + var ltp = 0, j, i0, j0, contextLabel = 0, bit, shift; + for (var i = 0; i < height; i++) { + if (prediction) { + var sltp = decoder.readBit(contexts, pseudoPixelContext); + ltp ^= sltp; + if (ltp) { + bitmap.push(row); // duplicate previous row + continue; + } + } + row = new Uint8Array(row); + bitmap.push(row); + for (j = 0; j < width; j++) { + if (useskip && skip[i][j]) { + row[j] = 0; + continue; + } + // Are we in the middle of a scanline, so we can reuse contextLabel + // bits? + if (j >= sbb_left && j < sbb_right && i >= sbb_top) { + // If yes, we can just shift the bits that are reusable and only + // fetch the remaining ones. + contextLabel = (contextLabel << 1) & reuseMask; + for (k = 0; k < changingEntriesLength; k++) { + i0 = i + changingTemplateY[k]; + j0 = j + changingTemplateX[k]; + bit = bitmap[i0][j0]; + if (bit) { + bit = changingTemplateBit[k]; + contextLabel |= bit; + } + } + } else { + // compute the contextLabel from scratch + contextLabel = 0; + shift = templateLength - 1; + for (k = 0; k < templateLength; k++, shift--) { + j0 = j + templateX[k]; + if (j0 >= 0 && j0 < width) { + i0 = i + templateY[k]; + if (i0 >= 0) { + bit = bitmap[i0][j0]; + if (bit) { + contextLabel |= bit << shift; + } + } + } + } + } + var pixel = decoder.readBit(contexts, contextLabel); + row[j] = pixel; + } + } + return bitmap; + } + + // 6.3.2 Generic Refinement Region Decoding Procedure + function decodeRefinement(width, height, templateIndex, referenceBitmap, + offsetX, offsetY, prediction, at, + decodingContext) { + var codingTemplate = RefinementTemplates[templateIndex].coding; + if (templateIndex === 0) { + codingTemplate = codingTemplate.concat([at[0]]); + } + var codingTemplateLength = codingTemplate.length; + var codingTemplateX = new Int32Array(codingTemplateLength); + var codingTemplateY = new Int32Array(codingTemplateLength); + var k; + for (k = 0; k < codingTemplateLength; k++) { + codingTemplateX[k] = codingTemplate[k].x; + codingTemplateY[k] = codingTemplate[k].y; + } + + var referenceTemplate = RefinementTemplates[templateIndex].reference; + if (templateIndex === 0) { + referenceTemplate = referenceTemplate.concat([at[1]]); + } + var referenceTemplateLength = referenceTemplate.length; + var referenceTemplateX = new Int32Array(referenceTemplateLength); + var referenceTemplateY = new Int32Array(referenceTemplateLength); + for (k = 0; k < referenceTemplateLength; k++) { + referenceTemplateX[k] = referenceTemplate[k].x; + referenceTemplateY[k] = referenceTemplate[k].y; + } + var referenceWidth = referenceBitmap[0].length; + var referenceHeight = referenceBitmap.length; + + var pseudoPixelContext = RefinementReusedContexts[templateIndex]; + var bitmap = []; + + var decoder = decodingContext.decoder; + var contexts = decodingContext.contextCache.getContexts('GR'); + + var ltp = 0; + for (var i = 0; i < height; i++) { + if (prediction) { + var sltp = decoder.readBit(contexts, pseudoPixelContext); + ltp ^= sltp; + if (ltp) { + error('JBIG2 error: prediction is not supported'); + } + } + var row = new Uint8Array(width); + bitmap.push(row); + for (var j = 0; j < width; j++) { + var i0, j0; + var contextLabel = 0; + for (k = 0; k < codingTemplateLength; k++) { + i0 = i + codingTemplateY[k]; + j0 = j + codingTemplateX[k]; + if (i0 < 0 || j0 < 0 || j0 >= width) { + contextLabel <<= 1; // out of bound pixel + } else { + contextLabel = (contextLabel << 1) | bitmap[i0][j0]; + } + } + for (k = 0; k < referenceTemplateLength; k++) { + i0 = i + referenceTemplateY[k] + offsetY; + j0 = j + referenceTemplateX[k] + offsetX; + if (i0 < 0 || i0 >= referenceHeight || j0 < 0 || + j0 >= referenceWidth) { + contextLabel <<= 1; // out of bound pixel + } else { + contextLabel = (contextLabel << 1) | referenceBitmap[i0][j0]; + } + } + var pixel = decoder.readBit(contexts, contextLabel); + row[j] = pixel; + } + } + + return bitmap; + } + + // 6.5.5 Decoding the symbol dictionary + function decodeSymbolDictionary(huffman, refinement, symbols, + numberOfNewSymbols, numberOfExportedSymbols, + huffmanTables, templateIndex, at, + refinementTemplateIndex, refinementAt, + decodingContext) { + if (huffman) { + error('JBIG2 error: huffman is not supported'); + } + + var newSymbols = []; + var currentHeight = 0; + var symbolCodeLength = log2(symbols.length + numberOfNewSymbols); + + var decoder = decodingContext.decoder; + var contextCache = decodingContext.contextCache; + + while (newSymbols.length < numberOfNewSymbols) { + var deltaHeight = decodeInteger(contextCache, 'IADH', decoder); // 6.5.6 + currentHeight += deltaHeight; + var currentWidth = 0; + var totalWidth = 0; + while (true) { + var deltaWidth = decodeInteger(contextCache, 'IADW', decoder); // 6.5.7 + if (deltaWidth === null) { + break; // OOB + } + currentWidth += deltaWidth; + totalWidth += currentWidth; + var bitmap; + if (refinement) { + // 6.5.8.2 Refinement/aggregate-coded symbol bitmap + var numberOfInstances = decodeInteger(contextCache, 'IAAI', decoder); + if (numberOfInstances > 1) { + bitmap = decodeTextRegion(huffman, refinement, + currentWidth, currentHeight, 0, + numberOfInstances, 1, //strip size + symbols.concat(newSymbols), + symbolCodeLength, + 0, //transposed + 0, //ds offset + 1, //top left 7.4.3.1.1 + 0, //OR operator + huffmanTables, + refinementTemplateIndex, refinementAt, + decodingContext); + } else { + var symbolId = decodeIAID(contextCache, decoder, symbolCodeLength); + var rdx = decodeInteger(contextCache, 'IARDX', decoder); // 6.4.11.3 + var rdy = decodeInteger(contextCache, 'IARDY', decoder); // 6.4.11.4 + var symbol = (symbolId < symbols.length ? symbols[symbolId] : + newSymbols[symbolId - symbols.length]); + bitmap = decodeRefinement(currentWidth, currentHeight, + refinementTemplateIndex, symbol, rdx, rdy, false, refinementAt, + decodingContext); + } + } else { + // 6.5.8.1 Direct-coded symbol bitmap + bitmap = decodeBitmap(false, currentWidth, currentHeight, + templateIndex, false, null, at, decodingContext); + } + newSymbols.push(bitmap); + } + } + // 6.5.10 Exported symbols + var exportedSymbols = []; + var flags = [], currentFlag = false; + var totalSymbolsLength = symbols.length + numberOfNewSymbols; + while (flags.length < totalSymbolsLength) { + var runLength = decodeInteger(contextCache, 'IAEX', decoder); + while (runLength--) { + flags.push(currentFlag); + } + currentFlag = !currentFlag; + } + for (var i = 0, ii = symbols.length; i < ii; i++) { + if (flags[i]) { + exportedSymbols.push(symbols[i]); + } + } + for (var j = 0; j < numberOfNewSymbols; i++, j++) { + if (flags[i]) { + exportedSymbols.push(newSymbols[j]); + } + } + return exportedSymbols; + } + + function decodeTextRegion(huffman, refinement, width, height, + defaultPixelValue, numberOfSymbolInstances, + stripSize, inputSymbols, symbolCodeLength, + transposed, dsOffset, referenceCorner, + combinationOperator, huffmanTables, + refinementTemplateIndex, refinementAt, + decodingContext) { + if (huffman) { + error('JBIG2 error: huffman is not supported'); + } + + // Prepare bitmap + var bitmap = []; + var i, row; + for (i = 0; i < height; i++) { + row = new Uint8Array(width); + if (defaultPixelValue) { + for (var j = 0; j < width; j++) { + row[j] = defaultPixelValue; + } + } + bitmap.push(row); + } + + var decoder = decodingContext.decoder; + var contextCache = decodingContext.contextCache; + var stripT = -decodeInteger(contextCache, 'IADT', decoder); // 6.4.6 + var firstS = 0; + i = 0; + while (i < numberOfSymbolInstances) { + var deltaT = decodeInteger(contextCache, 'IADT', decoder); // 6.4.6 + stripT += deltaT; + + var deltaFirstS = decodeInteger(contextCache, 'IAFS', decoder); // 6.4.7 + firstS += deltaFirstS; + var currentS = firstS; + do { + var currentT = (stripSize === 1 ? 0 : + decodeInteger(contextCache, 'IAIT', decoder)); // 6.4.9 + var t = stripSize * stripT + currentT; + var symbolId = decodeIAID(contextCache, decoder, symbolCodeLength); + var applyRefinement = (refinement && + decodeInteger(contextCache, 'IARI', decoder)); + var symbolBitmap = inputSymbols[symbolId]; + var symbolWidth = symbolBitmap[0].length; + var symbolHeight = symbolBitmap.length; + if (applyRefinement) { + var rdw = decodeInteger(contextCache, 'IARDW', decoder); // 6.4.11.1 + var rdh = decodeInteger(contextCache, 'IARDH', decoder); // 6.4.11.2 + var rdx = decodeInteger(contextCache, 'IARDX', decoder); // 6.4.11.3 + var rdy = decodeInteger(contextCache, 'IARDY', decoder); // 6.4.11.4 + symbolWidth += rdw; + symbolHeight += rdh; + symbolBitmap = decodeRefinement(symbolWidth, symbolHeight, + refinementTemplateIndex, symbolBitmap, (rdw >> 1) + rdx, + (rdh >> 1) + rdy, false, refinementAt, + decodingContext); + } + var offsetT = t - ((referenceCorner & 1) ? 0 : symbolHeight); + var offsetS = currentS - ((referenceCorner & 2) ? symbolWidth : 0); + var s2, t2, symbolRow; + if (transposed) { + // Place Symbol Bitmap from T1,S1 + for (s2 = 0; s2 < symbolHeight; s2++) { + row = bitmap[offsetS + s2]; + if (!row) { + continue; + } + symbolRow = symbolBitmap[s2]; + // To ignore Parts of Symbol bitmap which goes + // outside bitmap region + var maxWidth = Math.min(width - offsetT, symbolWidth); + switch (combinationOperator) { + case 0: // OR + for (t2 = 0; t2 < maxWidth; t2++) { + row[offsetT + t2] |= symbolRow[t2]; + } + break; + case 2: // XOR + for (t2 = 0; t2 < maxWidth; t2++) { + row[offsetT + t2] ^= symbolRow[t2]; + } + break; + default: + error('JBIG2 error: operator ' + combinationOperator + + ' is not supported'); + } + } + currentS += symbolHeight - 1; + } else { + for (t2 = 0; t2 < symbolHeight; t2++) { + row = bitmap[offsetT + t2]; + if (!row) { + continue; + } + symbolRow = symbolBitmap[t2]; + switch (combinationOperator) { + case 0: // OR + for (s2 = 0; s2 < symbolWidth; s2++) { + row[offsetS + s2] |= symbolRow[s2]; + } + break; + case 2: // XOR + for (s2 = 0; s2 < symbolWidth; s2++) { + row[offsetS + s2] ^= symbolRow[s2]; + } + break; + default: + error('JBIG2 error: operator ' + combinationOperator + + ' is not supported'); + } + } + currentS += symbolWidth - 1; + } + i++; + var deltaS = decodeInteger(contextCache, 'IADS', decoder); // 6.4.8 + if (deltaS === null) { + break; // OOB + } + currentS += deltaS + dsOffset; + } while (true); + } + return bitmap; + } + + function readSegmentHeader(data, start) { + var segmentHeader = {}; + segmentHeader.number = readUint32(data, start); + var flags = data[start + 4]; + var segmentType = flags & 0x3F; + if (!SegmentTypes[segmentType]) { + error('JBIG2 error: invalid segment type: ' + segmentType); + } + segmentHeader.type = segmentType; + segmentHeader.typeName = SegmentTypes[segmentType]; + segmentHeader.deferredNonRetain = !!(flags & 0x80); + + var pageAssociationFieldSize = !!(flags & 0x40); + var referredFlags = data[start + 5]; + var referredToCount = (referredFlags >> 5) & 7; + var retainBits = [referredFlags & 31]; + var position = start + 6; + if (referredFlags === 7) { + referredToCount = readUint32(data, position - 1) & 0x1FFFFFFF; + position += 3; + var bytes = (referredToCount + 7) >> 3; + retainBits[0] = data[position++]; + while (--bytes > 0) { + retainBits.push(data[position++]); + } + } else if (referredFlags === 5 || referredFlags === 6) { + error('JBIG2 error: invalid referred-to flags'); + } + + segmentHeader.retainBits = retainBits; + var referredToSegmentNumberSize = (segmentHeader.number <= 256 ? 1 : + (segmentHeader.number <= 65536 ? 2 : 4)); + var referredTo = []; + var i, ii; + for (i = 0; i < referredToCount; i++) { + var number = (referredToSegmentNumberSize === 1 ? data[position] : + (referredToSegmentNumberSize === 2 ? readUint16(data, position) : + readUint32(data, position))); + referredTo.push(number); + position += referredToSegmentNumberSize; + } + segmentHeader.referredTo = referredTo; + if (!pageAssociationFieldSize) { + segmentHeader.pageAssociation = data[position++]; + } else { + segmentHeader.pageAssociation = readUint32(data, position); + position += 4; + } + segmentHeader.length = readUint32(data, position); + position += 4; + + if (segmentHeader.length === 0xFFFFFFFF) { + // 7.2.7 Segment data length, unknown segment length + if (segmentType === 38) { // ImmediateGenericRegion + var genericRegionInfo = readRegionSegmentInformation(data, position); + var genericRegionSegmentFlags = data[position + + RegionSegmentInformationFieldLength]; + var genericRegionMmr = !!(genericRegionSegmentFlags & 1); + // searching for the segment end + var searchPatternLength = 6; + var searchPattern = new Uint8Array(searchPatternLength); + if (!genericRegionMmr) { + searchPattern[0] = 0xFF; + searchPattern[1] = 0xAC; + } + searchPattern[2] = (genericRegionInfo.height >>> 24) & 0xFF; + searchPattern[3] = (genericRegionInfo.height >> 16) & 0xFF; + searchPattern[4] = (genericRegionInfo.height >> 8) & 0xFF; + searchPattern[5] = genericRegionInfo.height & 0xFF; + for (i = position, ii = data.length; i < ii; i++) { + var j = 0; + while (j < searchPatternLength && searchPattern[j] === data[i + j]) { + j++; + } + if (j === searchPatternLength) { + segmentHeader.length = i + searchPatternLength; + break; + } + } + if (segmentHeader.length === 0xFFFFFFFF) { + error('JBIG2 error: segment end was not found'); + } + } else { + error('JBIG2 error: invalid unknown segment length'); + } + } + segmentHeader.headerEnd = position; + return segmentHeader; + } + + function readSegments(header, data, start, end) { + var segments = []; + var position = start; + while (position < end) { + var segmentHeader = readSegmentHeader(data, position); + position = segmentHeader.headerEnd; + var segment = { + header: segmentHeader, + data: data + }; + if (!header.randomAccess) { + segment.start = position; + position += segmentHeader.length; + segment.end = position; + } + segments.push(segment); + if (segmentHeader.type === 51) { + break; // end of file is found + } + } + if (header.randomAccess) { + for (var i = 0, ii = segments.length; i < ii; i++) { + segments[i].start = position; + position += segments[i].header.length; + segments[i].end = position; + } + } + return segments; + } + + // 7.4.1 Region segment information field + function readRegionSegmentInformation(data, start) { + return { + width: readUint32(data, start), + height: readUint32(data, start + 4), + x: readUint32(data, start + 8), + y: readUint32(data, start + 12), + combinationOperator: data[start + 16] & 7 + }; + } + var RegionSegmentInformationFieldLength = 17; + + function processSegment(segment, visitor) { + var header = segment.header; + + var data = segment.data, position = segment.start, end = segment.end; + var args, at, i, atLength; + switch (header.type) { + case 0: // SymbolDictionary + // 7.4.2 Symbol dictionary segment syntax + var dictionary = {}; + var dictionaryFlags = readUint16(data, position); // 7.4.2.1.1 + dictionary.huffman = !!(dictionaryFlags & 1); + dictionary.refinement = !!(dictionaryFlags & 2); + dictionary.huffmanDHSelector = (dictionaryFlags >> 2) & 3; + dictionary.huffmanDWSelector = (dictionaryFlags >> 4) & 3; + dictionary.bitmapSizeSelector = (dictionaryFlags >> 6) & 1; + dictionary.aggregationInstancesSelector = (dictionaryFlags >> 7) & 1; + dictionary.bitmapCodingContextUsed = !!(dictionaryFlags & 256); + dictionary.bitmapCodingContextRetained = !!(dictionaryFlags & 512); + dictionary.template = (dictionaryFlags >> 10) & 3; + dictionary.refinementTemplate = (dictionaryFlags >> 12) & 1; + position += 2; + if (!dictionary.huffman) { + atLength = dictionary.template === 0 ? 4 : 1; + at = []; + for (i = 0; i < atLength; i++) { + at.push({ + x: readInt8(data, position), + y: readInt8(data, position + 1) + }); + position += 2; + } + dictionary.at = at; + } + if (dictionary.refinement && !dictionary.refinementTemplate) { + at = []; + for (i = 0; i < 2; i++) { + at.push({ + x: readInt8(data, position), + y: readInt8(data, position + 1) + }); + position += 2; + } + dictionary.refinementAt = at; + } + dictionary.numberOfExportedSymbols = readUint32(data, position); + position += 4; + dictionary.numberOfNewSymbols = readUint32(data, position); + position += 4; + args = [dictionary, header.number, header.referredTo, + data, position, end]; + break; + case 6: // ImmediateTextRegion + case 7: // ImmediateLosslessTextRegion + var textRegion = {}; + textRegion.info = readRegionSegmentInformation(data, position); + position += RegionSegmentInformationFieldLength; + var textRegionSegmentFlags = readUint16(data, position); + position += 2; + textRegion.huffman = !!(textRegionSegmentFlags & 1); + textRegion.refinement = !!(textRegionSegmentFlags & 2); + textRegion.stripSize = 1 << ((textRegionSegmentFlags >> 2) & 3); + textRegion.referenceCorner = (textRegionSegmentFlags >> 4) & 3; + textRegion.transposed = !!(textRegionSegmentFlags & 64); + textRegion.combinationOperator = (textRegionSegmentFlags >> 7) & 3; + textRegion.defaultPixelValue = (textRegionSegmentFlags >> 9) & 1; + textRegion.dsOffset = (textRegionSegmentFlags << 17) >> 27; + textRegion.refinementTemplate = (textRegionSegmentFlags >> 15) & 1; + if (textRegion.huffman) { + var textRegionHuffmanFlags = readUint16(data, position); + position += 2; + textRegion.huffmanFS = (textRegionHuffmanFlags) & 3; + textRegion.huffmanDS = (textRegionHuffmanFlags >> 2) & 3; + textRegion.huffmanDT = (textRegionHuffmanFlags >> 4) & 3; + textRegion.huffmanRefinementDW = (textRegionHuffmanFlags >> 6) & 3; + textRegion.huffmanRefinementDH = (textRegionHuffmanFlags >> 8) & 3; + textRegion.huffmanRefinementDX = (textRegionHuffmanFlags >> 10) & 3; + textRegion.huffmanRefinementDY = (textRegionHuffmanFlags >> 12) & 3; + textRegion.huffmanRefinementSizeSelector = + !!(textRegionHuffmanFlags & 14); + } + if (textRegion.refinement && !textRegion.refinementTemplate) { + at = []; + for (i = 0; i < 2; i++) { + at.push({ + x: readInt8(data, position), + y: readInt8(data, position + 1) + }); + position += 2; + } + textRegion.refinementAt = at; + } + textRegion.numberOfSymbolInstances = readUint32(data, position); + position += 4; + // TODO 7.4.3.1.7 Symbol ID Huffman table decoding + if (textRegion.huffman) { + error('JBIG2 error: huffman is not supported'); + } + args = [textRegion, header.referredTo, data, position, end]; + break; + case 38: // ImmediateGenericRegion + case 39: // ImmediateLosslessGenericRegion + var genericRegion = {}; + genericRegion.info = readRegionSegmentInformation(data, position); + position += RegionSegmentInformationFieldLength; + var genericRegionSegmentFlags = data[position++]; + genericRegion.mmr = !!(genericRegionSegmentFlags & 1); + genericRegion.template = (genericRegionSegmentFlags >> 1) & 3; + genericRegion.prediction = !!(genericRegionSegmentFlags & 8); + if (!genericRegion.mmr) { + atLength = genericRegion.template === 0 ? 4 : 1; + at = []; + for (i = 0; i < atLength; i++) { + at.push({ + x: readInt8(data, position), + y: readInt8(data, position + 1) + }); + position += 2; + } + genericRegion.at = at; + } + args = [genericRegion, data, position, end]; + break; + case 48: // PageInformation + var pageInfo = { + width: readUint32(data, position), + height: readUint32(data, position + 4), + resolutionX: readUint32(data, position + 8), + resolutionY: readUint32(data, position + 12) + }; + if (pageInfo.height === 0xFFFFFFFF) { + delete pageInfo.height; + } + var pageSegmentFlags = data[position + 16]; + var pageStripingInformatiom = readUint16(data, position + 17); + pageInfo.lossless = !!(pageSegmentFlags & 1); + pageInfo.refinement = !!(pageSegmentFlags & 2); + pageInfo.defaultPixelValue = (pageSegmentFlags >> 2) & 1; + pageInfo.combinationOperator = (pageSegmentFlags >> 3) & 3; + pageInfo.requiresBuffer = !!(pageSegmentFlags & 32); + pageInfo.combinationOperatorOverride = !!(pageSegmentFlags & 64); + args = [pageInfo]; + break; + case 49: // EndOfPage + break; + case 50: // EndOfStripe + break; + case 51: // EndOfFile + break; + case 62: // 7.4.15 defines 2 extension types which + // are comments and can be ignored. + break; + default: + error('JBIG2 error: segment type ' + header.typeName + '(' + + header.type + ') is not implemented'); + } + var callbackName = 'on' + header.typeName; + if (callbackName in visitor) { + visitor[callbackName].apply(visitor, args); + } + } + + function processSegments(segments, visitor) { + for (var i = 0, ii = segments.length; i < ii; i++) { + processSegment(segments[i], visitor); + } + } + + function parseJbig2(data, start, end) { + var position = start; + if (data[position] !== 0x97 || data[position + 1] !== 0x4A || + data[position + 2] !== 0x42 || data[position + 3] !== 0x32 || + data[position + 4] !== 0x0D || data[position + 5] !== 0x0A || + data[position + 6] !== 0x1A || data[position + 7] !== 0x0A) { + error('JBIG2 error: invalid header'); + } + var header = {}; + position += 8; + var flags = data[position++]; + header.randomAccess = !(flags & 1); + if (!(flags & 2)) { + header.numberOfPages = readUint32(data, position); + position += 4; + } + var segments = readSegments(header, data, position, end); + error('Not implemented'); + // processSegments(segments, new SimpleSegmentVisitor()); + } + + function parseJbig2Chunks(chunks) { + var visitor = new SimpleSegmentVisitor(); + for (var i = 0, ii = chunks.length; i < ii; i++) { + var chunk = chunks[i]; + var segments = readSegments({}, chunk.data, chunk.start, chunk.end); + processSegments(segments, visitor); + } + return visitor.buffer; + } + + function SimpleSegmentVisitor() {} + + SimpleSegmentVisitor.prototype = { + onPageInformation: function SimpleSegmentVisitor_onPageInformation(info) { + this.currentPageInfo = info; + var rowSize = (info.width + 7) >> 3; + var buffer = new Uint8Array(rowSize * info.height); + // The contents of ArrayBuffers are initialized to 0. + // Fill the buffer with 0xFF only if info.defaultPixelValue is set + if (info.defaultPixelValue) { + for (var i = 0, ii = buffer.length; i < ii; i++) { + buffer[i] = 0xFF; + } + } + this.buffer = buffer; + }, + drawBitmap: function SimpleSegmentVisitor_drawBitmap(regionInfo, bitmap) { + var pageInfo = this.currentPageInfo; + var width = regionInfo.width, height = regionInfo.height; + var rowSize = (pageInfo.width + 7) >> 3; + var combinationOperator = pageInfo.combinationOperatorOverride ? + regionInfo.combinationOperator : pageInfo.combinationOperator; + var buffer = this.buffer; + var mask0 = 128 >> (regionInfo.x & 7); + var offset0 = regionInfo.y * rowSize + (regionInfo.x >> 3); + var i, j, mask, offset; + switch (combinationOperator) { + case 0: // OR + for (i = 0; i < height; i++) { + mask = mask0; + offset = offset0; + for (j = 0; j < width; j++) { + if (bitmap[i][j]) { + buffer[offset] |= mask; + } + mask >>= 1; + if (!mask) { + mask = 128; + offset++; + } + } + offset0 += rowSize; + } + break; + case 2: // XOR + for (i = 0; i < height; i++) { + mask = mask0; + offset = offset0; + for (j = 0; j < width; j++) { + if (bitmap[i][j]) { + buffer[offset] ^= mask; + } + mask >>= 1; + if (!mask) { + mask = 128; + offset++; + } + } + offset0 += rowSize; + } + break; + default: + error('JBIG2 error: operator ' + combinationOperator + + ' is not supported'); + } + }, + onImmediateGenericRegion: + function SimpleSegmentVisitor_onImmediateGenericRegion(region, data, + start, end) { + var regionInfo = region.info; + var decodingContext = new DecodingContext(data, start, end); + var bitmap = decodeBitmap(region.mmr, regionInfo.width, regionInfo.height, + region.template, region.prediction, null, + region.at, decodingContext); + this.drawBitmap(regionInfo, bitmap); + }, + onImmediateLosslessGenericRegion: + function SimpleSegmentVisitor_onImmediateLosslessGenericRegion() { + this.onImmediateGenericRegion.apply(this, arguments); + }, + onSymbolDictionary: + function SimpleSegmentVisitor_onSymbolDictionary(dictionary, + currentSegment, + referredSegments, + data, start, end) { + var huffmanTables; + if (dictionary.huffman) { + error('JBIG2 error: huffman is not supported'); + } + + // Combines exported symbols from all referred segments + var symbols = this.symbols; + if (!symbols) { + this.symbols = symbols = {}; + } + + var inputSymbols = []; + for (var i = 0, ii = referredSegments.length; i < ii; i++) { + inputSymbols = inputSymbols.concat(symbols[referredSegments[i]]); + } + + var decodingContext = new DecodingContext(data, start, end); + symbols[currentSegment] = decodeSymbolDictionary(dictionary.huffman, + dictionary.refinement, inputSymbols, dictionary.numberOfNewSymbols, + dictionary.numberOfExportedSymbols, huffmanTables, + dictionary.template, dictionary.at, + dictionary.refinementTemplate, dictionary.refinementAt, + decodingContext); + }, + onImmediateTextRegion: + function SimpleSegmentVisitor_onImmediateTextRegion(region, + referredSegments, + data, start, end) { + var regionInfo = region.info; + var huffmanTables; + + // Combines exported symbols from all referred segments + var symbols = this.symbols; + var inputSymbols = []; + for (var i = 0, ii = referredSegments.length; i < ii; i++) { + inputSymbols = inputSymbols.concat(symbols[referredSegments[i]]); + } + var symbolCodeLength = log2(inputSymbols.length); + + var decodingContext = new DecodingContext(data, start, end); + var bitmap = decodeTextRegion(region.huffman, region.refinement, + regionInfo.width, regionInfo.height, region.defaultPixelValue, + region.numberOfSymbolInstances, region.stripSize, inputSymbols, + symbolCodeLength, region.transposed, region.dsOffset, + region.referenceCorner, region.combinationOperator, huffmanTables, + region.refinementTemplate, region.refinementAt, decodingContext); + this.drawBitmap(regionInfo, bitmap); + }, + onImmediateLosslessTextRegion: + function SimpleSegmentVisitor_onImmediateLosslessTextRegion() { + this.onImmediateTextRegion.apply(this, arguments); + } + }; + + function Jbig2Image() {} + + Jbig2Image.prototype = { + parseChunks: function Jbig2Image_parseChunks(chunks) { + return parseJbig2Chunks(chunks); + } + }; + + return Jbig2Image; +})(); + + +var bidi = PDFJS.bidi = (function bidiClosure() { + // Character types for symbols from 0000 to 00FF. + var baseTypes = [ + 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'S', 'B', 'S', 'WS', + 'B', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', + 'BN', 'BN', 'B', 'B', 'B', 'S', 'WS', 'ON', 'ON', 'ET', 'ET', 'ET', 'ON', + 'ON', 'ON', 'ON', 'ON', 'ON', 'CS', 'ON', 'CS', 'ON', 'EN', 'EN', 'EN', + 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'EN', 'ON', 'ON', 'ON', 'ON', 'ON', + 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', + 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'ON', 'ON', + 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', + 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', + 'L', 'ON', 'ON', 'ON', 'ON', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'B', 'BN', + 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', + 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', 'BN', + 'BN', 'CS', 'ON', 'ET', 'ET', 'ET', 'ET', 'ON', 'ON', 'ON', 'ON', 'L', 'ON', + 'ON', 'ON', 'ON', 'ON', 'ET', 'ET', 'EN', 'EN', 'ON', 'L', 'ON', 'ON', 'ON', + 'EN', 'L', 'ON', 'ON', 'ON', 'ON', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', + 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', + 'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', + 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', + 'L', 'L', 'L', 'ON', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L' + ]; + + // Character types for symbols from 0600 to 06FF + var arabicTypes = [ + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'CS', 'AL', 'ON', 'ON', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', + 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', 'AN', + 'AN', 'ET', 'AN', 'AN', 'AL', 'AL', 'AL', 'NSM', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', + 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'NSM', 'ON', 'NSM', + 'NSM', 'NSM', 'NSM', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', + 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL', 'AL' + ]; + + function isOdd(i) { + return (i & 1) !== 0; + } + + function isEven(i) { + return (i & 1) === 0; + } + + function findUnequal(arr, start, value) { + for (var j = start, jj = arr.length; j < jj; ++j) { + if (arr[j] !== value) { + return j; + } + } + return j; + } + + function setValues(arr, start, end, value) { + for (var j = start; j < end; ++j) { + arr[j] = value; + } + } + + function reverseValues(arr, start, end) { + for (var i = start, j = end - 1; i < j; ++i, --j) { + var temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + } + + function createBidiText(str, isLTR, vertical) { + return { + str: str, + dir: (vertical ? 'ttb' : (isLTR ? 'ltr' : 'rtl')) + }; + } + + // These are used in bidi(), which is called frequently. We re-use them on + // each call to avoid unnecessary allocations. + var chars = []; + var types = []; + + function bidi(str, startLevel, vertical) { + var isLTR = true; + var strLength = str.length; + if (strLength === 0 || vertical) { + return createBidiText(str, isLTR, vertical); + } + + // Get types and fill arrays + chars.length = strLength; + types.length = strLength; + var numBidi = 0; + + var i, ii; + for (i = 0; i < strLength; ++i) { + chars[i] = str.charAt(i); + + var charCode = str.charCodeAt(i); + var charType = 'L'; + if (charCode <= 0x00ff) { + charType = baseTypes[charCode]; + } else if (0x0590 <= charCode && charCode <= 0x05f4) { + charType = 'R'; + } else if (0x0600 <= charCode && charCode <= 0x06ff) { + charType = arabicTypes[charCode & 0xff]; + } else if (0x0700 <= charCode && charCode <= 0x08AC) { + charType = 'AL'; + } + if (charType === 'R' || charType === 'AL' || charType === 'AN') { + numBidi++; + } + types[i] = charType; + } + + // Detect the bidi method + // - If there are no rtl characters then no bidi needed + // - If less than 30% chars are rtl then string is primarily ltr + // - If more than 30% chars are rtl then string is primarily rtl + if (numBidi === 0) { + isLTR = true; + return createBidiText(str, isLTR); + } + + if (startLevel === -1) { + if ((strLength / numBidi) < 0.3) { + isLTR = true; + startLevel = 0; + } else { + isLTR = false; + startLevel = 1; + } + } + + var levels = []; + for (i = 0; i < strLength; ++i) { + levels[i] = startLevel; + } + + /* + X1-X10: skip most of this, since we are NOT doing the embeddings. + */ + var e = (isOdd(startLevel) ? 'R' : 'L'); + var sor = e; + var eor = sor; + + /* + W1. Examine each non-spacing mark (NSM) in the level run, and change the + type of the NSM to the type of the previous character. If the NSM is at the + start of the level run, it will get the type of sor. + */ + var lastType = sor; + for (i = 0; i < strLength; ++i) { + if (types[i] === 'NSM') { + types[i] = lastType; + } else { + lastType = types[i]; + } + } + + /* + W2. Search backwards from each instance of a European number until the + first strong type (R, L, AL, or sor) is found. If an AL is found, change + the type of the European number to Arabic number. + */ + lastType = sor; + var t; + for (i = 0; i < strLength; ++i) { + t = types[i]; + if (t === 'EN') { + types[i] = (lastType === 'AL') ? 'AN' : 'EN'; + } else if (t === 'R' || t === 'L' || t === 'AL') { + lastType = t; + } + } + + /* + W3. Change all ALs to R. + */ + for (i = 0; i < strLength; ++i) { + t = types[i]; + if (t === 'AL') { + types[i] = 'R'; + } + } + + /* + W4. A single European separator between two European numbers changes to a + European number. A single common separator between two numbers of the same + type changes to that type: + */ + for (i = 1; i < strLength - 1; ++i) { + if (types[i] === 'ES' && types[i - 1] === 'EN' && types[i + 1] === 'EN') { + types[i] = 'EN'; + } + if (types[i] === 'CS' && + (types[i - 1] === 'EN' || types[i - 1] === 'AN') && + types[i + 1] === types[i - 1]) { + types[i] = types[i - 1]; + } + } + + /* + W5. A sequence of European terminators adjacent to European numbers changes + to all European numbers: + */ + for (i = 0; i < strLength; ++i) { + if (types[i] === 'EN') { + // do before + var j; + for (j = i - 1; j >= 0; --j) { + if (types[j] !== 'ET') { + break; + } + types[j] = 'EN'; + } + // do after + for (j = i + 1; j < strLength; --j) { + if (types[j] !== 'ET') { + break; + } + types[j] = 'EN'; + } + } + } + + /* + W6. Otherwise, separators and terminators change to Other Neutral: + */ + for (i = 0; i < strLength; ++i) { + t = types[i]; + if (t === 'WS' || t === 'ES' || t === 'ET' || t === 'CS') { + types[i] = 'ON'; + } + } + + /* + W7. Search backwards from each instance of a European number until the + first strong type (R, L, or sor) is found. If an L is found, then change + the type of the European number to L. + */ + lastType = sor; + for (i = 0; i < strLength; ++i) { + t = types[i]; + if (t === 'EN') { + types[i] = ((lastType === 'L') ? 'L' : 'EN'); + } else if (t === 'R' || t === 'L') { + lastType = t; + } + } + + /* + N1. A sequence of neutrals takes the direction of the surrounding strong + text if the text on both sides has the same direction. European and Arabic + numbers are treated as though they were R. Start-of-level-run (sor) and + end-of-level-run (eor) are used at level run boundaries. + */ + for (i = 0; i < strLength; ++i) { + if (types[i] === 'ON') { + var end = findUnequal(types, i + 1, 'ON'); + var before = sor; + if (i > 0) { + before = types[i - 1]; + } + + var after = eor; + if (end + 1 < strLength) { + after = types[end + 1]; + } + if (before !== 'L') { + before = 'R'; + } + if (after !== 'L') { + after = 'R'; + } + if (before === after) { + setValues(types, i, end, before); + } + i = end - 1; // reset to end (-1 so next iteration is ok) + } + } + + /* + N2. Any remaining neutrals take the embedding direction. + */ + for (i = 0; i < strLength; ++i) { + if (types[i] === 'ON') { + types[i] = e; + } + } + + /* + I1. For all characters with an even (left-to-right) embedding direction, + those of type R go up one level and those of type AN or EN go up two + levels. + I2. For all characters with an odd (right-to-left) embedding direction, + those of type L, EN or AN go up one level. + */ + for (i = 0; i < strLength; ++i) { + t = types[i]; + if (isEven(levels[i])) { + if (t === 'R') { + levels[i] += 1; + } else if (t === 'AN' || t === 'EN') { + levels[i] += 2; + } + } else { // isOdd + if (t === 'L' || t === 'AN' || t === 'EN') { + levels[i] += 1; + } + } + } + + /* + L1. On each line, reset the embedding level of the following characters to + the paragraph embedding level: + + segment separators, + paragraph separators, + any sequence of whitespace characters preceding a segment separator or + paragraph separator, and any sequence of white space characters at the end + of the line. + */ + + // don't bother as text is only single line + + /* + L2. From the highest level found in the text to the lowest odd level on + each line, reverse any contiguous sequence of characters that are at that + level or higher. + */ + + // find highest level & lowest odd level + var highestLevel = -1; + var lowestOddLevel = 99; + var level; + for (i = 0, ii = levels.length; i < ii; ++i) { + level = levels[i]; + if (highestLevel < level) { + highestLevel = level; + } + if (lowestOddLevel > level && isOdd(level)) { + lowestOddLevel = level; + } + } + + // now reverse between those limits + for (level = highestLevel; level >= lowestOddLevel; --level) { + // find segments to reverse + var start = -1; + for (i = 0, ii = levels.length; i < ii; ++i) { + if (levels[i] < level) { + if (start >= 0) { + reverseValues(chars, start, i); + start = -1; + } + } else if (start < 0) { + start = i; + } + } + if (start >= 0) { + reverseValues(chars, start, levels.length); + } + } + + /* + L3. Combining marks applied to a right-to-left base character will at this + point precede their base character. If the rendering engine expects them to + follow the base characters in the final display process, then the ordering + of the marks and the base character must be reversed. + */ + + // don't bother for now + + /* + L4. A character that possesses the mirrored property as specified by + Section 4.7, Mirrored, must be depicted by a mirrored glyph if the resolved + directionality of that character is R. + */ + + // don't mirror as characters are already mirrored in the pdf + + // Finally, return string + var result = ''; + for (i = 0, ii = chars.length; i < ii; ++i) { + var ch = chars[i]; + if (ch !== '<' && ch !== '>') { + result += ch; + } + } + return createBidiText(result, isLTR); + } + + return bidi; +})(); + +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +/* Copyright 2014 Opera Software ASA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * Based on https://code.google.com/p/smhasher/wiki/MurmurHash3. + * Hashes roughly 100 KB per millisecond on i7 3.4 GHz. + */ +/* globals Uint32ArrayView */ + +'use strict'; + +var MurmurHash3_64 = (function MurmurHash3_64Closure (seed) { + // Workaround for missing math precison in JS. + var MASK_HIGH = 0xffff0000; + var MASK_LOW = 0xffff; + + function MurmurHash3_64 (seed) { + var SEED = 0xc3d2e1f0; + this.h1 = seed ? seed & 0xffffffff : SEED; + this.h2 = seed ? seed & 0xffffffff : SEED; + } + + var alwaysUseUint32ArrayView = false; + // old webkits have issues with non-aligned arrays + try { + new Uint32Array(new Uint8Array(5).buffer, 0, 1); + } catch (e) { + alwaysUseUint32ArrayView = true; + } + + MurmurHash3_64.prototype = { + update: function MurmurHash3_64_update(input) { + var useUint32ArrayView = alwaysUseUint32ArrayView; + var i; + if (typeof input === 'string') { + var data = new Uint8Array(input.length * 2); + var length = 0; + for (i = 0; i < input.length; i++) { + var code = input.charCodeAt(i); + if (code <= 0xff) { + data[length++] = code; + } + else { + data[length++] = code >>> 8; + data[length++] = code & 0xff; + } + } + } else if (input instanceof Uint8Array) { + data = input; + length = data.length; + } else if (typeof input === 'object' && ('length' in input)) { + // processing regular arrays as well, e.g. for IE9 + data = input; + length = data.length; + useUint32ArrayView = true; + } else { + throw new Error('Wrong data format in MurmurHash3_64_update. ' + + 'Input must be a string or array.'); + } + + var blockCounts = length >> 2; + var tailLength = length - blockCounts * 4; + // we don't care about endianness here + var dataUint32 = useUint32ArrayView ? + new Uint32ArrayView(data, blockCounts) : + new Uint32Array(data.buffer, 0, blockCounts); + var k1 = 0; + var k2 = 0; + var h1 = this.h1; + var h2 = this.h2; + var C1 = 0xcc9e2d51; + var C2 = 0x1b873593; + var C1_LOW = C1 & MASK_LOW; + var C2_LOW = C2 & MASK_LOW; + + for (i = 0; i < blockCounts; i++) { + if (i & 1) { + k1 = dataUint32[i]; + k1 = (k1 * C1 & MASK_HIGH) | (k1 * C1_LOW & MASK_LOW); + k1 = k1 << 15 | k1 >>> 17; + k1 = (k1 * C2 & MASK_HIGH) | (k1 * C2_LOW & MASK_LOW); + h1 ^= k1; + h1 = h1 << 13 | h1 >>> 19; + h1 = h1 * 5 + 0xe6546b64; + } else { + k2 = dataUint32[i]; + k2 = (k2 * C1 & MASK_HIGH) | (k2 * C1_LOW & MASK_LOW); + k2 = k2 << 15 | k2 >>> 17; + k2 = (k2 * C2 & MASK_HIGH) | (k2 * C2_LOW & MASK_LOW); + h2 ^= k2; + h2 = h2 << 13 | h2 >>> 19; + h2 = h2 * 5 + 0xe6546b64; + } + } + + k1 = 0; + + switch (tailLength) { + case 3: + k1 ^= data[blockCounts * 4 + 2] << 16; + /* falls through */ + case 2: + k1 ^= data[blockCounts * 4 + 1] << 8; + /* falls through */ + case 1: + k1 ^= data[blockCounts * 4]; + /* falls through */ + k1 = (k1 * C1 & MASK_HIGH) | (k1 * C1_LOW & MASK_LOW); + k1 = k1 << 15 | k1 >>> 17; + k1 = (k1 * C2 & MASK_HIGH) | (k1 * C2_LOW & MASK_LOW); + if (blockCounts & 1) { + h1 ^= k1; + } else { + h2 ^= k1; + } + } + + this.h1 = h1; + this.h2 = h2; + return this; + }, + + hexdigest: function MurmurHash3_64_hexdigest () { + var h1 = this.h1; + var h2 = this.h2; + + h1 ^= h2 >>> 1; + h1 = (h1 * 0xed558ccd & MASK_HIGH) | (h1 * 0x8ccd & MASK_LOW); + h2 = (h2 * 0xff51afd7 & MASK_HIGH) | + (((h2 << 16 | h1 >>> 16) * 0xafd7ed55 & MASK_HIGH) >>> 16); + h1 ^= h2 >>> 1; + h1 = (h1 * 0x1a85ec53 & MASK_HIGH) | (h1 * 0xec53 & MASK_LOW); + h2 = (h2 * 0xc4ceb9fe & MASK_HIGH) | + (((h2 << 16 | h1 >>> 16) * 0xb9fe1a85 & MASK_HIGH) >>> 16); + h1 ^= h2 >>> 1; + + for (var i = 0, arr = [h1, h2], str = ''; i < arr.length; i++) { + var hex = (arr[i] >>> 0).toString(16); + while (hex.length < 8) { + hex = '0' + hex; + } + str += hex; + } + + return str; + } + }; + + return MurmurHash3_64; +})(); + + +}).call((typeof window === 'undefined') ? this : window); + +if (!PDFJS.workerSrc && typeof document !== 'undefined') { + // workerSrc is not set -- using last script url to define default location + PDFJS.workerSrc = (function () { + 'use strict'; + var scriptTagContainer = document.body || + document.getElementsByTagName('head')[0]; + var pdfjsSrc = scriptTagContainer.lastChild.src; + return pdfjsSrc && pdfjsSrc.replace(/\.js$/i, '.worker.js'); + })(); +} + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-EUC-H.bcmap new file mode 100644 index 0000000..2655fc7 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-EUC-V.bcmap new file mode 100644 index 0000000..f1ed853 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-H.bcmap new file mode 100644 index 0000000..39e89d3 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-RKSJ-H.bcmap new file mode 100644 index 0000000..e4167cb Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-RKSJ-V.bcmap new file mode 100644 index 0000000..50b1646 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-V.bcmap new file mode 100644 index 0000000..d7af99b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78ms-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78ms-RKSJ-H.bcmap new file mode 100644 index 0000000..37077d0 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78ms-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78ms-RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78ms-RKSJ-V.bcmap new file mode 100644 index 0000000..acf2323 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/78ms-RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/83pv-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/83pv-RKSJ-H.bcmap new file mode 100644 index 0000000..2359bc5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/83pv-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90ms-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90ms-RKSJ-H.bcmap new file mode 100644 index 0000000..af82938 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90ms-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90ms-RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90ms-RKSJ-V.bcmap new file mode 100644 index 0000000..780549d Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90ms-RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90msp-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90msp-RKSJ-H.bcmap new file mode 100644 index 0000000..bfd3119 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90msp-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90msp-RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90msp-RKSJ-V.bcmap new file mode 100644 index 0000000..25ef14a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90msp-RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90pv-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90pv-RKSJ-H.bcmap new file mode 100644 index 0000000..02f713b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90pv-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90pv-RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90pv-RKSJ-V.bcmap new file mode 100644 index 0000000..d08e0cc Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/90pv-RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-H.bcmap new file mode 100644 index 0000000..59442ac Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-RKSJ-H.bcmap new file mode 100644 index 0000000..a3065e4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-RKSJ-V.bcmap new file mode 100644 index 0000000..040014c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-V.bcmap new file mode 100644 index 0000000..2f816d3 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Add-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-0.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-0.bcmap new file mode 100644 index 0000000..88ec04a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-0.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-1.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-1.bcmap new file mode 100644 index 0000000..03a5014 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-1.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-2.bcmap new file mode 100644 index 0000000..2aa9514 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-3.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-3.bcmap new file mode 100644 index 0000000..86d8b8c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-3.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-4.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-4.bcmap new file mode 100644 index 0000000..f50fc6c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-4.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-5.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-5.bcmap new file mode 100644 index 0000000..6caf4a8 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-5.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-6.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-6.bcmap new file mode 100644 index 0000000..b77fb07 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-6.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-UCS2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-UCS2.bcmap new file mode 100644 index 0000000..69d79a2 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-CNS1-UCS2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-0.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-0.bcmap new file mode 100644 index 0000000..3610108 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-0.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-1.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-1.bcmap new file mode 100644 index 0000000..707bb10 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-1.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-2.bcmap new file mode 100644 index 0000000..f7648cc Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-3.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-3.bcmap new file mode 100644 index 0000000..8521458 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-3.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-4.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-4.bcmap new file mode 100644 index 0000000..e40c63a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-4.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-5.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-5.bcmap new file mode 100644 index 0000000..d7623b5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-5.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-UCS2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-UCS2.bcmap new file mode 100644 index 0000000..7586525 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-GB1-UCS2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-0.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-0.bcmap new file mode 100644 index 0000000..f0e94ec Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-0.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-1.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-1.bcmap new file mode 100644 index 0000000..dad42c5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-1.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-2.bcmap new file mode 100644 index 0000000..090819a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-3.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-3.bcmap new file mode 100644 index 0000000..087dfc1 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-3.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-4.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-4.bcmap new file mode 100644 index 0000000..46aa9bf Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-4.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-5.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-5.bcmap new file mode 100644 index 0000000..5b4b65c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-5.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-6.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-6.bcmap new file mode 100644 index 0000000..e77d699 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-6.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-UCS2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-UCS2.bcmap new file mode 100644 index 0000000..128a141 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Japan1-UCS2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-0.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-0.bcmap new file mode 100644 index 0000000..cef1a99 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-0.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-1.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-1.bcmap new file mode 100644 index 0000000..11ffa36 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-1.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-2.bcmap new file mode 100644 index 0000000..3172308 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-UCS2.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-UCS2.bcmap new file mode 100644 index 0000000..f3371c0 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Adobe-Korea1-UCS2.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5-H.bcmap new file mode 100644 index 0000000..beb4d22 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5-V.bcmap new file mode 100644 index 0000000..2d4f87d Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5pc-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5pc-H.bcmap new file mode 100644 index 0000000..ce00131 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5pc-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5pc-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5pc-V.bcmap new file mode 100644 index 0000000..73b99ff Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/B5pc-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS-EUC-H.bcmap new file mode 100644 index 0000000..61d1d0c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS-EUC-V.bcmap new file mode 100644 index 0000000..1a393a5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS1-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS1-H.bcmap new file mode 100644 index 0000000..f738e21 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS1-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS1-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS1-V.bcmap new file mode 100644 index 0000000..9c3169f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS1-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS2-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS2-H.bcmap new file mode 100644 index 0000000..c89b352 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS2-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS2-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS2-V.bcmap new file mode 100644 index 0000000..7588cec --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/CNS2-V.bcmap @@ -0,0 +1,3 @@ +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSECNS2-H \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETHK-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETHK-B5-H.bcmap new file mode 100644 index 0000000..cb29415 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETHK-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETHK-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETHK-B5-V.bcmap new file mode 100644 index 0000000..f09aec6 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETHK-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETen-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETen-B5-H.bcmap new file mode 100644 index 0000000..c2d7746 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETen-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETen-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETen-B5-V.bcmap new file mode 100644 index 0000000..89bff15 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETen-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETenms-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETenms-B5-H.bcmap new file mode 100644 index 0000000..a7d69db --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETenms-B5-H.bcmap @@ -0,0 +1,3 @@ +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSE ETen-B5-H` ^ \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETenms-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETenms-B5-V.bcmap new file mode 100644 index 0000000..adc5d61 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/ETenms-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/EUC-H.bcmap new file mode 100644 index 0000000..e92ea5b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/EUC-V.bcmap new file mode 100644 index 0000000..7a7c183 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-H.bcmap new file mode 100644 index 0000000..3b5cde4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-RKSJ-H.bcmap new file mode 100644 index 0000000..ea4d2d9 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-RKSJ-V.bcmap new file mode 100644 index 0000000..3457c27 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-V.bcmap new file mode 100644 index 0000000..4999ca4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Ext-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-EUC-H.bcmap new file mode 100644 index 0000000..e39908b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-EUC-V.bcmap new file mode 100644 index 0000000..d5be544 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-H.bcmap new file mode 100644 index 0000000..39189c5 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-H.bcmap @@ -0,0 +1,4 @@ +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. +See ./LICENSE!!]aX!!]`21> p z$]"Rd-U7* 4%+ Z {/%<9Kb1]." `],"] +"]h"]F"]$"]"]`"]>"]"]z"]X"]6"]"]r"]P"]."] "]j"]H"]&"]"]b"]@"]"]|"]Z"]8"]"]t"]R"]0"]"]l"]J"]("]"]d"]B"] "X~']W"]5"]"]q"]O"]-"] "]i"]G"]%"]"]a"]?"]"]{"]Y"]7"]"]s"]Q"]/"] "]k"]I"]'"]"]c"]A"]"]}"]["]9 \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-V.bcmap new file mode 100644 index 0000000..3108345 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GB-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK-EUC-H.bcmap new file mode 100644 index 0000000..05fff7e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK-EUC-V.bcmap new file mode 100644 index 0000000..0cdf6be Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK2K-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK2K-H.bcmap new file mode 100644 index 0000000..46f6ba5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK2K-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK2K-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK2K-V.bcmap new file mode 100644 index 0000000..d9a9479 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBK2K-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBKp-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBKp-EUC-H.bcmap new file mode 100644 index 0000000..5cb0af6 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBKp-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBKp-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBKp-EUC-V.bcmap new file mode 100644 index 0000000..bca93b8 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBKp-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-EUC-H.bcmap new file mode 100644 index 0000000..4b4e2d3 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-EUC-V.bcmap new file mode 100644 index 0000000..38f7066 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-H.bcmap new file mode 100644 index 0000000..8437ac3 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-V.bcmap new file mode 100644 index 0000000..697ab4a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBT-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBTpc-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBTpc-EUC-H.bcmap new file mode 100644 index 0000000..f6e50e8 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBTpc-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBTpc-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBTpc-EUC-V.bcmap new file mode 100644 index 0000000..6c0d71a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBTpc-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBpc-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBpc-EUC-H.bcmap new file mode 100644 index 0000000..c9edf67 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBpc-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBpc-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBpc-EUC-V.bcmap new file mode 100644 index 0000000..31450c9 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/GBpc-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/H.bcmap new file mode 100644 index 0000000..7b24ea4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdla-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdla-B5-H.bcmap new file mode 100644 index 0000000..7d30c05 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdla-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdla-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdla-B5-V.bcmap new file mode 100644 index 0000000..7894694 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdla-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdlb-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdlb-B5-H.bcmap new file mode 100644 index 0000000..d829a23 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdlb-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdlb-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdlb-B5-V.bcmap new file mode 100644 index 0000000..2b572b5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKdlb-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKgccs-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKgccs-B5-H.bcmap new file mode 100644 index 0000000..971a4f2 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKgccs-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKgccs-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKgccs-B5-V.bcmap new file mode 100644 index 0000000..d353ca2 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKgccs-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm314-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm314-B5-H.bcmap new file mode 100644 index 0000000..576dc01 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm314-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm314-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm314-B5-V.bcmap new file mode 100644 index 0000000..0e96d0e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm314-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm471-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm471-B5-H.bcmap new file mode 100644 index 0000000..11d170c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm471-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm471-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm471-B5-V.bcmap new file mode 100644 index 0000000..54959bf Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKm471-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKscs-B5-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKscs-B5-H.bcmap new file mode 100644 index 0000000..6ef7857 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKscs-B5-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKscs-B5-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKscs-B5-V.bcmap new file mode 100644 index 0000000..1fb2fa2 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/HKscs-B5-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Hankaku.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Hankaku.bcmap new file mode 100644 index 0000000..4b8ec7f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Hankaku.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Hiragana.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Hiragana.bcmap new file mode 100644 index 0000000..17e983e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Hiragana.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-EUC-H.bcmap new file mode 100644 index 0000000..a45c65f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-EUC-V.bcmap new file mode 100644 index 0000000..0e7b21f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-H.bcmap new file mode 100644 index 0000000..b9b22b6 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-Johab-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-Johab-H.bcmap new file mode 100644 index 0000000..2531ffc Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-Johab-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-Johab-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-Johab-V.bcmap new file mode 100644 index 0000000..367ceb2 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-Johab-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-V.bcmap new file mode 100644 index 0000000..6ae2f0b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-H.bcmap new file mode 100644 index 0000000..a8d4240 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-HW-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-HW-H.bcmap new file mode 100644 index 0000000..8b4ae18 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-HW-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-HW-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-HW-V.bcmap new file mode 100644 index 0000000..b655dbc Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-HW-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-V.bcmap new file mode 100644 index 0000000..21f97f6 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCms-UHC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCpc-EUC-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCpc-EUC-H.bcmap new file mode 100644 index 0000000..e06f361 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCpc-EUC-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCpc-EUC-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCpc-EUC-V.bcmap new file mode 100644 index 0000000..f3c9113 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/KSCpc-EUC-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Katakana.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Katakana.bcmap new file mode 100644 index 0000000..524303c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Katakana.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/LICENSE b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/LICENSE new file mode 100644 index 0000000..b1ad168 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/LICENSE @@ -0,0 +1,36 @@ +%%Copyright: ----------------------------------------------------------- +%%Copyright: Copyright 1990-2009 Adobe Systems Incorporated. +%%Copyright: All rights reserved. +%%Copyright: +%%Copyright: Redistribution and use in source and binary forms, with or +%%Copyright: without modification, are permitted provided that the +%%Copyright: following conditions are met: +%%Copyright: +%%Copyright: Redistributions of source code must retain the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer. +%%Copyright: +%%Copyright: Redistributions in binary form must reproduce the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer in the documentation and/or other materials +%%Copyright: provided with the distribution. +%%Copyright: +%%Copyright: Neither the name of Adobe Systems Incorporated nor the names +%%Copyright: of its contributors may be used to endorse or promote +%%Copyright: products derived from this software without specific prior +%%Copyright: written permission. +%%Copyright: +%%Copyright: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +%%Copyright: CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +%%Copyright: INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +%%Copyright: MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +%%Copyright: DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +%%Copyright: CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +%%Copyright: SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +%%Copyright: NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +%%Copyright: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +%%Copyright: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +%%Copyright: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +%%Copyright: OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +%%Copyright: SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +%%Copyright: ----------------------------------------------------------- diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/NWP-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/NWP-H.bcmap new file mode 100644 index 0000000..afc5e4b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/NWP-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/NWP-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/NWP-V.bcmap new file mode 100644 index 0000000..bb5785e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/NWP-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/RKSJ-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/RKSJ-H.bcmap new file mode 100644 index 0000000..fb8d298 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/RKSJ-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/RKSJ-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/RKSJ-V.bcmap new file mode 100644 index 0000000..a2555a6 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/RKSJ-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Roman.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Roman.bcmap new file mode 100644 index 0000000..f896dcf Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/Roman.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UCS2-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UCS2-H.bcmap new file mode 100644 index 0000000..d5db27c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UCS2-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UCS2-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UCS2-V.bcmap new file mode 100644 index 0000000..1dc9b7a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UCS2-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF16-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF16-H.bcmap new file mode 100644 index 0000000..961afef Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF16-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF16-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF16-V.bcmap new file mode 100644 index 0000000..df0cffe Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF16-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF32-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF32-H.bcmap new file mode 100644 index 0000000..1ab18a1 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF32-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF32-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF32-V.bcmap new file mode 100644 index 0000000..ad14662 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF32-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF8-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF8-H.bcmap new file mode 100644 index 0000000..83c6bd7 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF8-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF8-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF8-V.bcmap new file mode 100644 index 0000000..22a27e4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniCNS-UTF8-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UCS2-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UCS2-H.bcmap new file mode 100644 index 0000000..5bd6228 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UCS2-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UCS2-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UCS2-V.bcmap new file mode 100644 index 0000000..53c534b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UCS2-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF16-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF16-H.bcmap new file mode 100644 index 0000000..b95045b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF16-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF16-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF16-V.bcmap new file mode 100644 index 0000000..51f023e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF16-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF32-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF32-H.bcmap new file mode 100644 index 0000000..f0dbd14 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF32-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF32-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF32-V.bcmap new file mode 100644 index 0000000..ce9c30a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF32-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF8-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF8-H.bcmap new file mode 100644 index 0000000..982ca46 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF8-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF8-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF8-V.bcmap new file mode 100644 index 0000000..f78020d Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniGB-UTF8-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-H.bcmap new file mode 100644 index 0000000..7daf56a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-HW-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-HW-H.bcmap new file mode 100644 index 0000000..ac9975c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-HW-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-HW-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-HW-V.bcmap new file mode 100644 index 0000000..3da0a1c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-HW-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-V.bcmap new file mode 100644 index 0000000..c50b9dd Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UCS2-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF16-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF16-H.bcmap new file mode 100644 index 0000000..6761344 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF16-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF16-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF16-V.bcmap new file mode 100644 index 0000000..70bf90c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF16-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF32-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF32-H.bcmap new file mode 100644 index 0000000..7a83d53 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF32-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF32-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF32-V.bcmap new file mode 100644 index 0000000..7a87135 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF32-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF8-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF8-H.bcmap new file mode 100644 index 0000000..9f0334c Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF8-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF8-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF8-V.bcmap new file mode 100644 index 0000000..808a94f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS-UTF8-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF16-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF16-H.bcmap new file mode 100644 index 0000000..d768bf8 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF16-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF16-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF16-V.bcmap new file mode 100644 index 0000000..3d5bf6f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF16-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF32-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF32-H.bcmap new file mode 100644 index 0000000..09eee10 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF32-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF32-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF32-V.bcmap new file mode 100644 index 0000000..6c54600 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF32-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF8-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF8-H.bcmap new file mode 100644 index 0000000..1b1a64f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF8-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF8-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF8-V.bcmap new file mode 100644 index 0000000..994aa9e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJIS2004-UTF8-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UCS2-HW-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UCS2-HW-V.bcmap new file mode 100644 index 0000000..643f921 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UCS2-HW-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UCS2-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UCS2-V.bcmap new file mode 100644 index 0000000..c148f67 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UCS2-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UTF8-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UTF8-V.bcmap new file mode 100644 index 0000000..1849d80 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISPro-UTF8-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX0213-UTF32-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX0213-UTF32-H.bcmap new file mode 100644 index 0000000..a83a677 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX0213-UTF32-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX0213-UTF32-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX0213-UTF32-V.bcmap new file mode 100644 index 0000000..f527248 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX0213-UTF32-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX02132004-UTF32-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX02132004-UTF32-H.bcmap new file mode 100644 index 0000000..e1a988d Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX02132004-UTF32-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX02132004-UTF32-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX02132004-UTF32-V.bcmap new file mode 100644 index 0000000..47e054a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniJISX02132004-UTF32-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UCS2-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UCS2-H.bcmap new file mode 100644 index 0000000..b5b9485 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UCS2-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UCS2-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UCS2-V.bcmap new file mode 100644 index 0000000..026adca Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UCS2-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF16-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF16-H.bcmap new file mode 100644 index 0000000..fd4e66e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF16-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF16-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF16-V.bcmap new file mode 100644 index 0000000..075efb7 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF16-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF32-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF32-H.bcmap new file mode 100644 index 0000000..769d214 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF32-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF32-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF32-V.bcmap new file mode 100644 index 0000000..bdab208 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF32-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF8-H.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF8-H.bcmap new file mode 100644 index 0000000..6ff8674 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF8-H.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF8-V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF8-V.bcmap new file mode 100644 index 0000000..8dfa76a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/UniKS-UTF8-V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/V.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/V.bcmap new file mode 100644 index 0000000..fdec990 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/V.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/WP-Symbol.bcmap b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/WP-Symbol.bcmap new file mode 100644 index 0000000..46729bb Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/cmaps/WP-Symbol.bcmap differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/compatibility.js b/test-module-system/test-system-biz/src/main/resources/static/generic/web/compatibility.js new file mode 100644 index 0000000..06f54bf --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/compatibility.js @@ -0,0 +1,577 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* globals VBArray, PDFJS */ + +'use strict'; + +// Initializing PDFJS global object here, it case if we need to change/disable +// some PDF.js features, e.g. range requests +if (typeof PDFJS === 'undefined') { + (typeof window !== 'undefined' ? window : this).PDFJS = {}; +} + +// Checking if the typed arrays are supported +// Support: iOS<6.0 (subarray), IE<10, Android<4.0 +(function checkTypedArrayCompatibility() { + if (typeof Uint8Array !== 'undefined') { + // Support: iOS<6.0 + if (typeof Uint8Array.prototype.subarray === 'undefined') { + Uint8Array.prototype.subarray = function subarray(start, end) { + return new Uint8Array(this.slice(start, end)); + }; + Float32Array.prototype.subarray = function subarray(start, end) { + return new Float32Array(this.slice(start, end)); + }; + } + + // Support: Android<4.1 + if (typeof Float64Array === 'undefined') { + window.Float64Array = Float32Array; + } + return; + } + + function subarray(start, end) { + return new TypedArray(this.slice(start, end)); + } + + function setArrayOffset(array, offset) { + if (arguments.length < 2) { + offset = 0; + } + for (var i = 0, n = array.length; i < n; ++i, ++offset) { + this[offset] = array[i] & 0xFF; + } + } + + function TypedArray(arg1) { + var result, i, n; + if (typeof arg1 === 'number') { + result = []; + for (i = 0; i < arg1; ++i) { + result[i] = 0; + } + } else if ('slice' in arg1) { + result = arg1.slice(0); + } else { + result = []; + for (i = 0, n = arg1.length; i < n; ++i) { + result[i] = arg1[i]; + } + } + + result.subarray = subarray; + result.buffer = result; + result.byteLength = result.length; + result.set = setArrayOffset; + + if (typeof arg1 === 'object' && arg1.buffer) { + result.buffer = arg1.buffer; + } + return result; + } + + window.Uint8Array = TypedArray; + window.Int8Array = TypedArray; + + // we don't need support for set, byteLength for 32-bit array + // so we can use the TypedArray as well + window.Uint32Array = TypedArray; + window.Int32Array = TypedArray; + window.Uint16Array = TypedArray; + window.Float32Array = TypedArray; + window.Float64Array = TypedArray; +})(); + +// URL = URL || webkitURL +// Support: Safari<7, Android 4.2+ +(function normalizeURLObject() { + if (!window.URL) { + window.URL = window.webkitURL; + } +})(); + +// Object.defineProperty()? +// Support: Android<4.0, Safari<5.1 +(function checkObjectDefinePropertyCompatibility() { + if (typeof Object.defineProperty !== 'undefined') { + var definePropertyPossible = true; + try { + // some browsers (e.g. safari) cannot use defineProperty() on DOM objects + // and thus the native version is not sufficient + Object.defineProperty(new Image(), 'id', { value: 'test' }); + // ... another test for android gb browser for non-DOM objects + var Test = function Test() {}; + Test.prototype = { get id() { } }; + Object.defineProperty(new Test(), 'id', + { value: '', configurable: true, enumerable: true, writable: false }); + } catch (e) { + definePropertyPossible = false; + } + if (definePropertyPossible) { + return; + } + } + + Object.defineProperty = function objectDefineProperty(obj, name, def) { + delete obj[name]; + if ('get' in def) { + obj.__defineGetter__(name, def['get']); + } + if ('set' in def) { + obj.__defineSetter__(name, def['set']); + } + if ('value' in def) { + obj.__defineSetter__(name, function objectDefinePropertySetter(value) { + this.__defineGetter__(name, function objectDefinePropertyGetter() { + return value; + }); + return value; + }); + obj[name] = def.value; + } + }; +})(); + + +// No XMLHttpRequest#response? +// Support: IE<11, Android <4.0 +(function checkXMLHttpRequestResponseCompatibility() { + var xhrPrototype = XMLHttpRequest.prototype; + var xhr = new XMLHttpRequest(); + if (!('overrideMimeType' in xhr)) { + // IE10 might have response, but not overrideMimeType + // Support: IE10 + Object.defineProperty(xhrPrototype, 'overrideMimeType', { + value: function xmlHttpRequestOverrideMimeType(mimeType) {} + }); + } + if ('responseType' in xhr) { + return; + } + + // The worker will be using XHR, so we can save time and disable worker. + PDFJS.disableWorker = true; + + Object.defineProperty(xhrPrototype, 'responseType', { + get: function xmlHttpRequestGetResponseType() { + return this._responseType || 'text'; + }, + set: function xmlHttpRequestSetResponseType(value) { + if (value === 'text' || value === 'arraybuffer') { + this._responseType = value; + if (value === 'arraybuffer' && + typeof this.overrideMimeType === 'function') { + this.overrideMimeType('text/plain; charset=x-user-defined'); + } + } + } + }); + + // Support: IE9 + if (typeof VBArray !== 'undefined') { + Object.defineProperty(xhrPrototype, 'response', { + get: function xmlHttpRequestResponseGet() { + if (this.responseType === 'arraybuffer') { + return new Uint8Array(new VBArray(this.responseBody).toArray()); + } else { + return this.responseText; + } + } + }); + return; + } + + Object.defineProperty(xhrPrototype, 'response', { + get: function xmlHttpRequestResponseGet() { + if (this.responseType !== 'arraybuffer') { + return this.responseText; + } + var text = this.responseText; + var i, n = text.length; + var result = new Uint8Array(n); + for (i = 0; i < n; ++i) { + result[i] = text.charCodeAt(i) & 0xFF; + } + return result.buffer; + } + }); +})(); + +// window.btoa (base64 encode function) ? +// Support: IE<10 +(function checkWindowBtoaCompatibility() { + if ('btoa' in window) { + return; + } + + var digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + window.btoa = function windowBtoa(chars) { + var buffer = ''; + var i, n; + for (i = 0, n = chars.length; i < n; i += 3) { + var b1 = chars.charCodeAt(i) & 0xFF; + var b2 = chars.charCodeAt(i + 1) & 0xFF; + var b3 = chars.charCodeAt(i + 2) & 0xFF; + var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); + var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; + var d4 = i + 2 < n ? (b3 & 0x3F) : 64; + buffer += (digits.charAt(d1) + digits.charAt(d2) + + digits.charAt(d3) + digits.charAt(d4)); + } + return buffer; + }; +})(); + +// window.atob (base64 encode function)? +// Support: IE<10 +(function checkWindowAtobCompatibility() { + if ('atob' in window) { + return; + } + + // https://github.com/davidchambers/Base64.js + var digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + window.atob = function (input) { + input = input.replace(/=+$/, ''); + if (input.length % 4 === 1) { + throw new Error('bad atob input'); + } + for ( + // initialize result and counters + var bc = 0, bs, buffer, idx = 0, output = ''; + // get next character + buffer = input.charAt(idx++); + // character found in table? + // initialize bit storage and add its ascii value + ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, + // and if not first of each 4 characters, + // convert the first 8 bits to one ascii character + bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 + ) { + // try to find character in table (0-63, not found => -1) + buffer = digits.indexOf(buffer); + } + return output; + }; +})(); + +// Function.prototype.bind? +// Support: Android<4.0, iOS<6.0 +(function checkFunctionPrototypeBindCompatibility() { + if (typeof Function.prototype.bind !== 'undefined') { + return; + } + + Function.prototype.bind = function functionPrototypeBind(obj) { + var fn = this, headArgs = Array.prototype.slice.call(arguments, 1); + var bound = function functionPrototypeBindBound() { + var args = headArgs.concat(Array.prototype.slice.call(arguments)); + return fn.apply(obj, args); + }; + return bound; + }; +})(); + +// HTMLElement dataset property +// Support: IE<11, Safari<5.1, Android<4.0 +(function checkDatasetProperty() { + var div = document.createElement('div'); + if ('dataset' in div) { + return; // dataset property exists + } + + Object.defineProperty(HTMLElement.prototype, 'dataset', { + get: function() { + if (this._dataset) { + return this._dataset; + } + + var dataset = {}; + for (var j = 0, jj = this.attributes.length; j < jj; j++) { + var attribute = this.attributes[j]; + if (attribute.name.substring(0, 5) !== 'data-') { + continue; + } + var key = attribute.name.substring(5).replace(/\-([a-z])/g, + function(all, ch) { + return ch.toUpperCase(); + }); + dataset[key] = attribute.value; + } + + Object.defineProperty(this, '_dataset', { + value: dataset, + writable: false, + enumerable: false + }); + return dataset; + }, + enumerable: true + }); +})(); + +// HTMLElement classList property +// Support: IE<10, Android<4.0, iOS<5.0 +(function checkClassListProperty() { + var div = document.createElement('div'); + if ('classList' in div) { + return; // classList property exists + } + + function changeList(element, itemName, add, remove) { + var s = element.className || ''; + var list = s.split(/\s+/g); + if (list[0] === '') { + list.shift(); + } + var index = list.indexOf(itemName); + if (index < 0 && add) { + list.push(itemName); + } + if (index >= 0 && remove) { + list.splice(index, 1); + } + element.className = list.join(' '); + return (index >= 0); + } + + var classListPrototype = { + add: function(name) { + changeList(this.element, name, true, false); + }, + contains: function(name) { + return changeList(this.element, name, false, false); + }, + remove: function(name) { + changeList(this.element, name, false, true); + }, + toggle: function(name) { + changeList(this.element, name, true, true); + } + }; + + Object.defineProperty(HTMLElement.prototype, 'classList', { + get: function() { + if (this._classList) { + return this._classList; + } + + var classList = Object.create(classListPrototype, { + element: { + value: this, + writable: false, + enumerable: true + } + }); + Object.defineProperty(this, '_classList', { + value: classList, + writable: false, + enumerable: false + }); + return classList; + }, + enumerable: true + }); +})(); + +// Check console compatibility +// In older IE versions the console object is not available +// unless console is open. +// Support: IE<10 +(function checkConsoleCompatibility() { + if (!('console' in window)) { + window.console = { + log: function() {}, + error: function() {}, + warn: function() {} + }; + } else if (!('bind' in console.log)) { + // native functions in IE9 might not have bind + console.log = (function(fn) { + return function(msg) { return fn(msg); }; + })(console.log); + console.error = (function(fn) { + return function(msg) { return fn(msg); }; + })(console.error); + console.warn = (function(fn) { + return function(msg) { return fn(msg); }; + })(console.warn); + } +})(); + +// Check onclick compatibility in Opera +// Support: Opera<15 +(function checkOnClickCompatibility() { + // workaround for reported Opera bug DSK-354448: + // onclick fires on disabled buttons with opaque content + function ignoreIfTargetDisabled(event) { + if (isDisabled(event.target)) { + event.stopPropagation(); + } + } + function isDisabled(node) { + return node.disabled || (node.parentNode && isDisabled(node.parentNode)); + } + if (navigator.userAgent.indexOf('Opera') !== -1) { + // use browser detection since we cannot feature-check this bug + document.addEventListener('click', ignoreIfTargetDisabled, true); + } +})(); + +// Checks if possible to use URL.createObjectURL() +// Support: IE +(function checkOnBlobSupport() { + // sometimes IE loosing the data created with createObjectURL(), see #3977 + if (navigator.userAgent.indexOf('Trident') >= 0) { + PDFJS.disableCreateObjectURL = true; + } +})(); + +// Checks if navigator.language is supported +(function checkNavigatorLanguage() { + if ('language' in navigator) { + return; + } + PDFJS.locale = navigator.userLanguage || 'en-US'; +})(); + +(function checkRangeRequests() { + // Safari has issues with cached range requests see: + // https://github.com/mozilla/pdf.js/issues/3260 + // Last tested with version 6.0.4. + // Support: Safari 6.0+ + var isSafari = Object.prototype.toString.call( + window.HTMLElement).indexOf('Constructor') > 0; + + // Older versions of Android (pre 3.0) has issues with range requests, see: + // https://github.com/mozilla/pdf.js/issues/3381. + // Make sure that we only match webkit-based Android browsers, + // since Firefox/Fennec works as expected. + // Support: Android<3.0 + var regex = /Android\s[0-2][^\d]/; + var isOldAndroid = regex.test(navigator.userAgent); + + // Range requests are broken in Chrome 39 and 40, https://crbug.com/442318 + var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent); + + if (isSafari || isOldAndroid || isChromeWithRangeBug) { + PDFJS.disableRange = true; + PDFJS.disableStream = true; + } +})(); + +// Check if the browser supports manipulation of the history. +// Support: IE<10, Android<4.2 +(function checkHistoryManipulation() { + // Android 2.x has so buggy pushState support that it was removed in + // Android 3.0 and restored as late as in Android 4.2. + // Support: Android 2.x + if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) { + PDFJS.disableHistory = true; + } +})(); + +// Support: IE<11, Chrome<21, Android<4.4, Safari<6 +(function checkSetPresenceInImageData() { + // IE < 11 will use window.CanvasPixelArray which lacks set function. + if (window.CanvasPixelArray) { + if (typeof window.CanvasPixelArray.prototype.set !== 'function') { + window.CanvasPixelArray.prototype.set = function(arr) { + for (var i = 0, ii = this.length; i < ii; i++) { + this[i] = arr[i]; + } + }; + } + } else { + // Old Chrome and Android use an inaccessible CanvasPixelArray prototype. + // Because we cannot feature detect it, we rely on user agent parsing. + var polyfill = false, versionMatch; + if (navigator.userAgent.indexOf('Chrom') >= 0) { + versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + // Chrome < 21 lacks the set function. + polyfill = versionMatch && parseInt(versionMatch[2]) < 21; + } else if (navigator.userAgent.indexOf('Android') >= 0) { + // Android < 4.4 lacks the set function. + // Android >= 4.4 will contain Chrome in the user agent, + // thus pass the Chrome check above and not reach this block. + polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent); + } else if (navigator.userAgent.indexOf('Safari') >= 0) { + versionMatch = navigator.userAgent. + match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//); + // Safari < 6 lacks the set function. + polyfill = versionMatch && parseInt(versionMatch[1]) < 6; + } + + if (polyfill) { + var contextPrototype = window.CanvasRenderingContext2D.prototype; + contextPrototype._createImageData = contextPrototype.createImageData; + contextPrototype.createImageData = function(w, h) { + var imageData = this._createImageData(w, h); + imageData.data.set = function(arr) { + for (var i = 0, ii = this.length; i < ii; i++) { + this[i] = arr[i]; + } + }; + return imageData; + }; + } + } +})(); + +// Support: IE<10, Android<4.0, iOS +(function checkRequestAnimationFrame() { + function fakeRequestAnimationFrame(callback) { + window.setTimeout(callback, 20); + } + + var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + if (isIOS) { + // requestAnimationFrame on iOS is broken, replacing with fake one. + window.requestAnimationFrame = fakeRequestAnimationFrame; + return; + } + if ('requestAnimationFrame' in window) { + return; + } + window.requestAnimationFrame = + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + fakeRequestAnimationFrame; +})(); + +(function checkCanvasSizeLimitation() { + var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + var isAndroid = /Android/g.test(navigator.userAgent); + if (isIOS || isAndroid) { + // 5MP + PDFJS.maxCanvasPixels = 5242880; + } +})(); + +// Disable fullscreen support for certain problematic configurations. +// Support: IE11+ (when embedded). +(function checkFullscreenSupport() { + var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 && + window.parent !== window); + if (isEmbeddedIE) { + PDFJS.disableFullscreen = true; + } +})(); diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/compressed.tracemonkey-pldi-09.pdf b/test-module-system/test-system-biz/src/main/resources/static/generic/web/compressed.tracemonkey-pldi-09.pdf new file mode 100644 index 0000000..6557018 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/compressed.tracemonkey-pldi-09.pdf differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/debugger.js b/test-module-system/test-system-biz/src/main/resources/static/generic/web/debugger.js new file mode 100644 index 0000000..046fd34 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/debugger.js @@ -0,0 +1,620 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* globals PDFJS */ + +'use strict'; + +var FontInspector = (function FontInspectorClosure() { + var fonts; + var active = false; + var fontAttribute = 'data-font-name'; + function removeSelection() { + var divs = document.querySelectorAll('div[' + fontAttribute + ']'); + for (var i = 0, ii = divs.length; i < ii; ++i) { + var div = divs[i]; + div.className = ''; + } + } + function resetSelection() { + var divs = document.querySelectorAll('div[' + fontAttribute + ']'); + for (var i = 0, ii = divs.length; i < ii; ++i) { + var div = divs[i]; + div.className = 'debuggerHideText'; + } + } + function selectFont(fontName, show) { + var divs = document.querySelectorAll('div[' + fontAttribute + '=' + + fontName + ']'); + for (var i = 0, ii = divs.length; i < ii; ++i) { + var div = divs[i]; + div.className = show ? 'debuggerShowText' : 'debuggerHideText'; + } + } + function textLayerClick(e) { + if (!e.target.dataset.fontName || + e.target.tagName.toUpperCase() !== 'DIV') { + return; + } + var fontName = e.target.dataset.fontName; + var selects = document.getElementsByTagName('input'); + for (var i = 0; i < selects.length; ++i) { + var select = selects[i]; + if (select.dataset.fontName !== fontName) { + continue; + } + select.checked = !select.checked; + selectFont(fontName, select.checked); + select.scrollIntoView(); + } + } + return { + // Properties/functions needed by PDFBug. + id: 'FontInspector', + name: 'Font Inspector', + panel: null, + manager: null, + init: function init() { + var panel = this.panel; + panel.setAttribute('style', 'padding: 5px;'); + var tmp = document.createElement('button'); + tmp.addEventListener('click', resetSelection); + tmp.textContent = 'Refresh'; + panel.appendChild(tmp); + + fonts = document.createElement('div'); + panel.appendChild(fonts); + }, + cleanup: function cleanup() { + fonts.textContent = ''; + }, + enabled: false, + get active() { + return active; + }, + set active(value) { + active = value; + if (active) { + document.body.addEventListener('click', textLayerClick, true); + resetSelection(); + } else { + document.body.removeEventListener('click', textLayerClick, true); + removeSelection(); + } + }, + // FontInspector specific functions. + fontAdded: function fontAdded(fontObj, url) { + function properties(obj, list) { + var moreInfo = document.createElement('table'); + for (var i = 0; i < list.length; i++) { + var tr = document.createElement('tr'); + var td1 = document.createElement('td'); + td1.textContent = list[i]; + tr.appendChild(td1); + var td2 = document.createElement('td'); + td2.textContent = obj[list[i]].toString(); + tr.appendChild(td2); + moreInfo.appendChild(tr); + } + return moreInfo; + } + var moreInfo = properties(fontObj, ['name', 'type']); + var fontName = fontObj.loadedName; + var font = document.createElement('div'); + var name = document.createElement('span'); + name.textContent = fontName; + var download = document.createElement('a'); + if (url) { + url = /url\(['"]?([^\)"']+)/.exec(url); + download.href = url[1]; + } else if (fontObj.data) { + url = URL.createObjectURL(new Blob([fontObj.data], { + type: fontObj.mimeType + })); + download.href = url; + } + download.textContent = 'Download'; + var logIt = document.createElement('a'); + logIt.href = ''; + logIt.textContent = 'Log'; + logIt.addEventListener('click', function(event) { + event.preventDefault(); + console.log(fontObj); + }); + var select = document.createElement('input'); + select.setAttribute('type', 'checkbox'); + select.dataset.fontName = fontName; + select.addEventListener('click', (function(select, fontName) { + return (function() { + selectFont(fontName, select.checked); + }); + })(select, fontName)); + font.appendChild(select); + font.appendChild(name); + font.appendChild(document.createTextNode(' ')); + font.appendChild(download); + font.appendChild(document.createTextNode(' ')); + font.appendChild(logIt); + font.appendChild(moreInfo); + fonts.appendChild(font); + // Somewhat of a hack, should probably add a hook for when the text layer + // is done rendering. + setTimeout(function() { + if (this.active) { + resetSelection(); + } + }.bind(this), 2000); + } + }; +})(); + +// Manages all the page steppers. +var StepperManager = (function StepperManagerClosure() { + var steppers = []; + var stepperDiv = null; + var stepperControls = null; + var stepperChooser = null; + var breakPoints = {}; + return { + // Properties/functions needed by PDFBug. + id: 'Stepper', + name: 'Stepper', + panel: null, + manager: null, + init: function init() { + var self = this; + this.panel.setAttribute('style', 'padding: 5px;'); + stepperControls = document.createElement('div'); + stepperChooser = document.createElement('select'); + stepperChooser.addEventListener('change', function(event) { + self.selectStepper(this.value); + }); + stepperControls.appendChild(stepperChooser); + stepperDiv = document.createElement('div'); + this.panel.appendChild(stepperControls); + this.panel.appendChild(stepperDiv); + if (sessionStorage.getItem('pdfjsBreakPoints')) { + breakPoints = JSON.parse(sessionStorage.getItem('pdfjsBreakPoints')); + } + }, + cleanup: function cleanup() { + stepperChooser.textContent = ''; + stepperDiv.textContent = ''; + steppers = []; + }, + enabled: false, + active: false, + // Stepper specific functions. + create: function create(pageIndex) { + var debug = document.createElement('div'); + debug.id = 'stepper' + pageIndex; + debug.setAttribute('hidden', true); + debug.className = 'stepper'; + stepperDiv.appendChild(debug); + var b = document.createElement('option'); + b.textContent = 'Page ' + (pageIndex + 1); + b.value = pageIndex; + stepperChooser.appendChild(b); + var initBreakPoints = breakPoints[pageIndex] || []; + var stepper = new Stepper(debug, pageIndex, initBreakPoints); + steppers.push(stepper); + if (steppers.length === 1) { + this.selectStepper(pageIndex, false); + } + return stepper; + }, + selectStepper: function selectStepper(pageIndex, selectPanel) { + var i; + pageIndex = pageIndex | 0; + if (selectPanel) { + this.manager.selectPanel(this); + } + for (i = 0; i < steppers.length; ++i) { + var stepper = steppers[i]; + if (stepper.pageIndex === pageIndex) { + stepper.panel.removeAttribute('hidden'); + } else { + stepper.panel.setAttribute('hidden', true); + } + } + var options = stepperChooser.options; + for (i = 0; i < options.length; ++i) { + var option = options[i]; + option.selected = (option.value | 0) === pageIndex; + } + }, + saveBreakPoints: function saveBreakPoints(pageIndex, bps) { + breakPoints[pageIndex] = bps; + sessionStorage.setItem('pdfjsBreakPoints', JSON.stringify(breakPoints)); + } + }; +})(); + +// The stepper for each page's IRQueue. +var Stepper = (function StepperClosure() { + // Shorter way to create element and optionally set textContent. + function c(tag, textContent) { + var d = document.createElement(tag); + if (textContent) { + d.textContent = textContent; + } + return d; + } + + var opMap = null; + + function simplifyArgs(args) { + if (typeof args === 'string') { + var MAX_STRING_LENGTH = 75; + return args.length <= MAX_STRING_LENGTH ? args : + args.substr(0, MAX_STRING_LENGTH) + '...'; + } + if (typeof args !== 'object' || args === null) { + return args; + } + if ('length' in args) { // array + var simpleArgs = [], i, ii; + var MAX_ITEMS = 10; + for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) { + simpleArgs.push(simplifyArgs(args[i])); + } + if (i < args.length) { + simpleArgs.push('...'); + } + return simpleArgs; + } + var simpleObj = {}; + for (var key in args) { + simpleObj[key] = simplifyArgs(args[key]); + } + return simpleObj; + } + + function Stepper(panel, pageIndex, initialBreakPoints) { + this.panel = panel; + this.breakPoint = 0; + this.nextBreakPoint = null; + this.pageIndex = pageIndex; + this.breakPoints = initialBreakPoints; + this.currentIdx = -1; + this.operatorListIdx = 0; + } + Stepper.prototype = { + init: function init() { + var panel = this.panel; + var content = c('div', 'c=continue, s=step'); + var table = c('table'); + content.appendChild(table); + table.cellSpacing = 0; + var headerRow = c('tr'); + table.appendChild(headerRow); + headerRow.appendChild(c('th', 'Break')); + headerRow.appendChild(c('th', 'Idx')); + headerRow.appendChild(c('th', 'fn')); + headerRow.appendChild(c('th', 'args')); + panel.appendChild(content); + this.table = table; + if (!opMap) { + opMap = Object.create(null); + for (var key in PDFJS.OPS) { + opMap[PDFJS.OPS[key]] = key; + } + } + }, + updateOperatorList: function updateOperatorList(operatorList) { + var self = this; + + function cboxOnClick() { + var x = +this.dataset.idx; + if (this.checked) { + self.breakPoints.push(x); + } else { + self.breakPoints.splice(self.breakPoints.indexOf(x), 1); + } + StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints); + } + + var MAX_OPERATORS_COUNT = 15000; + if (this.operatorListIdx > MAX_OPERATORS_COUNT) { + return; + } + + var chunk = document.createDocumentFragment(); + var operatorsToDisplay = Math.min(MAX_OPERATORS_COUNT, + operatorList.fnArray.length); + for (var i = this.operatorListIdx; i < operatorsToDisplay; i++) { + var line = c('tr'); + line.className = 'line'; + line.dataset.idx = i; + chunk.appendChild(line); + var checked = this.breakPoints.indexOf(i) !== -1; + var args = operatorList.argsArray[i] || []; + + var breakCell = c('td'); + var cbox = c('input'); + cbox.type = 'checkbox'; + cbox.className = 'points'; + cbox.checked = checked; + cbox.dataset.idx = i; + cbox.onclick = cboxOnClick; + + breakCell.appendChild(cbox); + line.appendChild(breakCell); + line.appendChild(c('td', i.toString())); + var fn = opMap[operatorList.fnArray[i]]; + var decArgs = args; + if (fn === 'showText') { + var glyphs = args[0]; + var newArgs = []; + var str = []; + for (var j = 0; j < glyphs.length; j++) { + var glyph = glyphs[j]; + if (typeof glyph === 'object' && glyph !== null) { + str.push(glyph.fontChar); + } else { + if (str.length > 0) { + newArgs.push(str.join('')); + str = []; + } + newArgs.push(glyph); // null or number + } + } + if (str.length > 0) { + newArgs.push(str.join('')); + } + decArgs = [newArgs]; + } + line.appendChild(c('td', fn)); + line.appendChild(c('td', JSON.stringify(simplifyArgs(decArgs)))); + } + if (operatorsToDisplay < operatorList.fnArray.length) { + line = c('tr'); + var lastCell = c('td', '...'); + lastCell.colspan = 4; + chunk.appendChild(lastCell); + } + this.operatorListIdx = operatorList.fnArray.length; + this.table.appendChild(chunk); + }, + getNextBreakPoint: function getNextBreakPoint() { + this.breakPoints.sort(function(a, b) { return a - b; }); + for (var i = 0; i < this.breakPoints.length; i++) { + if (this.breakPoints[i] > this.currentIdx) { + return this.breakPoints[i]; + } + } + return null; + }, + breakIt: function breakIt(idx, callback) { + StepperManager.selectStepper(this.pageIndex, true); + var self = this; + var dom = document; + self.currentIdx = idx; + var listener = function(e) { + switch (e.keyCode) { + case 83: // step + dom.removeEventListener('keydown', listener, false); + self.nextBreakPoint = self.currentIdx + 1; + self.goTo(-1); + callback(); + break; + case 67: // continue + dom.removeEventListener('keydown', listener, false); + var breakPoint = self.getNextBreakPoint(); + self.nextBreakPoint = breakPoint; + self.goTo(-1); + callback(); + break; + } + }; + dom.addEventListener('keydown', listener, false); + self.goTo(idx); + }, + goTo: function goTo(idx) { + var allRows = this.panel.getElementsByClassName('line'); + for (var x = 0, xx = allRows.length; x < xx; ++x) { + var row = allRows[x]; + if ((row.dataset.idx | 0) === idx) { + row.style.backgroundColor = 'rgb(251,250,207)'; + row.scrollIntoView(); + } else { + row.style.backgroundColor = null; + } + } + } + }; + return Stepper; +})(); + +var Stats = (function Stats() { + var stats = []; + function clear(node) { + while (node.hasChildNodes()) { + node.removeChild(node.lastChild); + } + } + function getStatIndex(pageNumber) { + for (var i = 0, ii = stats.length; i < ii; ++i) { + if (stats[i].pageNumber === pageNumber) { + return i; + } + } + return false; + } + return { + // Properties/functions needed by PDFBug. + id: 'Stats', + name: 'Stats', + panel: null, + manager: null, + init: function init() { + this.panel.setAttribute('style', 'padding: 5px;'); + PDFJS.enableStats = true; + }, + enabled: false, + active: false, + // Stats specific functions. + add: function(pageNumber, stat) { + if (!stat) { + return; + } + var statsIndex = getStatIndex(pageNumber); + if (statsIndex !== false) { + var b = stats[statsIndex]; + this.panel.removeChild(b.div); + stats.splice(statsIndex, 1); + } + var wrapper = document.createElement('div'); + wrapper.className = 'stats'; + var title = document.createElement('div'); + title.className = 'title'; + title.textContent = 'Page: ' + pageNumber; + var statsDiv = document.createElement('div'); + statsDiv.textContent = stat.toString(); + wrapper.appendChild(title); + wrapper.appendChild(statsDiv); + stats.push({ pageNumber: pageNumber, div: wrapper }); + stats.sort(function(a, b) { return a.pageNumber - b.pageNumber; }); + clear(this.panel); + for (var i = 0, ii = stats.length; i < ii; ++i) { + this.panel.appendChild(stats[i].div); + } + }, + cleanup: function () { + stats = []; + clear(this.panel); + } + }; +})(); + +// Manages all the debugging tools. +var PDFBug = (function PDFBugClosure() { + var panelWidth = 300; + var buttons = []; + var activePanel = null; + + return { + tools: [ + FontInspector, + StepperManager, + Stats + ], + enable: function(ids) { + var all = false, tools = this.tools; + if (ids.length === 1 && ids[0] === 'all') { + all = true; + } + for (var i = 0; i < tools.length; ++i) { + var tool = tools[i]; + if (all || ids.indexOf(tool.id) !== -1) { + tool.enabled = true; + } + } + if (!all) { + // Sort the tools by the order they are enabled. + tools.sort(function(a, b) { + var indexA = ids.indexOf(a.id); + indexA = indexA < 0 ? tools.length : indexA; + var indexB = ids.indexOf(b.id); + indexB = indexB < 0 ? tools.length : indexB; + return indexA - indexB; + }); + } + }, + init: function init() { + /* + * Basic Layout: + * PDFBug + * Controls + * Panels + * Panel + * Panel + * ... + */ + var ui = document.createElement('div'); + ui.id = 'PDFBug'; + + var controls = document.createElement('div'); + controls.setAttribute('class', 'controls'); + ui.appendChild(controls); + + var panels = document.createElement('div'); + panels.setAttribute('class', 'panels'); + ui.appendChild(panels); + + var container = document.getElementById('viewerContainer'); + container.appendChild(ui); + container.style.right = panelWidth + 'px'; + + // Initialize all the debugging tools. + var tools = this.tools; + var self = this; + for (var i = 0; i < tools.length; ++i) { + var tool = tools[i]; + var panel = document.createElement('div'); + var panelButton = document.createElement('button'); + panelButton.textContent = tool.name; + panelButton.addEventListener('click', (function(selected) { + return function(event) { + event.preventDefault(); + self.selectPanel(selected); + }; + })(i)); + controls.appendChild(panelButton); + panels.appendChild(panel); + tool.panel = panel; + tool.manager = this; + if (tool.enabled) { + tool.init(); + } else { + panel.textContent = tool.name + ' is disabled. To enable add ' + + ' "' + tool.id + '" to the pdfBug parameter ' + + 'and refresh (seperate multiple by commas).'; + } + buttons.push(panelButton); + } + this.selectPanel(0); + }, + cleanup: function cleanup() { + for (var i = 0, ii = this.tools.length; i < ii; i++) { + if (this.tools[i].enabled) { + this.tools[i].cleanup(); + } + } + }, + selectPanel: function selectPanel(index) { + if (typeof index !== 'number') { + index = this.tools.indexOf(index); + } + if (index === activePanel) { + return; + } + activePanel = index; + var tools = this.tools; + for (var j = 0; j < tools.length; ++j) { + if (j === index) { + buttons[j].setAttribute('class', 'active'); + tools[j].active = true; + tools[j].panel.removeAttribute('hidden'); + } else { + buttons[j].setAttribute('class', ''); + tools[j].active = false; + tools[j].panel.setAttribute('hidden', 'true'); + } + } + } + }; +})(); diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-check.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-check.svg new file mode 100644 index 0000000..71cd16d --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-check.svg @@ -0,0 +1,11 @@ + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-comment.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-comment.svg new file mode 100644 index 0000000..86f1f17 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-comment.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-help.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-help.svg new file mode 100644 index 0000000..00938fe --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-help.svg @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-insert.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-insert.svg new file mode 100644 index 0000000..519ef68 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-insert.svg @@ -0,0 +1,10 @@ + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-key.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-key.svg new file mode 100644 index 0000000..8d09d53 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-key.svg @@ -0,0 +1,11 @@ + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-newparagraph.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-newparagraph.svg new file mode 100644 index 0000000..38d2497 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-newparagraph.svg @@ -0,0 +1,11 @@ + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-noicon.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-noicon.svg new file mode 100644 index 0000000..c07d108 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-noicon.svg @@ -0,0 +1,7 @@ + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-note.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-note.svg new file mode 100644 index 0000000..7017365 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-note.svg @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-paragraph.svg b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-paragraph.svg new file mode 100644 index 0000000..6ae5212 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/annotation-paragraph.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next-rtl.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next-rtl.png new file mode 100644 index 0000000..bef0274 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next-rtl.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next-rtl@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next-rtl@2x.png new file mode 100644 index 0000000..1da6dc9 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next-rtl@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next.png new file mode 100644 index 0000000..de1d0fc Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next@2x.png new file mode 100644 index 0000000..0250307 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-next@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous-rtl.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous-rtl.png new file mode 100644 index 0000000..de1d0fc Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous-rtl.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous-rtl@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous-rtl@2x.png new file mode 100644 index 0000000..0250307 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous-rtl@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous.png new file mode 100644 index 0000000..bef0274 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous@2x.png new file mode 100644 index 0000000..1da6dc9 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/findbarButton-previous@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/grab.cur b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/grab.cur new file mode 100644 index 0000000..db7ad5a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/grab.cur differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/grabbing.cur b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/grabbing.cur new file mode 100644 index 0000000..e0dfd04 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/grabbing.cur differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-icon.gif b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-icon.gif new file mode 100644 index 0000000..1c72ebb Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-icon.gif differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-small.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-small.png new file mode 100644 index 0000000..8831a80 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-small.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-small@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-small@2x.png new file mode 100644 index 0000000..b25b445 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/loading-small@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-documentProperties.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-documentProperties.png new file mode 100644 index 0000000..40925e2 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-documentProperties.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-documentProperties@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-documentProperties@2x.png new file mode 100644 index 0000000..adb240e Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-documentProperties@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-firstPage.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-firstPage.png new file mode 100644 index 0000000..e68846a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-firstPage.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-firstPage@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-firstPage@2x.png new file mode 100644 index 0000000..3ad8af5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-firstPage@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-handTool.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-handTool.png new file mode 100644 index 0000000..cb85a84 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-handTool.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-handTool@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-handTool@2x.png new file mode 100644 index 0000000..5c13f77 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-handTool@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-lastPage.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-lastPage.png new file mode 100644 index 0000000..be763e0 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-lastPage.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-lastPage@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-lastPage@2x.png new file mode 100644 index 0000000..8570984 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-lastPage@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCcw.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCcw.png new file mode 100644 index 0000000..675d6da Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCcw.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCcw@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCcw@2x.png new file mode 100644 index 0000000..b9e7431 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCcw@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCw.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCw.png new file mode 100644 index 0000000..e1c7598 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCw.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCw@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCw@2x.png new file mode 100644 index 0000000..cb257b4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/secondaryToolbarButton-rotateCw@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/shadow.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/shadow.png new file mode 100644 index 0000000..31d3bdb Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/shadow.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/texture.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/texture.png new file mode 100644 index 0000000..eb5ccb5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/texture.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-bookmark.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-bookmark.png new file mode 100644 index 0000000..a187be6 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-bookmark.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-bookmark@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-bookmark@2x.png new file mode 100644 index 0000000..4efbaa6 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-bookmark@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-download.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-download.png new file mode 100644 index 0000000..eaab35f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-download.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-download@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-download@2x.png new file mode 100644 index 0000000..896face Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-download@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-menuArrows.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-menuArrows.png new file mode 100644 index 0000000..306eb43 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-menuArrows.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-menuArrows@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-menuArrows@2x.png new file mode 100644 index 0000000..f7570bc Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-menuArrows@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-openFile.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-openFile.png new file mode 100644 index 0000000..b5cf1bd Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-openFile.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-openFile@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-openFile@2x.png new file mode 100644 index 0000000..91ab765 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-openFile@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown-rtl.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown-rtl.png new file mode 100644 index 0000000..1957f79 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown-rtl.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown-rtl@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown-rtl@2x.png new file mode 100644 index 0000000..16ebcb8 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown-rtl@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown.png new file mode 100644 index 0000000..8219ecf Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown@2x.png new file mode 100644 index 0000000..758c01d Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageDown@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp-rtl.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp-rtl.png new file mode 100644 index 0000000..98e7ce4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp-rtl.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp-rtl@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp-rtl@2x.png new file mode 100644 index 0000000..a01b023 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp-rtl@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp.png new file mode 100644 index 0000000..fb9daa3 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp@2x.png new file mode 100644 index 0000000..a5cfd75 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-pageUp@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-presentationMode.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-presentationMode.png new file mode 100644 index 0000000..3ac2124 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-presentationMode.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-presentationMode@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-presentationMode@2x.png new file mode 100644 index 0000000..cada9e7 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-presentationMode@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-print.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-print.png new file mode 100644 index 0000000..51275e5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-print.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-print@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-print@2x.png new file mode 100644 index 0000000..53d18da Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-print@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-search.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-search.png new file mode 100644 index 0000000..f9b7557 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-search.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-search@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-search@2x.png new file mode 100644 index 0000000..456b133 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-search@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle-rtl.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle-rtl.png new file mode 100644 index 0000000..8437095 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle-rtl.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png new file mode 100644 index 0000000..9d9bfa4 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle.png new file mode 100644 index 0000000..1f90f83 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle@2x.png new file mode 100644 index 0000000..b066fe5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-secondaryToolbarToggle@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle-rtl.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle-rtl.png new file mode 100644 index 0000000..6f85ec0 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle-rtl.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle-rtl@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle-rtl@2x.png new file mode 100644 index 0000000..291e006 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle-rtl@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle.png new file mode 100644 index 0000000..025dc90 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle@2x.png new file mode 100644 index 0000000..7f834df Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-sidebarToggle@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewAttachments.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewAttachments.png new file mode 100644 index 0000000..fcd0b26 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewAttachments.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewAttachments@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewAttachments@2x.png new file mode 100644 index 0000000..b979e52 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewAttachments@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline-rtl.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline-rtl.png new file mode 100644 index 0000000..aaa9430 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline-rtl.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline-rtl@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline-rtl@2x.png new file mode 100644 index 0000000..3410f70 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline-rtl@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline.png new file mode 100644 index 0000000..976365a Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline@2x.png new file mode 100644 index 0000000..b6a197f Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewOutline@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewThumbnail.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewThumbnail.png new file mode 100644 index 0000000..584ba55 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewThumbnail.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewThumbnail@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewThumbnail@2x.png new file mode 100644 index 0000000..fb7db93 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-viewThumbnail@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomIn.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomIn.png new file mode 100644 index 0000000..513d081 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomIn.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomIn@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomIn@2x.png new file mode 100644 index 0000000..d5d49d5 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomIn@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomOut.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomOut.png new file mode 100644 index 0000000..156c26b Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomOut.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomOut@2x.png b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomOut@2x.png new file mode 100644 index 0000000..959e191 Binary files /dev/null and b/test-module-system/test-system-biz/src/main/resources/static/generic/web/images/toolbarButton-zoomOut@2x.png differ diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/l10n.js b/test-module-system/test-system-biz/src/main/resources/static/generic/web/l10n.js new file mode 100644 index 0000000..3d5ecff --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/l10n.js @@ -0,0 +1,1033 @@ +/** + * Copyright (c) 2011-2013 Fabien Cazenave, Mozilla. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +/* + Additional modifications for PDF.js project: + - Disables language initialization on page loading; + - Removes consoleWarn and consoleLog and use console.log/warn directly. + - Removes window._ assignment. + - Remove compatibility code for OldIE. +*/ + +/*jshint browser: true, devel: true, es5: true, globalstrict: true */ +'use strict'; + +document.webL10n = (function(window, document, undefined) { + var gL10nData = {}; + var gTextData = ''; + var gTextProp = 'textContent'; + var gLanguage = ''; + var gMacros = {}; + var gReadyState = 'loading'; + + + /** + * Synchronously loading l10n resources significantly minimizes flickering + * from displaying the app with non-localized strings and then updating the + * strings. Although this will block all script execution on this page, we + * expect that the l10n resources are available locally on flash-storage. + * + * As synchronous XHR is generally considered as a bad idea, we're still + * loading l10n resources asynchronously -- but we keep this in a setting, + * just in case... and applications using this library should hide their + * content until the `localized' event happens. + */ + + var gAsyncResourceLoading = true; // read-only + + + /** + * DOM helpers for the so-called "HTML API". + * + * These functions are written for modern browsers. For old versions of IE, + * they're overridden in the 'startup' section at the end of this file. + */ + + function getL10nResourceLinks() { + return document.querySelectorAll('link[type="application/l10n"]'); + } + + function getL10nDictionary() { + var script = document.querySelector('script[type="application/l10n"]'); + // TODO: support multiple and external JSON dictionaries + return script ? JSON.parse(script.innerHTML) : null; + } + + function getTranslatableChildren(element) { + return element ? element.querySelectorAll('*[data-l10n-id]') : []; + } + + function getL10nAttributes(element) { + if (!element) + return {}; + + var l10nId = element.getAttribute('data-l10n-id'); + var l10nArgs = element.getAttribute('data-l10n-args'); + var args = {}; + if (l10nArgs) { + try { + args = JSON.parse(l10nArgs); + } catch (e) { + console.warn('could not parse arguments for #' + l10nId); + } + } + return { id: l10nId, args: args }; + } + + function fireL10nReadyEvent(lang) { + var evtObject = document.createEvent('Event'); + evtObject.initEvent('localized', true, false); + evtObject.language = lang; + document.dispatchEvent(evtObject); + } + + function xhrLoadText(url, onSuccess, onFailure) { + onSuccess = onSuccess || function _onSuccess(data) {}; + onFailure = onFailure || function _onFailure() { + console.warn(url + ' not found.'); + }; + + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, gAsyncResourceLoading); + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=utf-8'); + } + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200 || xhr.status === 0) { + onSuccess(xhr.responseText); + } else { + onFailure(); + } + } + }; + xhr.onerror = onFailure; + xhr.ontimeout = onFailure; + + // in Firefox OS with the app:// protocol, trying to XHR a non-existing + // URL will raise an exception here -- hence this ugly try...catch. + try { + xhr.send(null); + } catch (e) { + onFailure(); + } + } + + + /** + * l10n resource parser: + * - reads (async XHR) the l10n resource matching `lang'; + * - imports linked resources (synchronously) when specified; + * - parses the text data (fills `gL10nData' and `gTextData'); + * - triggers success/failure callbacks when done. + * + * @param {string} href + * URL of the l10n resource to parse. + * + * @param {string} lang + * locale (language) to parse. Must be a lowercase string. + * + * @param {Function} successCallback + * triggered when the l10n resource has been successully parsed. + * + * @param {Function} failureCallback + * triggered when the an error has occured. + * + * @return {void} + * uses the following global variables: gL10nData, gTextData, gTextProp. + */ + + function parseResource(href, lang, successCallback, failureCallback) { + var baseURL = href.replace(/[^\/]*$/, '') || './'; + + // handle escaped characters (backslashes) in a string + function evalString(text) { + if (text.lastIndexOf('\\') < 0) + return text; + return text.replace(/\\\\/g, '\\') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\b/g, '\b') + .replace(/\\f/g, '\f') + .replace(/\\{/g, '{') + .replace(/\\}/g, '}') + .replace(/\\"/g, '"') + .replace(/\\'/g, "'"); + } + + // parse *.properties text data into an l10n dictionary + // If gAsyncResourceLoading is false, then the callback will be called + // synchronously. Otherwise it is called asynchronously. + function parseProperties(text, parsedPropertiesCallback) { + var dictionary = {}; + + // token expressions + var reBlank = /^\s*|\s*$/; + var reComment = /^\s*#|^\s*$/; + var reSection = /^\s*\[(.*)\]\s*$/; + var reImport = /^\s*@import\s+url\((.*)\)\s*$/i; + var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\' + + // parse the *.properties file into an associative array + function parseRawLines(rawText, extendedSyntax, parsedRawLinesCallback) { + var entries = rawText.replace(reBlank, '').split(/[\r\n]+/); + var currentLang = '*'; + var genericLang = lang.split('-', 1)[0]; + var skipLang = false; + var match = ''; + + function nextEntry() { + // Use infinite loop instead of recursion to avoid reaching the + // maximum recursion limit for content with many lines. + while (true) { + if (!entries.length) { + parsedRawLinesCallback(); + return; + } + var line = entries.shift(); + + // comment or blank line? + if (reComment.test(line)) + continue; + + // the extended syntax supports [lang] sections and @import rules + if (extendedSyntax) { + match = reSection.exec(line); + if (match) { // section start? + // RFC 4646, section 4.4, "All comparisons MUST be performed + // in a case-insensitive manner." + + currentLang = match[1].toLowerCase(); + skipLang = (currentLang !== '*') && + (currentLang !== lang) && (currentLang !== genericLang); + continue; + } else if (skipLang) { + continue; + } + match = reImport.exec(line); + if (match) { // @import rule? + loadImport(baseURL + match[1], nextEntry); + return; + } + } + + // key-value pair + var tmp = line.match(reSplit); + if (tmp && tmp.length == 3) { + dictionary[tmp[1]] = evalString(tmp[2]); + } + } + } + nextEntry(); + } + + // import another *.properties file + function loadImport(url, callback) { + xhrLoadText(url, function(content) { + parseRawLines(content, false, callback); // don't allow recursive imports + }, null); + } + + // fill the dictionary + parseRawLines(text, true, function() { + parsedPropertiesCallback(dictionary); + }); + } + + // load and parse l10n data (warning: global variables are used here) + xhrLoadText(href, function(response) { + gTextData += response; // mostly for debug + + // parse *.properties text data into an l10n dictionary + parseProperties(response, function(data) { + + // find attribute descriptions, if any + for (var key in data) { + var id, prop, index = key.lastIndexOf('.'); + if (index > 0) { // an attribute has been specified + id = key.substring(0, index); + prop = key.substr(index + 1); + } else { // no attribute: assuming text content by default + id = key; + prop = gTextProp; + } + if (!gL10nData[id]) { + gL10nData[id] = {}; + } + gL10nData[id][prop] = data[key]; + } + + // trigger callback + if (successCallback) { + successCallback(); + } + }); + }, failureCallback); + } + + // load and parse all resources for the specified locale + function loadLocale(lang, callback) { + // RFC 4646, section 2.1 states that language tags have to be treated as + // case-insensitive. Convert to lowercase for case-insensitive comparisons. + if (lang) { + lang = lang.toLowerCase(); + } + + callback = callback || function _callback() {}; + + clear(); + gLanguage = lang; + + // check all nodes + // and load the resource files + var langLinks = getL10nResourceLinks(); + var langCount = langLinks.length; + if (langCount === 0) { + // we might have a pre-compiled dictionary instead + var dict = getL10nDictionary(); + if (dict && dict.locales && dict.default_locale) { + console.log('using the embedded JSON directory, early way out'); + gL10nData = dict.locales[lang]; + if (!gL10nData) { + var defaultLocale = dict.default_locale.toLowerCase(); + for (var anyCaseLang in dict.locales) { + anyCaseLang = anyCaseLang.toLowerCase(); + if (anyCaseLang === lang) { + gL10nData = dict.locales[lang]; + break; + } else if (anyCaseLang === defaultLocale) { + gL10nData = dict.locales[defaultLocale]; + } + } + } + callback(); + } else { + console.log('no resource to load, early way out'); + } + // early way out + fireL10nReadyEvent(lang); + gReadyState = 'complete'; + return; + } + + // start the callback when all resources are loaded + var onResourceLoaded = null; + var gResourceCount = 0; + onResourceLoaded = function() { + gResourceCount++; + if (gResourceCount >= langCount) { + callback(); + fireL10nReadyEvent(lang); + gReadyState = 'complete'; + } + }; + + // load all resource files + function L10nResourceLink(link) { + var href = link.href; + // Note: If |gAsyncResourceLoading| is false, then the following callbacks + // are synchronously called. + this.load = function(lang, callback) { + parseResource(href, lang, callback, function() { + console.warn(href + ' not found.'); + // lang not found, used default resource instead + console.warn('"' + lang + '" resource not found'); + gLanguage = ''; + // Resource not loaded, but we still need to call the callback. + callback(); + }); + }; + } + + for (var i = 0; i < langCount; i++) { + var resource = new L10nResourceLink(langLinks[i]); + resource.load(lang, onResourceLoaded); + } + } + + // clear all l10n data + function clear() { + gL10nData = {}; + gTextData = ''; + gLanguage = ''; + // TODO: clear all non predefined macros. + // There's no such macro /yet/ but we're planning to have some... + } + + + /** + * Get rules for plural forms (shared with JetPack), see: + * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html + * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p + * + * @param {string} lang + * locale (language) used. + * + * @return {Function} + * returns a function that gives the plural form name for a given integer: + * var fun = getPluralRules('en'); + * fun(1) -> 'one' + * fun(0) -> 'other' + * fun(1000) -> 'other'. + */ + + function getPluralRules(lang) { + var locales2rules = { + 'af': 3, + 'ak': 4, + 'am': 4, + 'ar': 1, + 'asa': 3, + 'az': 0, + 'be': 11, + 'bem': 3, + 'bez': 3, + 'bg': 3, + 'bh': 4, + 'bm': 0, + 'bn': 3, + 'bo': 0, + 'br': 20, + 'brx': 3, + 'bs': 11, + 'ca': 3, + 'cgg': 3, + 'chr': 3, + 'cs': 12, + 'cy': 17, + 'da': 3, + 'de': 3, + 'dv': 3, + 'dz': 0, + 'ee': 3, + 'el': 3, + 'en': 3, + 'eo': 3, + 'es': 3, + 'et': 3, + 'eu': 3, + 'fa': 0, + 'ff': 5, + 'fi': 3, + 'fil': 4, + 'fo': 3, + 'fr': 5, + 'fur': 3, + 'fy': 3, + 'ga': 8, + 'gd': 24, + 'gl': 3, + 'gsw': 3, + 'gu': 3, + 'guw': 4, + 'gv': 23, + 'ha': 3, + 'haw': 3, + 'he': 2, + 'hi': 4, + 'hr': 11, + 'hu': 0, + 'id': 0, + 'ig': 0, + 'ii': 0, + 'is': 3, + 'it': 3, + 'iu': 7, + 'ja': 0, + 'jmc': 3, + 'jv': 0, + 'ka': 0, + 'kab': 5, + 'kaj': 3, + 'kcg': 3, + 'kde': 0, + 'kea': 0, + 'kk': 3, + 'kl': 3, + 'km': 0, + 'kn': 0, + 'ko': 0, + 'ksb': 3, + 'ksh': 21, + 'ku': 3, + 'kw': 7, + 'lag': 18, + 'lb': 3, + 'lg': 3, + 'ln': 4, + 'lo': 0, + 'lt': 10, + 'lv': 6, + 'mas': 3, + 'mg': 4, + 'mk': 16, + 'ml': 3, + 'mn': 3, + 'mo': 9, + 'mr': 3, + 'ms': 0, + 'mt': 15, + 'my': 0, + 'nah': 3, + 'naq': 7, + 'nb': 3, + 'nd': 3, + 'ne': 3, + 'nl': 3, + 'nn': 3, + 'no': 3, + 'nr': 3, + 'nso': 4, + 'ny': 3, + 'nyn': 3, + 'om': 3, + 'or': 3, + 'pa': 3, + 'pap': 3, + 'pl': 13, + 'ps': 3, + 'pt': 3, + 'rm': 3, + 'ro': 9, + 'rof': 3, + 'ru': 11, + 'rwk': 3, + 'sah': 0, + 'saq': 3, + 'se': 7, + 'seh': 3, + 'ses': 0, + 'sg': 0, + 'sh': 11, + 'shi': 19, + 'sk': 12, + 'sl': 14, + 'sma': 7, + 'smi': 7, + 'smj': 7, + 'smn': 7, + 'sms': 7, + 'sn': 3, + 'so': 3, + 'sq': 3, + 'sr': 11, + 'ss': 3, + 'ssy': 3, + 'st': 3, + 'sv': 3, + 'sw': 3, + 'syr': 3, + 'ta': 3, + 'te': 3, + 'teo': 3, + 'th': 0, + 'ti': 4, + 'tig': 3, + 'tk': 3, + 'tl': 4, + 'tn': 3, + 'to': 0, + 'tr': 0, + 'ts': 3, + 'tzm': 22, + 'uk': 11, + 'ur': 3, + 've': 3, + 'vi': 0, + 'vun': 3, + 'wa': 4, + 'wae': 3, + 'wo': 0, + 'xh': 3, + 'xog': 3, + 'yo': 0, + 'zh': 0, + 'zu': 3 + }; + + // utility functions for plural rules methods + function isIn(n, list) { + return list.indexOf(n) !== -1; + } + function isBetween(n, start, end) { + return start <= n && n <= end; + } + + // list of all plural rules methods: + // map an integer to the plural form name to use + var pluralRules = { + '0': function(n) { + return 'other'; + }, + '1': function(n) { + if ((isBetween((n % 100), 3, 10))) + return 'few'; + if (n === 0) + return 'zero'; + if ((isBetween((n % 100), 11, 99))) + return 'many'; + if (n == 2) + return 'two'; + if (n == 1) + return 'one'; + return 'other'; + }, + '2': function(n) { + if (n !== 0 && (n % 10) === 0) + return 'many'; + if (n == 2) + return 'two'; + if (n == 1) + return 'one'; + return 'other'; + }, + '3': function(n) { + if (n == 1) + return 'one'; + return 'other'; + }, + '4': function(n) { + if ((isBetween(n, 0, 1))) + return 'one'; + return 'other'; + }, + '5': function(n) { + if ((isBetween(n, 0, 2)) && n != 2) + return 'one'; + return 'other'; + }, + '6': function(n) { + if (n === 0) + return 'zero'; + if ((n % 10) == 1 && (n % 100) != 11) + return 'one'; + return 'other'; + }, + '7': function(n) { + if (n == 2) + return 'two'; + if (n == 1) + return 'one'; + return 'other'; + }, + '8': function(n) { + if ((isBetween(n, 3, 6))) + return 'few'; + if ((isBetween(n, 7, 10))) + return 'many'; + if (n == 2) + return 'two'; + if (n == 1) + return 'one'; + return 'other'; + }, + '9': function(n) { + if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19))) + return 'few'; + if (n == 1) + return 'one'; + return 'other'; + }, + '10': function(n) { + if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) + return 'few'; + if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19))) + return 'one'; + return 'other'; + }, + '11': function(n) { + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) + return 'few'; + if ((n % 10) === 0 || + (isBetween((n % 10), 5, 9)) || + (isBetween((n % 100), 11, 14))) + return 'many'; + if ((n % 10) == 1 && (n % 100) != 11) + return 'one'; + return 'other'; + }, + '12': function(n) { + if ((isBetween(n, 2, 4))) + return 'few'; + if (n == 1) + return 'one'; + return 'other'; + }, + '13': function(n) { + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) + return 'few'; + if (n != 1 && (isBetween((n % 10), 0, 1)) || + (isBetween((n % 10), 5, 9)) || + (isBetween((n % 100), 12, 14))) + return 'many'; + if (n == 1) + return 'one'; + return 'other'; + }, + '14': function(n) { + if ((isBetween((n % 100), 3, 4))) + return 'few'; + if ((n % 100) == 2) + return 'two'; + if ((n % 100) == 1) + return 'one'; + return 'other'; + }, + '15': function(n) { + if (n === 0 || (isBetween((n % 100), 2, 10))) + return 'few'; + if ((isBetween((n % 100), 11, 19))) + return 'many'; + if (n == 1) + return 'one'; + return 'other'; + }, + '16': function(n) { + if ((n % 10) == 1 && n != 11) + return 'one'; + return 'other'; + }, + '17': function(n) { + if (n == 3) + return 'few'; + if (n === 0) + return 'zero'; + if (n == 6) + return 'many'; + if (n == 2) + return 'two'; + if (n == 1) + return 'one'; + return 'other'; + }, + '18': function(n) { + if (n === 0) + return 'zero'; + if ((isBetween(n, 0, 2)) && n !== 0 && n != 2) + return 'one'; + return 'other'; + }, + '19': function(n) { + if ((isBetween(n, 2, 10))) + return 'few'; + if ((isBetween(n, 0, 1))) + return 'one'; + return 'other'; + }, + '20': function(n) { + if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !( + isBetween((n % 100), 10, 19) || + isBetween((n % 100), 70, 79) || + isBetween((n % 100), 90, 99) + )) + return 'few'; + if ((n % 1000000) === 0 && n !== 0) + return 'many'; + if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92])) + return 'two'; + if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91])) + return 'one'; + return 'other'; + }, + '21': function(n) { + if (n === 0) + return 'zero'; + if (n == 1) + return 'one'; + return 'other'; + }, + '22': function(n) { + if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) + return 'one'; + return 'other'; + }, + '23': function(n) { + if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) + return 'one'; + return 'other'; + }, + '24': function(n) { + if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) + return 'few'; + if (isIn(n, [2, 12])) + return 'two'; + if (isIn(n, [1, 11])) + return 'one'; + return 'other'; + } + }; + + // return a function that gives the plural form name for a given integer + var index = locales2rules[lang.replace(/-.*$/, '')]; + if (!(index in pluralRules)) { + console.warn('plural form unknown for [' + lang + ']'); + return function() { return 'other'; }; + } + return pluralRules[index]; + } + + // pre-defined 'plural' macro + gMacros.plural = function(str, param, key, prop) { + var n = parseFloat(param); + if (isNaN(n)) + return str; + + // TODO: support other properties (l20n still doesn't...) + if (prop != gTextProp) + return str; + + // initialize _pluralRules + if (!gMacros._pluralRules) { + gMacros._pluralRules = getPluralRules(gLanguage); + } + var index = '[' + gMacros._pluralRules(n) + ']'; + + // try to find a [zero|one|two] key if it's defined + if (n === 0 && (key + '[zero]') in gL10nData) { + str = gL10nData[key + '[zero]'][prop]; + } else if (n == 1 && (key + '[one]') in gL10nData) { + str = gL10nData[key + '[one]'][prop]; + } else if (n == 2 && (key + '[two]') in gL10nData) { + str = gL10nData[key + '[two]'][prop]; + } else if ((key + index) in gL10nData) { + str = gL10nData[key + index][prop]; + } else if ((key + '[other]') in gL10nData) { + str = gL10nData[key + '[other]'][prop]; + } + + return str; + }; + + + /** + * l10n dictionary functions + */ + + // fetch an l10n object, warn if not found, apply `args' if possible + function getL10nData(key, args, fallback) { + var data = gL10nData[key]; + if (!data) { + console.warn('#' + key + ' is undefined.'); + if (!fallback) { + return null; + } + data = fallback; + } + + /** This is where l10n expressions should be processed. + * The plan is to support C-style expressions from the l20n project; + * until then, only two kinds of simple expressions are supported: + * {[ index ]} and {{ arguments }}. + */ + var rv = {}; + for (var prop in data) { + var str = data[prop]; + str = substIndexes(str, args, key, prop); + str = substArguments(str, args, key); + rv[prop] = str; + } + return rv; + } + + // replace {[macros]} with their values + function substIndexes(str, args, key, prop) { + var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/; + var reMatch = reIndex.exec(str); + if (!reMatch || !reMatch.length) + return str; + + // an index/macro has been found + // Note: at the moment, only one parameter is supported + var macroName = reMatch[1]; + var paramName = reMatch[2]; + var param; + if (args && paramName in args) { + param = args[paramName]; + } else if (paramName in gL10nData) { + param = gL10nData[paramName]; + } + + // there's no macro parser yet: it has to be defined in gMacros + if (macroName in gMacros) { + var macro = gMacros[macroName]; + str = macro(str, param, key, prop); + } + return str; + } + + // replace {{arguments}} with their values + function substArguments(str, args, key) { + var reArgs = /\{\{\s*(.+?)\s*\}\}/g; + return str.replace(reArgs, function(matched_text, arg) { + if (args && arg in args) { + return args[arg]; + } + if (arg in gL10nData) { + return gL10nData[arg]; + } + console.log('argument {{' + arg + '}} for #' + key + ' is undefined.'); + return matched_text; + }); + } + + // translate an HTML element + function translateElement(element) { + var l10n = getL10nAttributes(element); + if (!l10n.id) + return; + + // get the related l10n object + var data = getL10nData(l10n.id, l10n.args); + if (!data) { + console.warn('#' + l10n.id + ' is undefined.'); + return; + } + + // translate element (TODO: security checks?) + if (data[gTextProp]) { // XXX + if (getChildElementCount(element) === 0) { + element[gTextProp] = data[gTextProp]; + } else { + // this element has element children: replace the content of the first + // (non-empty) child textNode and clear other child textNodes + var children = element.childNodes; + var found = false; + for (var i = 0, l = children.length; i < l; i++) { + if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) { + if (found) { + children[i].nodeValue = ''; + } else { + children[i].nodeValue = data[gTextProp]; + found = true; + } + } + } + // if no (non-empty) textNode is found, insert a textNode before the + // first element child. + if (!found) { + var textNode = document.createTextNode(data[gTextProp]); + element.insertBefore(textNode, element.firstChild); + } + } + delete data[gTextProp]; + } + + for (var k in data) { + element[k] = data[k]; + } + } + + // webkit browsers don't currently support 'children' on SVG elements... + function getChildElementCount(element) { + if (element.children) { + return element.children.length; + } + if (typeof element.childElementCount !== 'undefined') { + return element.childElementCount; + } + var count = 0; + for (var i = 0; i < element.childNodes.length; i++) { + count += element.nodeType === 1 ? 1 : 0; + } + return count; + } + + // translate an HTML subtree + function translateFragment(element) { + element = element || document.documentElement; + + // check all translatable children (= w/ a `data-l10n-id' attribute) + var children = getTranslatableChildren(element); + var elementCount = children.length; + for (var i = 0; i < elementCount; i++) { + translateElement(children[i]); + } + + // translate element itself if necessary + translateElement(element); + } + + return { + // get a localized string + get: function(key, args, fallbackString) { + var index = key.lastIndexOf('.'); + var prop = gTextProp; + if (index > 0) { // An attribute has been specified + prop = key.substr(index + 1); + key = key.substring(0, index); + } + var fallback; + if (fallbackString) { + fallback = {}; + fallback[prop] = fallbackString; + } + var data = getL10nData(key, args, fallback); + if (data && prop in data) { + return data[prop]; + } + return '{{' + key + '}}'; + }, + + // debug + getData: function() { return gL10nData; }, + getText: function() { return gTextData; }, + + // get|set the document language + getLanguage: function() { return gLanguage; }, + setLanguage: function(lang, callback) { + loadLocale(lang, function() { + if (callback) + callback(); + translateFragment(); + }); + }, + + // get the direction (ltr|rtl) of the current language + getDirection: function() { + // http://www.w3.org/International/questions/qa-scripts + // Arabic, Hebrew, Farsi, Pashto, Urdu + var rtlList = ['ar', 'he', 'fa', 'ps', 'ur']; + var shortCode = gLanguage.split('-', 1)[0]; + return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr'; + }, + + // translate an element or document fragment + translate: translateFragment, + + // this can be used to prevent race conditions + getReadyState: function() { return gReadyState; }, + ready: function(callback) { + if (!callback) { + return; + } else if (gReadyState == 'complete' || gReadyState == 'interactive') { + window.setTimeout(function() { + callback(); + }); + } else if (document.addEventListener) { + document.addEventListener('localized', function once() { + document.removeEventListener('localized', once); + callback(); + }); + } + } + }; +}) (window, document); diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/locale/locale.properties b/test-module-system/test-system-biz/src/main/resources/static/generic/web/locale/locale.properties new file mode 100644 index 0000000..9aded1b --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/locale/locale.properties @@ -0,0 +1,312 @@ +[ach] +@import url(ach/viewer.properties) + +[af] +@import url(af/viewer.properties) + +[ak] +@import url(ak/viewer.properties) + +[an] +@import url(an/viewer.properties) + +[ar] +@import url(ar/viewer.properties) + +[as] +@import url(as/viewer.properties) + +[ast] +@import url(ast/viewer.properties) + +[az] +@import url(az/viewer.properties) + +[be] +@import url(be/viewer.properties) + +[bg] +@import url(bg/viewer.properties) + +[bn-BD] +@import url(bn-BD/viewer.properties) + +[bn-IN] +@import url(bn-IN/viewer.properties) + +[br] +@import url(br/viewer.properties) + +[bs] +@import url(bs/viewer.properties) + +[ca] +@import url(ca/viewer.properties) + +[cs] +@import url(cs/viewer.properties) + +[csb] +@import url(csb/viewer.properties) + +[cy] +@import url(cy/viewer.properties) + +[da] +@import url(da/viewer.properties) + +[de] +@import url(de/viewer.properties) + +[el] +@import url(el/viewer.properties) + +[en-GB] +@import url(en-GB/viewer.properties) + +[en-US] +@import url(en-US/viewer.properties) + +[en-ZA] +@import url(en-ZA/viewer.properties) + +[eo] +@import url(eo/viewer.properties) + +[es-AR] +@import url(es-AR/viewer.properties) + +[es-CL] +@import url(es-CL/viewer.properties) + +[es-ES] +@import url(es-ES/viewer.properties) + +[es-MX] +@import url(es-MX/viewer.properties) + +[et] +@import url(et/viewer.properties) + +[eu] +@import url(eu/viewer.properties) + +[fa] +@import url(fa/viewer.properties) + +[ff] +@import url(ff/viewer.properties) + +[fi] +@import url(fi/viewer.properties) + +[fr] +@import url(fr/viewer.properties) + +[fy-NL] +@import url(fy-NL/viewer.properties) + +[ga-IE] +@import url(ga-IE/viewer.properties) + +[gd] +@import url(gd/viewer.properties) + +[gl] +@import url(gl/viewer.properties) + +[gu-IN] +@import url(gu-IN/viewer.properties) + +[he] +@import url(he/viewer.properties) + +[hi-IN] +@import url(hi-IN/viewer.properties) + +[hr] +@import url(hr/viewer.properties) + +[hu] +@import url(hu/viewer.properties) + +[hy-AM] +@import url(hy-AM/viewer.properties) + +[id] +@import url(id/viewer.properties) + +[is] +@import url(is/viewer.properties) + +[it] +@import url(it/viewer.properties) + +[ja] +@import url(ja/viewer.properties) + +[ka] +@import url(ka/viewer.properties) + +[kk] +@import url(kk/viewer.properties) + +[km] +@import url(km/viewer.properties) + +[kn] +@import url(kn/viewer.properties) + +[ko] +@import url(ko/viewer.properties) + +[ku] +@import url(ku/viewer.properties) + +[lg] +@import url(lg/viewer.properties) + +[lij] +@import url(lij/viewer.properties) + +[lt] +@import url(lt/viewer.properties) + +[lv] +@import url(lv/viewer.properties) + +[mai] +@import url(mai/viewer.properties) + +[mk] +@import url(mk/viewer.properties) + +[ml] +@import url(ml/viewer.properties) + +[mn] +@import url(mn/viewer.properties) + +[mr] +@import url(mr/viewer.properties) + +[ms] +@import url(ms/viewer.properties) + +[my] +@import url(my/viewer.properties) + +[nb-NO] +@import url(nb-NO/viewer.properties) + +[nl] +@import url(nl/viewer.properties) + +[nn-NO] +@import url(nn-NO/viewer.properties) + +[nso] +@import url(nso/viewer.properties) + +[oc] +@import url(oc/viewer.properties) + +[or] +@import url(or/viewer.properties) + +[pa-IN] +@import url(pa-IN/viewer.properties) + +[pl] +@import url(pl/viewer.properties) + +[pt-BR] +@import url(pt-BR/viewer.properties) + +[pt-PT] +@import url(pt-PT/viewer.properties) + +[rm] +@import url(rm/viewer.properties) + +[ro] +@import url(ro/viewer.properties) + +[ru] +@import url(ru/viewer.properties) + +[rw] +@import url(rw/viewer.properties) + +[sah] +@import url(sah/viewer.properties) + +[si] +@import url(si/viewer.properties) + +[sk] +@import url(sk/viewer.properties) + +[sl] +@import url(sl/viewer.properties) + +[son] +@import url(son/viewer.properties) + +[sq] +@import url(sq/viewer.properties) + +[sr] +@import url(sr/viewer.properties) + +[sv-SE] +@import url(sv-SE/viewer.properties) + +[sw] +@import url(sw/viewer.properties) + +[ta] +@import url(ta/viewer.properties) + +[ta-LK] +@import url(ta-LK/viewer.properties) + +[te] +@import url(te/viewer.properties) + +[th] +@import url(th/viewer.properties) + +[tl] +@import url(tl/viewer.properties) + +[tn] +@import url(tn/viewer.properties) + +[tr] +@import url(tr/viewer.properties) + +[uk] +@import url(uk/viewer.properties) + +[ur] +@import url(ur/viewer.properties) + +[vi] +@import url(vi/viewer.properties) + +[wo] +@import url(wo/viewer.properties) + +[xh] +@import url(xh/viewer.properties) + +[zh-CN] +@import url(zh-CN/viewer.properties) + +[zh-TW] +@import url(zh-TW/viewer.properties) + +[zu] +@import url(zu/viewer.properties) + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/locale/zh-CN/viewer.properties b/test-module-system/test-system-biz/src/main/resources/static/generic/web/locale/zh-CN/viewer.properties new file mode 100644 index 0000000..6ec25f7 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/locale/zh-CN/viewer.properties @@ -0,0 +1,167 @@ +# Copyright 2012 Mozilla Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Main toolbar buttons (tooltips and alt text for images) +previous.title=上一页 +previous_label=上一页 +next.title=下一页 +next_label=下一页 + +# LOCALIZATION NOTE (page_label, page_of): +# These strings are concatenated to form the "Page: X of Y" string. +# Do not translate "{{pageCount}}", it will be substituted with a number +# representing the total number of pages. +page_label=页面: +page_of=/ {{pageCount}} + +zoom_out.title=缩小 +zoom_out_label=缩小 +zoom_in.title=放大 +zoom_in_label=放大 +zoom.title=缩放 +presentation_mode.title=切换到演示模式 +presentation_mode_label=演示模式 +open_file.title=打开文件 +open_file_label=打开 +print.title=打印 +print_label=打印 +download.title=下载 +download_label=下载 +bookmark.title=当前视图(复制或在新窗口中打开) +bookmark_label=当前视图 + +# Secondary toolbar and context menu +tools.title=工具 +tools_label=工具 +first_page.title=转到第一页 +first_page.label=转到第一页 +first_page_label=转到第一页 +last_page.title=转到最后一页 +last_page.label=转到最后一页 +last_page_label=转到最后一页 +page_rotate_cw.title=顺时针旋转 +page_rotate_cw.label=顺时针旋转 +page_rotate_cw_label=顺时针旋转 +page_rotate_ccw.title=逆时针旋转 +page_rotate_ccw.label=逆时针旋转 +page_rotate_ccw_label=逆时针旋转 + +hand_tool_enable.title=启用手形工具 +hand_tool_enable_label=启用手形工具 +hand_tool_disable.title=禁用手形工具 +hand_tool_disable_label=禁用手形工具 + +# Document properties dialog box +document_properties.title=文档属性… +document_properties_label=文档属性… +document_properties_file_name=文件名: +document_properties_file_size=文件大小: +document_properties_kb={{size_kb}} KB ({{size_b}} 字节) +document_properties_mb={{size_mb}} MB ({{size_b}} 字节) +document_properties_title=标题: +document_properties_author=作者: +document_properties_subject=主题: +document_properties_keywords=关键词: +document_properties_creation_date=创建日期: +document_properties_modification_date=修改日期: +document_properties_date_string={{date}}, {{time}} +document_properties_creator=创建者: +document_properties_producer=PDF 制作者: +document_properties_version=PDF 版本: +document_properties_page_count=页数: +document_properties_close=关闭 + +# Tooltips and alt text for side panel toolbar buttons +# (the _label strings are alt text for the buttons, the .title strings are +# tooltips) +toggle_sidebar.title=切换侧栏 +toggle_sidebar_label=切换侧栏 +outline.title=显示文档大纲 +outline_label=文档大纲 +attachments.title=显示附件 +attachments_label=附件 +thumbs.title=显示缩略图 +thumbs_label=缩略图 +findbar.title=在文档中查找 +findbar_label=查找 + +# Thumbnails panel item (tooltip and alt text for images) +# LOCALIZATION NOTE (thumb_page_title): "{{page}}" will be replaced by the page +# number. +thumb_page_title=页码 {{page}} +# LOCALIZATION NOTE (thumb_page_canvas): "{{page}}" will be replaced by the page +# number. +thumb_page_canvas=页面 {{page}} 的缩略图 + +# Find panel button title and messages +find_label=查找: +find_previous.title=查找词语上一次出现的位置 +find_previous_label=上一页 +find_next.title=查找词语后一次出现的位置 +find_next_label=下一页 +find_highlight=全部高亮显示 +find_match_case_label=区分大小写 +find_reached_top=到达文档开头,从末尾继续 +find_reached_bottom=到达文档末尾,从开头继续 +find_not_found=词语未找到 + +# Error panel labels +error_more_info=更多信息 +error_less_info=更少信息 +error_close=关闭 +# LOCALIZATION NOTE (error_version_info): "{{version}}" and "{{build}}" will be +# replaced by the PDF.JS version and build ID. +error_version_info=PDF.js v{{version}} (build: {{build}}) +# LOCALIZATION NOTE (error_message): "{{message}}" will be replaced by an +# english string describing the error. +error_message=信息:{{message}} +# LOCALIZATION NOTE (error_stack): "{{stack}}" will be replaced with a stack +# trace. +error_stack=堆栈:{{stack}} +# LOCALIZATION NOTE (error_file): "{{file}}" will be replaced with a filename +error_file=文件:{{file}} +# LOCALIZATION NOTE (error_line): "{{line}}" will be replaced with a line number +error_line=行号:{{line}} +rendering_error=渲染页面时发生错误。 + +# Predefined zoom values +page_scale_width=适合页宽 +page_scale_fit=适合页面 +page_scale_auto=自动缩放 +page_scale_actual=实际大小 +# LOCALIZATION NOTE (page_scale_percent): "{{scale}}" will be replaced by a +# numerical scale value. +page_scale_percent={{scale}}% + +# Loading indicator messages +loading_error_indicator=错误 +loading_error=载入PDF时发生错误。 +invalid_file_error=无效或损坏的PDF文件。 +missing_file_error=缺少PDF文件。 +unexpected_response_error=意外的服务器响应。 + +# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip. +# "{{type}}" will be replaced with an annotation type from a list defined in +# the PDF spec (32000-1:2008 Table 169 – Annotation types). +# Some common types are e.g.: "Check", "Text", "Comment", "Note" +text_annotation_type.alt=[{{type}} 注解] +password_label=输入密码以打开此 PDF 文件。 +password_invalid=密码无效。请重试。 +password_ok=确定 +password_cancel=取消 + +printing_not_supported=警告:打印功能不完全支持此浏览器。 +printing_not_ready=警告:该 PDF 未完全加载以供打印。 +web_fonts_disabled=Web 字体已被禁用:无法使用嵌入的PDF字体。 +document_colors_disabled=不允许 PDF 文档使用自己的颜色:浏览器中“允许页面选择自己的颜色”的选项已停用。 diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/viewer.css b/test-module-system/test-system-biz/src/main/resources/static/generic/web/viewer.css new file mode 100644 index 0000000..a82150c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/viewer.css @@ -0,0 +1,1999 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.textLayer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + opacity: 0.2; +} + +.textLayer > div { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + -webkit-transform-origin: 0% 0%; + -moz-transform-origin: 0% 0%; + -o-transform-origin: 0% 0%; + -ms-transform-origin: 0% 0%; + transform-origin: 0% 0%; +} + +.textLayer .highlight { + margin: -1px; + padding: 1px; + + background-color: rgb(180, 0, 170); + border-radius: 4px; +} + +.textLayer .highlight.begin { + border-radius: 4px 0px 0px 4px; +} + +.textLayer .highlight.end { + border-radius: 0px 4px 4px 0px; +} + +.textLayer .highlight.middle { + border-radius: 0px; +} + +.textLayer .highlight.selected { + background-color: rgb(0, 100, 0); +} + +.textLayer ::selection { background: rgb(0,0,255); } +.textLayer ::-moz-selection { background: rgb(0,0,255); } + +.pdfViewer .canvasWrapper { + overflow: hidden; +} + +.pdfViewer .page { + direction: ltr; + width: 816px; + height: 1056px; + margin: 1px auto -8px auto; + position: relative; + overflow: visible; + border: 9px solid transparent; + background-clip: content-box; + border-image: url(images/shadow.png) 9 9 repeat; + background-color: white; +} + +.pdfViewer.removePageBorders .page { + margin: 0px auto 10px auto; + border: none; +} + +.pdfViewer .page canvas { + margin: 0; + display: block; +} + +.pdfViewer .page .loadingIcon { + position: absolute; + display: block; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: url('images/loading-icon.gif') center no-repeat; +} + +.pdfViewer .page .annotLink > a:hover { + opacity: 0.2; + background: #ff0; + box-shadow: 0px 2px 10px #ff0; +} + +.pdfPresentationMode:-webkit-full-screen .pdfViewer .page { + margin-bottom: 100%; + border: 0; +} + +.pdfPresentationMode:-moz-full-screen .pdfViewer .page { + margin-bottom: 100%; + border: 0; +} + +.pdfPresentationMode:-ms-fullscreen .pdfViewer .page { + margin-bottom: 100% !important; + border: 0; +} + +.pdfPresentationMode:fullscreen .pdfViewer .page { + margin-bottom: 100%; + border: 0; +} + +.pdfViewer .page .annotText > img { + position: absolute; + cursor: pointer; +} + +.pdfViewer .page .annotTextContentWrapper { + position: absolute; + width: 20em; +} + +.pdfViewer .page .annotTextContent { + z-index: 200; + float: left; + max-width: 20em; + background-color: #FFFF99; + box-shadow: 0px 2px 5px #333; + border-radius: 2px; + padding: 0.6em; + cursor: pointer; +} + +.pdfViewer .page .annotTextContent > h1 { + font-size: 1em; + border-bottom: 1px solid #000000; + padding-bottom: 0.2em; +} + +.pdfViewer .page .annotTextContent > p { + padding-top: 0.2em; +} + +.pdfViewer .page .annotLink > a { + position: absolute; + font-size: 1em; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.pdfViewer .page .annotLink > a /* -ms-a */ { + background: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAA\ + LAAAAAABAAEAAAIBRAA7") 0 0 repeat; +} + +* { + padding: 0; + margin: 0; +} + +html { + height: 100%; + /* Font size is needed to make the activity bar the correct size. */ + font-size: 10px; +} + +body { + height: 100%; + background-color: #404040; + background-image: url(images/texture.png); +} + +body, +input, +button, +select { + font: message-box; + outline: none; +} + +.hidden { + display: none !important; +} +[hidden] { + display: none !important; +} + +#viewerContainer.pdfPresentationMode:-webkit-full-screen { + top: 0px; + border-top: 2px solid transparent; + background-color: #000; + width: 100%; + height: 100%; + overflow: hidden; + cursor: none; + -webkit-user-select: none; +} + +#viewerContainer.pdfPresentationMode:-moz-full-screen { + top: 0px; + border-top: 2px solid transparent; + background-color: #000; + width: 100%; + height: 100%; + overflow: hidden; + cursor: none; + -moz-user-select: none; +} + +#viewerContainer.pdfPresentationMode:-ms-fullscreen { + top: 0px !important; + border-top: 2px solid transparent; + width: 100%; + height: 100%; + overflow: hidden !important; + cursor: none; + -ms-user-select: none; +} + +#viewerContainer.pdfPresentationMode:-ms-fullscreen::-ms-backdrop { + background-color: #000; +} + +#viewerContainer.pdfPresentationMode:fullscreen { + top: 0px; + border-top: 2px solid transparent; + background-color: #000; + width: 100%; + height: 100%; + overflow: hidden; + cursor: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.pdfPresentationMode:-webkit-full-screen a:not(.internalLink) { + display: none; +} + +.pdfPresentationMode:-moz-full-screen a:not(.internalLink) { + display: none; +} + +.pdfPresentationMode:-ms-fullscreen a:not(.internalLink) { + display: none !important; +} + +.pdfPresentationMode:fullscreen a:not(.internalLink) { + display: none; +} + +.pdfPresentationMode:-webkit-full-screen .textLayer > div { + cursor: none; +} + +.pdfPresentationMode:-moz-full-screen .textLayer > div { + cursor: none; +} + +.pdfPresentationMode:-ms-fullscreen .textLayer > div { + cursor: none; +} + +.pdfPresentationMode:fullscreen .textLayer > div { + cursor: none; +} + +.pdfPresentationMode.pdfPresentationModeControls > *, +.pdfPresentationMode.pdfPresentationModeControls .textLayer > div { + cursor: default; +} + +/* outer/inner center provides horizontal center */ +.outerCenter { + pointer-events: none; + position: relative; +} +html[dir='ltr'] .outerCenter { + float: right; + right: 50%; +} +html[dir='rtl'] .outerCenter { + float: left; + left: 50%; +} +.innerCenter { + pointer-events: auto; + position: relative; +} +html[dir='ltr'] .innerCenter { + float: right; + right: -50%; +} +html[dir='rtl'] .innerCenter { + float: left; + left: -50%; +} + +#outerContainer { + width: 100%; + height: 100%; + position: relative; +} + +#sidebarContainer { + position: absolute; + top: 0; + bottom: 0; + width: 200px; + visibility: hidden; + -webkit-transition-duration: 200ms; + -webkit-transition-timing-function: ease; + transition-duration: 200ms; + transition-timing-function: ease; + +} +html[dir='ltr'] #sidebarContainer { + -webkit-transition-property: left; + transition-property: left; + left: -200px; +} +html[dir='rtl'] #sidebarContainer { + -webkit-transition-property: right; + transition-property: right; + right: -200px; +} + +#outerContainer.sidebarMoving > #sidebarContainer, +#outerContainer.sidebarOpen > #sidebarContainer { + visibility: visible; +} +html[dir='ltr'] #outerContainer.sidebarOpen > #sidebarContainer { + left: 0px; +} +html[dir='rtl'] #outerContainer.sidebarOpen > #sidebarContainer { + right: 0px; +} + +#mainContainer { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + min-width: 320px; + -webkit-transition-duration: 200ms; + -webkit-transition-timing-function: ease; + transition-duration: 200ms; + transition-timing-function: ease; +} +html[dir='ltr'] #outerContainer.sidebarOpen > #mainContainer { + -webkit-transition-property: left; + transition-property: left; + left: 200px; +} +html[dir='rtl'] #outerContainer.sidebarOpen > #mainContainer { + -webkit-transition-property: right; + transition-property: right; + right: 200px; +} + +#sidebarContent { + top: 32px; + bottom: 0; + overflow: auto; + -webkit-overflow-scrolling: touch; + position: absolute; + width: 200px; + background-color: hsla(0,0%,0%,.1); +} +html[dir='ltr'] #sidebarContent { + left: 0; + box-shadow: inset -1px 0 0 hsla(0,0%,0%,.25); +} +html[dir='rtl'] #sidebarContent { + right: 0; + box-shadow: inset 1px 0 0 hsla(0,0%,0%,.25); +} + +#viewerContainer { + overflow: auto; + -webkit-overflow-scrolling: touch; + position: absolute; + top: 32px; + right: 0; + bottom: 0; + left: 0; + outline: none; +} +html[dir='ltr'] #viewerContainer { + box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05); +} +html[dir='rtl'] #viewerContainer { + box-shadow: inset -1px 0 0 hsla(0,0%,100%,.05); +} + +.toolbar { + position: relative; + left: 0; + right: 0; + z-index: 9999; + cursor: default; +} + +#toolbarContainer { + width: 100%; +} + +#toolbarSidebar { + width: 200px; + height: 32px; + background-color: #424242; /* fallback */ + background-image: url(images/texture.png), + linear-gradient(hsla(0,0%,30%,.99), hsla(0,0%,25%,.95)); +} +html[dir='ltr'] #toolbarSidebar { + box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25), + inset 0 -1px 0 hsla(0,0%,100%,.05), + 0 1px 0 hsla(0,0%,0%,.15), + 0 0 1px hsla(0,0%,0%,.1); +} +html[dir='rtl'] #toolbarSidebar { + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.25), + inset 0 1px 0 hsla(0,0%,100%,.05), + 0 1px 0 hsla(0,0%,0%,.15), + 0 0 1px hsla(0,0%,0%,.1); +} + +#toolbarContainer, .findbar, .secondaryToolbar { + position: relative; + height: 32px; + background-color: #474747; /* fallback */ + background-image: url(images/texture.png), + linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); +} +html[dir='ltr'] #toolbarContainer, .findbar, .secondaryToolbar { + box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08), + inset 0 1px 1px hsla(0,0%,0%,.15), + inset 0 -1px 0 hsla(0,0%,100%,.05), + 0 1px 0 hsla(0,0%,0%,.15), + 0 1px 1px hsla(0,0%,0%,.1); +} +html[dir='rtl'] #toolbarContainer, .findbar, .secondaryToolbar { + box-shadow: inset -1px 0 0 hsla(0,0%,100%,.08), + inset 0 1px 1px hsla(0,0%,0%,.15), + inset 0 -1px 0 hsla(0,0%,100%,.05), + 0 1px 0 hsla(0,0%,0%,.15), + 0 1px 1px hsla(0,0%,0%,.1); +} + +#toolbarViewer { + height: 32px; +} + +#loadingBar { + position: relative; + width: 100%; + height: 4px; + background-color: #333; + border-bottom: 1px solid #333; +} + +#loadingBar .progress { + position: absolute; + top: 0; + left: 0; + width: 0%; + height: 100%; + background-color: #ddd; + overflow: hidden; + -webkit-transition: width 200ms; + transition: width 200ms; +} + +@-webkit-keyframes progressIndeterminate { + 0% { left: 0%; } + 50% { left: 100%; } + 100% { left: 100%; } +} + +@keyframes progressIndeterminate { + 0% { left: 0%; } + 50% { left: 100%; } + 100% { left: 100%; } +} + +#loadingBar .progress.indeterminate { + background-color: #999; + -webkit-transition: none; + transition: none; +} + +#loadingBar .indeterminate .glimmer { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 50px; + + background-image: linear-gradient(to right, #999 0%, #fff 50%, #999 100%); + background-size: 100% 100%; + background-repeat: no-repeat; + + -webkit-animation: progressIndeterminate 2s linear infinite; + animation: progressIndeterminate 2s linear infinite; +} + +.findbar, .secondaryToolbar { + top: 32px; + position: absolute; + z-index: 10000; + height: 32px; + + min-width: 16px; + padding: 0px 6px 0px 6px; + margin: 4px 2px 4px 2px; + color: hsl(0,0%,85%); + font-size: 12px; + line-height: 14px; + text-align: left; + cursor: default; +} + +html[dir='ltr'] .findbar { + left: 68px; +} + +html[dir='rtl'] .findbar { + right: 68px; +} + +.findbar label { + -webkit-user-select: none; + -moz-user-select: none; +} + +#findInput[data-status="pending"] { + background-image: url(images/loading-small.png); + background-repeat: no-repeat; + background-position: right; +} +html[dir='rtl'] #findInput[data-status="pending"] { + background-position: left; +} + +.secondaryToolbar { + padding: 6px; + height: auto; + z-index: 30000; +} +html[dir='ltr'] .secondaryToolbar { + right: 4px; +} +html[dir='rtl'] .secondaryToolbar { + left: 4px; +} + +#secondaryToolbarButtonContainer { + max-width: 200px; + max-height: 400px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + margin-bottom: -4px; +} + +.doorHanger, +.doorHangerRight { + border: 1px solid hsla(0,0%,0%,.5); + border-radius: 2px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} +.doorHanger:after, .doorHanger:before, +.doorHangerRight:after, .doorHangerRight:before { + bottom: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} +.doorHanger:after, +.doorHangerRight:after { + border-bottom-color: hsla(0,0%,32%,.99); + border-width: 8px; +} +.doorHanger:before, +.doorHangerRight:before { + border-bottom-color: hsla(0,0%,0%,.5); + border-width: 9px; +} + +html[dir='ltr'] .doorHanger:after, +html[dir='rtl'] .doorHangerRight:after { + left: 13px; + margin-left: -8px; +} + +html[dir='ltr'] .doorHanger:before, +html[dir='rtl'] .doorHangerRight:before { + left: 13px; + margin-left: -9px; +} + +html[dir='rtl'] .doorHanger:after, +html[dir='ltr'] .doorHangerRight:after { + right: 13px; + margin-right: -8px; +} + +html[dir='rtl'] .doorHanger:before, +html[dir='ltr'] .doorHangerRight:before { + right: 13px; + margin-right: -9px; +} + +#findMsg { + font-style: italic; + color: #A6B7D0; +} + +#findInput.notFound { + background-color: rgb(255, 102, 102); +} + +html[dir='ltr'] #toolbarViewerLeft { + margin-left: -1px; +} +html[dir='rtl'] #toolbarViewerRight { + margin-right: -1px; +} + +html[dir='ltr'] #toolbarViewerLeft, +html[dir='rtl'] #toolbarViewerRight { + position: absolute; + top: 0; + left: 0; +} +html[dir='ltr'] #toolbarViewerRight, +html[dir='rtl'] #toolbarViewerLeft { + position: absolute; + top: 0; + right: 0; +} +html[dir='ltr'] #toolbarViewerLeft > *, +html[dir='ltr'] #toolbarViewerMiddle > *, +html[dir='ltr'] #toolbarViewerRight > *, +html[dir='ltr'] .findbar > * { + position: relative; + float: left; +} +html[dir='rtl'] #toolbarViewerLeft > *, +html[dir='rtl'] #toolbarViewerMiddle > *, +html[dir='rtl'] #toolbarViewerRight > *, +html[dir='rtl'] .findbar > * { + position: relative; + float: right; +} + +html[dir='ltr'] .splitToolbarButton { + margin: 3px 2px 4px 0; + display: inline-block; +} +html[dir='rtl'] .splitToolbarButton { + margin: 3px 0 4px 2px; + display: inline-block; +} +html[dir='ltr'] .splitToolbarButton > .toolbarButton { + border-radius: 0; + float: left; +} +html[dir='rtl'] .splitToolbarButton > .toolbarButton { + border-radius: 0; + float: right; +} + +.toolbarButton, +.secondaryToolbarButton, +.overlayButton { + border: 0 none; + background: none; + width: 32px; + height: 25px; +} + +.toolbarButton > span { + display: inline-block; + width: 0; + height: 0; + overflow: hidden; +} + +.toolbarButton[disabled], +.secondaryToolbarButton[disabled], +.overlayButton[disabled] { + opacity: .5; +} + +.toolbarButton.group { + margin-right: 0; +} + +.splitToolbarButton.toggled .toolbarButton { + margin: 0; +} + +.splitToolbarButton:hover > .toolbarButton, +.splitToolbarButton:focus > .toolbarButton, +.splitToolbarButton.toggled > .toolbarButton, +.toolbarButton.textButton { + background-color: hsla(0,0%,0%,.12); + background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); + background-clip: padding-box; + border: 1px solid hsla(0,0%,0%,.35); + border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42); + box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, + 0 0 1px hsla(0,0%,100%,.15) inset, + 0 1px 0 hsla(0,0%,100%,.05); + -webkit-transition-property: background-color, border-color, box-shadow; + -webkit-transition-duration: 150ms; + -webkit-transition-timing-function: ease; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; + transition-timing-function: ease; + +} +.splitToolbarButton > .toolbarButton:hover, +.splitToolbarButton > .toolbarButton:focus, +.dropdownToolbarButton:hover, +.overlayButton:hover, +.toolbarButton.textButton:hover, +.toolbarButton.textButton:focus { + background-color: hsla(0,0%,0%,.2); + box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, + 0 0 1px hsla(0,0%,100%,.15) inset, + 0 0 1px hsla(0,0%,0%,.05); + z-index: 199; +} +.splitToolbarButton > .toolbarButton { + position: relative; +} +html[dir='ltr'] .splitToolbarButton > .toolbarButton:first-child, +html[dir='rtl'] .splitToolbarButton > .toolbarButton:last-child { + position: relative; + margin: 0; + margin-right: -1px; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + border-right-color: transparent; +} +html[dir='ltr'] .splitToolbarButton > .toolbarButton:last-child, +html[dir='rtl'] .splitToolbarButton > .toolbarButton:first-child { + position: relative; + margin: 0; + margin-left: -1px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-left-color: transparent; +} +.splitToolbarButtonSeparator { + padding: 8px 0; + width: 1px; + background-color: hsla(0,0%,0%,.5); + z-index: 99; + box-shadow: 0 0 0 1px hsla(0,0%,100%,.08); + display: inline-block; + margin: 5px 0; +} +html[dir='ltr'] .splitToolbarButtonSeparator { + float: left; +} +html[dir='rtl'] .splitToolbarButtonSeparator { + float: right; +} +.splitToolbarButton:hover > .splitToolbarButtonSeparator, +.splitToolbarButton.toggled > .splitToolbarButtonSeparator { + padding: 12px 0; + margin: 1px 0; + box-shadow: 0 0 0 1px hsla(0,0%,100%,.03); + -webkit-transition-property: padding; + -webkit-transition-duration: 10ms; + -webkit-transition-timing-function: ease; + transition-property: padding; + transition-duration: 10ms; + transition-timing-function: ease; +} + +.toolbarButton, +.dropdownToolbarButton, +.secondaryToolbarButton, +.overlayButton { + min-width: 16px; + padding: 2px 6px 0; + border: 1px solid transparent; + border-radius: 2px; + color: hsla(0,0%,100%,.8); + font-size: 12px; + line-height: 14px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + /* Opera does not support user-select, use <... unselectable="on"> instead */ + cursor: default; + -webkit-transition-property: background-color, border-color, box-shadow; + -webkit-transition-duration: 150ms; + -webkit-transition-timing-function: ease; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; + transition-timing-function: ease; +} + +html[dir='ltr'] .toolbarButton, +html[dir='ltr'] .overlayButton, +html[dir='ltr'] .dropdownToolbarButton { + margin: 3px 2px 4px 0; +} +html[dir='rtl'] .toolbarButton, +html[dir='rtl'] .overlayButton, +html[dir='rtl'] .dropdownToolbarButton { + margin: 3px 0 4px 2px; +} + +.toolbarButton:hover, +.toolbarButton:focus, +.dropdownToolbarButton, +.overlayButton, +.secondaryToolbarButton:hover, +.secondaryToolbarButton:focus { + background-color: hsla(0,0%,0%,.12); + background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); + background-clip: padding-box; + border: 1px solid hsla(0,0%,0%,.35); + border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42); + box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset, + 0 0 1px hsla(0,0%,100%,.15) inset, + 0 1px 0 hsla(0,0%,100%,.05); +} + +.toolbarButton:hover:active, +.overlayButton:hover:active, +.dropdownToolbarButton:hover:active, +.secondaryToolbarButton:hover:active { + background-color: hsla(0,0%,0%,.2); + background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); + border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45); + box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, + 0 0 1px hsla(0,0%,0%,.2) inset, + 0 1px 0 hsla(0,0%,100%,.05); + -webkit-transition-property: background-color, border-color, box-shadow; + -webkit-transition-duration: 10ms; + -webkit-transition-timing-function: linear; + transition-property: background-color, border-color, box-shadow; + transition-duration: 10ms; + transition-timing-function: linear; +} + +.toolbarButton.toggled, +.splitToolbarButton.toggled > .toolbarButton.toggled, +.secondaryToolbarButton.toggled { + background-color: hsla(0,0%,0%,.3); + background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); + border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5); + box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset, + 0 0 1px hsla(0,0%,0%,.2) inset, + 0 1px 0 hsla(0,0%,100%,.05); + -webkit-transition-property: background-color, border-color, box-shadow; + -webkit-transition-duration: 10ms; + -webkit-transition-timing-function: linear; + transition-property: background-color, border-color, box-shadow; + transition-duration: 10ms; + transition-timing-function: linear; +} + +.toolbarButton.toggled:hover:active, +.splitToolbarButton.toggled > .toolbarButton.toggled:hover:active, +.secondaryToolbarButton.toggled:hover:active { + background-color: hsla(0,0%,0%,.4); + border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.5) hsla(0,0%,0%,.55); + box-shadow: 0 1px 1px hsla(0,0%,0%,.2) inset, + 0 0 1px hsla(0,0%,0%,.3) inset, + 0 1px 0 hsla(0,0%,100%,.05); +} + +.dropdownToolbarButton { + width: 120px; + max-width: 120px; + padding: 3px 2px 2px; + overflow: hidden; + background: url(images/toolbarButton-menuArrows.png) no-repeat; +} +html[dir='ltr'] .dropdownToolbarButton { + background-position: 95%; +} +html[dir='rtl'] .dropdownToolbarButton { + background-position: 5%; +} + +.dropdownToolbarButton > select { + min-width: 140px; + font-size: 12px; + color: hsl(0,0%,95%); + margin: 0; + padding: 0; + border: none; + background: rgba(0,0,0,0); /* Opera does not support 'transparent' +
+ +
+ +
+ + + + + + + + + +
+
+
+
+ +
+ +
+ +
+ +
+ + + +
+
+ + + + + + + + + Current View + + +
+ + +
+
+
+
+ +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + +
+
+
+ + + + + + + +
+ + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/generic/web/viewer.js b/test-module-system/test-system-biz/src/main/resources/static/generic/web/viewer.js new file mode 100644 index 0000000..7322208 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/generic/web/viewer.js @@ -0,0 +1,7614 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, ProgressBar, + DownloadManager, getFileName, getPDFFileNameFromURL, + PDFHistory, Preferences, SidebarView, ViewHistory, Stats, + PDFThumbnailViewer, URL, noContextMenuHandler, SecondaryToolbar, + PasswordPrompt, PDFPresentationMode, HandTool, Promise, + PDFDocumentProperties, PDFOutlineView, PDFAttachmentView, + OverlayManager, PDFFindController, PDFFindBar, getVisibleElements, + watchScroll, PDFViewer, PDFRenderingQueue, PresentationModeState, + RenderingStates, DEFAULT_SCALE, UNKNOWN_SCALE, + IGNORE_CURRENT_POSITION_ON_ZOOM: true */ + +'use strict'; + +var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf'; +var DEFAULT_SCALE_DELTA = 1.1; +var MIN_SCALE = 0.25; +var MAX_SCALE = 10.0; +var VIEW_HISTORY_MEMORY = 20; +var SCALE_SELECT_CONTAINER_PADDING = 8; +var SCALE_SELECT_PADDING = 22; +var PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading'; +var DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000; + +PDFJS.imageResourcesPath = './images/'; + PDFJS.workerSrc = '../build/pdf.worker.js'; + PDFJS.cMapUrl = '../web/cmaps/'; + PDFJS.cMapPacked = true; + +var mozL10n = document.mozL10n || document.webL10n; + + +var CSS_UNITS = 96.0 / 72.0; +var DEFAULT_SCALE = 'auto'; +var UNKNOWN_SCALE = 0; +var MAX_AUTO_SCALE = 1.25; +var SCROLLBAR_PADDING = 40; +var VERTICAL_PADDING = 5; + +// optimised CSS custom property getter/setter +var CustomStyle = (function CustomStyleClosure() { + + // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ + // animate-css-transforms-firefox-webkit.html + // in some versions of IE9 it is critical that ms appear in this list + // before Moz + var prefixes = ['ms', 'Moz', 'Webkit', 'O']; + var _cache = {}; + + function CustomStyle() {} + + CustomStyle.getProp = function get(propName, element) { + // check cache only when no element is given + if (arguments.length === 1 && typeof _cache[propName] === 'string') { + return _cache[propName]; + } + + element = element || document.documentElement; + var style = element.style, prefixed, uPropName; + + // test standard property first + if (typeof style[propName] === 'string') { + return (_cache[propName] = propName); + } + + // capitalize + uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); + + // test vendor specific properties + for (var i = 0, l = prefixes.length; i < l; i++) { + prefixed = prefixes[i] + uPropName; + if (typeof style[prefixed] === 'string') { + return (_cache[propName] = prefixed); + } + } + + //if all fails then set to undefined + return (_cache[propName] = 'undefined'); + }; + + CustomStyle.setProp = function set(propName, element, str) { + var prop = this.getProp(propName); + if (prop !== 'undefined') { + element.style[prop] = str; + } + }; + + return CustomStyle; +})(); + +function getFileName(url) { + var anchor = url.indexOf('#'); + var query = url.indexOf('?'); + var end = Math.min( + anchor > 0 ? anchor : url.length, + query > 0 ? query : url.length); + return url.substring(url.lastIndexOf('/', end) + 1, end); +} + +/** + * Returns scale factor for the canvas. It makes sense for the HiDPI displays. + * @return {Object} The object with horizontal (sx) and vertical (sy) + scales. The scaled property is set to false if scaling is + not required, true otherwise. + */ +function getOutputScale(ctx) { + var devicePixelRatio = window.devicePixelRatio || 1; + var backingStoreRatio = ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1; + var pixelRatio = devicePixelRatio / backingStoreRatio; + return { + sx: pixelRatio, + sy: pixelRatio, + scaled: pixelRatio !== 1 + }; +} + +/** + * Scrolls specified element into view of its parent. + * element {Object} The element to be visible. + * spot {Object} An object with optional top and left properties, + * specifying the offset from the top left edge. + */ +function scrollIntoView(element, spot) { + // Assuming offsetParent is available (it's not available when viewer is in + // hidden iframe or object). We have to scroll: if the offsetParent is not set + // producing the error. See also animationStartedClosure. + var parent = element.offsetParent; + var offsetY = element.offsetTop + element.clientTop; + var offsetX = element.offsetLeft + element.clientLeft; + if (!parent) { + console.error('offsetParent is not set -- cannot scroll'); + return; + } + while (parent.clientHeight === parent.scrollHeight) { + if (parent.dataset._scaleY) { + offsetY /= parent.dataset._scaleY; + offsetX /= parent.dataset._scaleX; + } + offsetY += parent.offsetTop; + offsetX += parent.offsetLeft; + parent = parent.offsetParent; + if (!parent) { + return; // no need to scroll + } + } + if (spot) { + if (spot.top !== undefined) { + offsetY += spot.top; + } + if (spot.left !== undefined) { + offsetX += spot.left; + parent.scrollLeft = offsetX; + } + } + parent.scrollTop = offsetY; +} + +/** + * Helper function to start monitoring the scroll event and converting them into + * PDF.js friendly one: with scroll debounce and scroll direction. + */ +function watchScroll(viewAreaElement, callback) { + var debounceScroll = function debounceScroll(evt) { + if (rAF) { + return; + } + // schedule an invocation of scroll for next animation frame. + rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { + rAF = null; + + var currentY = viewAreaElement.scrollTop; + var lastY = state.lastY; + if (currentY !== lastY) { + state.down = currentY > lastY; + } + state.lastY = currentY; + callback(state); + }); + }; + + var state = { + down: true, + lastY: viewAreaElement.scrollTop, + _eventHandler: debounceScroll + }; + + var rAF = null; + viewAreaElement.addEventListener('scroll', debounceScroll, true); + return state; +} + +/** + * Use binary search to find the index of the first item in a given array which + * passes a given condition. The items are expected to be sorted in the sense + * that if the condition is true for one item in the array, then it is also true + * for all following items. + * + * @returns {Number} Index of the first array element to pass the test, + * or |items.length| if no such element exists. + */ +function binarySearchFirstItem(items, condition) { + var minIndex = 0; + var maxIndex = items.length - 1; + + if (items.length === 0 || !condition(items[maxIndex])) { + return items.length; + } + if (condition(items[minIndex])) { + return minIndex; + } + + while (minIndex < maxIndex) { + var currentIndex = (minIndex + maxIndex) >> 1; + var currentItem = items[currentIndex]; + if (condition(currentItem)) { + maxIndex = currentIndex; + } else { + minIndex = currentIndex + 1; + } + } + return minIndex; /* === maxIndex */ +} + +/** + * Generic helper to find out what elements are visible within a scroll pane. + */ +function getVisibleElements(scrollEl, views, sortByVisibility) { + var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; + var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; + + function isElementBottomBelowViewTop(view) { + var element = view.div; + var elementBottom = + element.offsetTop + element.clientTop + element.clientHeight; + return elementBottom > top; + } + + var visible = [], view, element; + var currentHeight, viewHeight, hiddenHeight, percentHeight; + var currentWidth, viewWidth; + var firstVisibleElementInd = (views.length === 0) ? 0 : + binarySearchFirstItem(views, isElementBottomBelowViewTop); + + for (var i = firstVisibleElementInd, ii = views.length; i < ii; i++) { + view = views[i]; + element = view.div; + currentHeight = element.offsetTop + element.clientTop; + viewHeight = element.clientHeight; + + if (currentHeight > bottom) { + break; + } + + currentWidth = element.offsetLeft + element.clientLeft; + viewWidth = element.clientWidth; + if (currentWidth + viewWidth < left || currentWidth > right) { + continue; + } + hiddenHeight = Math.max(0, top - currentHeight) + + Math.max(0, currentHeight + viewHeight - bottom); + percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; + + visible.push({ + id: view.id, + x: currentWidth, + y: currentHeight, + view: view, + percent: percentHeight + }); + } + + var first = visible[0]; + var last = visible[visible.length - 1]; + + if (sortByVisibility) { + visible.sort(function(a, b) { + var pc = a.percent - b.percent; + if (Math.abs(pc) > 0.001) { + return -pc; + } + return a.id - b.id; // ensure stability + }); + } + return {first: first, last: last, views: visible}; +} + +/** + * Event handler to suppress context menu. + */ +function noContextMenuHandler(e) { + e.preventDefault(); +} + +/** + * Returns the filename or guessed filename from the url (see issue 3455). + * url {String} The original PDF location. + * @return {String} Guessed PDF file name. + */ +function getPDFFileNameFromURL(url) { + var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/; + // SCHEME HOST 1.PATH 2.QUERY 3.REF + // Pattern to get last matching NAME.pdf + var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i; + var splitURI = reURI.exec(url); + var suggestedFilename = reFilename.exec(splitURI[1]) || + reFilename.exec(splitURI[2]) || + reFilename.exec(splitURI[3]); + if (suggestedFilename) { + suggestedFilename = suggestedFilename[0]; + if (suggestedFilename.indexOf('%') !== -1) { + // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf + try { + suggestedFilename = + reFilename.exec(decodeURIComponent(suggestedFilename))[0]; + } catch(e) { // Possible (extremely rare) errors: + // URIError "Malformed URI", e.g. for "%AA.pdf" + // TypeError "null has no properties", e.g. for "%2F.pdf" + } + } + } + return suggestedFilename || 'document.pdf'; +} + +var ProgressBar = (function ProgressBarClosure() { + + function clamp(v, min, max) { + return Math.min(Math.max(v, min), max); + } + + function ProgressBar(id, opts) { + this.visible = true; + + // Fetch the sub-elements for later. + this.div = document.querySelector(id + ' .progress'); + + // Get the loading bar element, so it can be resized to fit the viewer. + this.bar = this.div.parentNode; + + // Get options, with sensible defaults. + this.height = opts.height || 100; + this.width = opts.width || 100; + this.units = opts.units || '%'; + + // Initialize heights. + this.div.style.height = this.height + this.units; + this.percent = 0; + } + + ProgressBar.prototype = { + + updateBar: function ProgressBar_updateBar() { + if (this._indeterminate) { + this.div.classList.add('indeterminate'); + this.div.style.width = this.width + this.units; + return; + } + + this.div.classList.remove('indeterminate'); + var progressSize = this.width * this._percent / 100; + this.div.style.width = progressSize + this.units; + }, + + get percent() { + return this._percent; + }, + + set percent(val) { + this._indeterminate = isNaN(val); + this._percent = clamp(val, 0, 100); + this.updateBar(); + }, + + setWidth: function ProgressBar_setWidth(viewer) { + if (viewer) { + var container = viewer.parentNode; + var scrollbarWidth = container.offsetWidth - viewer.offsetWidth; + if (scrollbarWidth > 0) { + this.bar.setAttribute('style', 'width: calc(100% - ' + + scrollbarWidth + 'px);'); + } + } + }, + + hide: function ProgressBar_hide() { + if (!this.visible) { + return; + } + this.visible = false; + this.bar.classList.add('hidden'); + document.body.classList.remove('loadingInProgress'); + }, + + show: function ProgressBar_show() { + if (this.visible) { + return; + } + this.visible = true; + document.body.classList.add('loadingInProgress'); + this.bar.classList.remove('hidden'); + } + }; + + return ProgressBar; +})(); + + + +var DEFAULT_PREFERENCES = { + showPreviousViewOnLoad: true, + defaultZoomValue: '', + sidebarViewOnLoad: 0, + enableHandToolOnLoad: false, + enableWebGL: false, + pdfBugEnabled: false, + disableRange: false, + disableStream: false, + disableAutoFetch: false, + disableFontFace: false, + disableTextLayer: false, + useOnlyCssZoom: false +}; + + +var SidebarView = { + NONE: 0, + THUMBS: 1, + OUTLINE: 2, + ATTACHMENTS: 3 +}; + +/** + * Preferences - Utility for storing persistent settings. + * Used for settings that should be applied to all opened documents, + * or every time the viewer is loaded. + */ +var Preferences = { + prefs: Object.create(DEFAULT_PREFERENCES), + isInitializedPromiseResolved: false, + initializedPromise: null, + + /** + * Initialize and fetch the current preference values from storage. + * @return {Promise} A promise that is resolved when the preferences + * have been initialized. + */ + initialize: function preferencesInitialize() { + return this.initializedPromise = + this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) { + this.isInitializedPromiseResolved = true; + if (prefObj) { + this.prefs = prefObj; + } + }.bind(this)); + }, + + /** + * Stub function for writing preferences to storage. + * NOTE: This should be overridden by a build-specific function defined below. + * @param {Object} prefObj The preferences that should be written to storage. + * @return {Promise} A promise that is resolved when the preference values + * have been written. + */ + _writeToStorage: function preferences_writeToStorage(prefObj) { + return Promise.resolve(); + }, + + /** + * Stub function for reading preferences from storage. + * NOTE: This should be overridden by a build-specific function defined below. + * @param {Object} prefObj The preferences that should be read from storage. + * @return {Promise} A promise that is resolved with an {Object} containing + * the preferences that have been read. + */ + _readFromStorage: function preferences_readFromStorage(prefObj) { + return Promise.resolve(); + }, + + /** + * Reset the preferences to their default values and update storage. + * @return {Promise} A promise that is resolved when the preference values + * have been reset. + */ + reset: function preferencesReset() { + return this.initializedPromise.then(function() { + this.prefs = Object.create(DEFAULT_PREFERENCES); + return this._writeToStorage(DEFAULT_PREFERENCES); + }.bind(this)); + }, + + /** + * Replace the current preference values with the ones from storage. + * @return {Promise} A promise that is resolved when the preference values + * have been updated. + */ + reload: function preferencesReload() { + return this.initializedPromise.then(function () { + this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) { + if (prefObj) { + this.prefs = prefObj; + } + }.bind(this)); + }.bind(this)); + }, + + /** + * Set the value of a preference. + * @param {string} name The name of the preference that should be changed. + * @param {boolean|number|string} value The new value of the preference. + * @return {Promise} A promise that is resolved when the value has been set, + * provided that the preference exists and the types match. + */ + set: function preferencesSet(name, value) { + return this.initializedPromise.then(function () { + if (DEFAULT_PREFERENCES[name] === undefined) { + throw new Error('preferencesSet: \'' + name + '\' is undefined.'); + } else if (value === undefined) { + throw new Error('preferencesSet: no value is specified.'); + } + var valueType = typeof value; + var defaultType = typeof DEFAULT_PREFERENCES[name]; + + if (valueType !== defaultType) { + if (valueType === 'number' && defaultType === 'string') { + value = value.toString(); + } else { + throw new Error('Preferences_set: \'' + value + '\' is a \"' + + valueType + '\", expected \"' + defaultType + '\".'); + } + } else { + if (valueType === 'number' && (value | 0) !== value) { + throw new Error('Preferences_set: \'' + value + + '\' must be an \"integer\".'); + } + } + this.prefs[name] = value; + return this._writeToStorage(this.prefs); + }.bind(this)); + }, + + /** + * Get the value of a preference. + * @param {string} name The name of the preference whose value is requested. + * @return {Promise} A promise that is resolved with a {boolean|number|string} + * containing the value of the preference. + */ + get: function preferencesGet(name) { + return this.initializedPromise.then(function () { + var defaultValue = DEFAULT_PREFERENCES[name]; + + if (defaultValue === undefined) { + throw new Error('preferencesGet: \'' + name + '\' is undefined.'); + } else { + var prefValue = this.prefs[name]; + + if (prefValue !== undefined) { + return prefValue; + } + } + return defaultValue; + }.bind(this)); + } +}; + + + +Preferences._writeToStorage = function (prefObj) { + return new Promise(function (resolve) { + localStorage.setItem('pdfjs.preferences', JSON.stringify(prefObj)); + resolve(); + }); +}; + +Preferences._readFromStorage = function (prefObj) { + return new Promise(function (resolve) { + var readPrefs = JSON.parse(localStorage.getItem('pdfjs.preferences')); + resolve(readPrefs); + }); +}; + + +(function mozPrintCallbackPolyfillClosure() { + if ('mozPrintCallback' in document.createElement('canvas')) { + return; + } + // Cause positive result on feature-detection: + HTMLCanvasElement.prototype.mozPrintCallback = undefined; + + var canvases; // During print task: non-live NodeList of elements + var index; // Index of element that is being processed + + var print = window.print; + window.print = function print() { + if (canvases) { + console.warn('Ignored window.print() because of a pending print job.'); + return; + } + try { + dispatchEvent('beforeprint'); + } finally { + canvases = document.querySelectorAll('canvas'); + index = -1; + next(); + } + }; + + function dispatchEvent(eventType) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent(eventType, false, false, 'custom'); + window.dispatchEvent(event); + } + + function next() { + if (!canvases) { + return; // Print task cancelled by user (state reset in abort()) + } + + renderProgress(); + if (++index < canvases.length) { + var canvas = canvases[index]; + if (typeof canvas.mozPrintCallback === 'function') { + canvas.mozPrintCallback({ + context: canvas.getContext('2d'), + abort: abort, + done: next + }); + } else { + next(); + } + } else { + renderProgress(); + print.call(window); + setTimeout(abort, 20); // Tidy-up + } + } + + function abort() { + if (canvases) { + canvases = null; + renderProgress(); + dispatchEvent('afterprint'); + } + } + + function renderProgress() { + var progressContainer = document.getElementById('mozPrintCallback-shim'); + if (canvases) { + var progress = Math.round(100 * index / canvases.length); + var progressBar = progressContainer.querySelector('progress'); + var progressPerc = progressContainer.querySelector('.relative-progress'); + progressBar.value = progress; + progressPerc.textContent = progress + '%'; + progressContainer.removeAttribute('hidden'); + progressContainer.onclick = abort; + } else { + progressContainer.setAttribute('hidden', ''); + } + } + + var hasAttachEvent = !!document.attachEvent; + + window.addEventListener('keydown', function(event) { + // Intercept Cmd/Ctrl + P in all browsers. + // Also intercept Cmd/Ctrl + Shift + P in Chrome and Opera + if (event.keyCode === 80/*P*/ && (event.ctrlKey || event.metaKey) && + !event.altKey && (!event.shiftKey || window.chrome || window.opera)) { + window.print(); + if (hasAttachEvent) { + // Only attachEvent can cancel Ctrl + P dialog in IE <=10 + // attachEvent is gone in IE11, so the dialog will re-appear in IE11. + return; + } + event.preventDefault(); + if (event.stopImmediatePropagation) { + event.stopImmediatePropagation(); + } else { + event.stopPropagation(); + } + return; + } + if (event.keyCode === 27 && canvases) { // Esc + abort(); + } + }, true); + if (hasAttachEvent) { + document.attachEvent('onkeydown', function(event) { + event = event || window.event; + if (event.keyCode === 80/*P*/ && event.ctrlKey) { + event.keyCode = 0; + return false; + } + }); + } + + if ('onbeforeprint' in window) { + // Do not propagate before/afterprint events when they are not triggered + // from within this polyfill. (FF/IE). + var stopPropagationIfNeeded = function(event) { + if (event.detail !== 'custom' && event.stopImmediatePropagation) { + event.stopImmediatePropagation(); + } + }; + window.addEventListener('beforeprint', stopPropagationIfNeeded, false); + window.addEventListener('afterprint', stopPropagationIfNeeded, false); + } +})(); + + + +var DownloadManager = (function DownloadManagerClosure() { + + function download(blobUrl, filename) { + var a = document.createElement('a'); + if (a.click) { + // Use a.click() if available. Otherwise, Chrome might show + // "Unsafe JavaScript attempt to initiate a navigation change + // for frame with URL" and not open the PDF at all. + // Supported by (not mentioned = untested): + // - Firefox 6 - 19 (4- does not support a.click, 5 ignores a.click) + // - Chrome 19 - 26 (18- does not support a.click) + // - Opera 9 - 12.15 + // - Internet Explorer 6 - 10 + // - Safari 6 (5.1- does not support a.click) + a.href = blobUrl; + a.target = '_parent'; + // Use a.download if available. This increases the likelihood that + // the file is downloaded instead of opened by another PDF plugin. + if ('download' in a) { + a.download = filename; + } + // must be in the document for IE and recent Firefox versions. + // (otherwise .click() is ignored) + (document.body || document.documentElement).appendChild(a); + a.click(); + a.parentNode.removeChild(a); + } else { + if (window.top === window && + blobUrl.split('#')[0] === window.location.href.split('#')[0]) { + // If _parent == self, then opening an identical URL with different + // location hash will only cause a navigation, not a download. + var padCharacter = blobUrl.indexOf('?') === -1 ? '?' : '&'; + blobUrl = blobUrl.replace(/#|$/, padCharacter + '$&'); + } + window.open(blobUrl, '_parent'); + } + } + + function DownloadManager() {} + + DownloadManager.prototype = { + downloadUrl: function DownloadManager_downloadUrl(url, filename) { + if (!PDFJS.isValidUrl(url, true)) { + return; // restricted/invalid URL + } + + download(url + '#pdfjs.action=download', filename); + }, + + downloadData: function DownloadManager_downloadData(data, filename, + contentType) { + if (navigator.msSaveBlob) { // IE10 and above + return navigator.msSaveBlob(new Blob([data], { type: contentType }), + filename); + } + + var blobUrl = PDFJS.createObjectURL(data, contentType); + download(blobUrl, filename); + }, + + download: function DownloadManager_download(blob, url, filename) { + if (!URL) { + // URL.createObjectURL is not supported + this.downloadUrl(url, filename); + return; + } + + if (navigator.msSaveBlob) { + // IE10 / IE11 + if (!navigator.msSaveBlob(blob, filename)) { + this.downloadUrl(url, filename); + } + return; + } + + var blobUrl = URL.createObjectURL(blob); + download(blobUrl, filename); + } + }; + + return DownloadManager; +})(); + + + + + +/** + * View History - This is a utility for saving various view parameters for + * recently opened files. + * + * The way that the view parameters are stored depends on how PDF.js is built, + * for 'node make ' the following cases exist: + * - FIREFOX or MOZCENTRAL - uses sessionStorage. + * - B2G - uses asyncStorage. + * - GENERIC or CHROME - uses localStorage, if it is available. + */ +var ViewHistory = (function ViewHistoryClosure() { + function ViewHistory(fingerprint) { + this.fingerprint = fingerprint; + this.isInitializedPromiseResolved = false; + this.initializedPromise = + this._readFromStorage().then(function (databaseStr) { + this.isInitializedPromiseResolved = true; + + var database = JSON.parse(databaseStr || '{}'); + if (!('files' in database)) { + database.files = []; + } + if (database.files.length >= VIEW_HISTORY_MEMORY) { + database.files.shift(); + } + var index; + for (var i = 0, length = database.files.length; i < length; i++) { + var branch = database.files[i]; + if (branch.fingerprint === this.fingerprint) { + index = i; + break; + } + } + if (typeof index !== 'number') { + index = database.files.push({fingerprint: this.fingerprint}) - 1; + } + this.file = database.files[index]; + this.database = database; + }.bind(this)); + } + + ViewHistory.prototype = { + _writeToStorage: function ViewHistory_writeToStorage() { + return new Promise(function (resolve) { + var databaseStr = JSON.stringify(this.database); + + + + localStorage.setItem('database', databaseStr); + resolve(); + }.bind(this)); + }, + + _readFromStorage: function ViewHistory_readFromStorage() { + return new Promise(function (resolve) { + + + resolve(localStorage.getItem('database')); + }); + }, + + set: function ViewHistory_set(name, val) { + if (!this.isInitializedPromiseResolved) { + return; + } + this.file[name] = val; + return this._writeToStorage(); + }, + + setMultiple: function ViewHistory_setMultiple(properties) { + if (!this.isInitializedPromiseResolved) { + return; + } + for (var name in properties) { + this.file[name] = properties[name]; + } + return this._writeToStorage(); + }, + + get: function ViewHistory_get(name, defaultValue) { + if (!this.isInitializedPromiseResolved) { + return defaultValue; + } + return this.file[name] || defaultValue; + } + }; + + return ViewHistory; +})(); + + +/** + * Creates a "search bar" given a set of DOM elements that act as controls + * for searching or for setting search preferences in the UI. This object + * also sets up the appropriate events for the controls. Actual searching + * is done by PDFFindController. + */ +var PDFFindBar = (function PDFFindBarClosure() { + function PDFFindBar(options) { + this.opened = false; + this.bar = options.bar || null; + this.toggleButton = options.toggleButton || null; + this.findField = options.findField || null; + this.highlightAll = options.highlightAllCheckbox || null; + this.caseSensitive = options.caseSensitiveCheckbox || null; + this.findMsg = options.findMsg || null; + this.findStatusIcon = options.findStatusIcon || null; + this.findPreviousButton = options.findPreviousButton || null; + this.findNextButton = options.findNextButton || null; + this.findController = options.findController || null; + + if (this.findController === null) { + throw new Error('PDFFindBar cannot be used without a ' + + 'PDFFindController instance.'); + } + + // Add event listeners to the DOM elements. + var self = this; + this.toggleButton.addEventListener('click', function() { + self.toggle(); + }); + + this.findField.addEventListener('input', function() { + self.dispatchEvent(''); + }); + + this.bar.addEventListener('keydown', function(evt) { + switch (evt.keyCode) { + case 13: // Enter + if (evt.target === self.findField) { + self.dispatchEvent('again', evt.shiftKey); + } + break; + case 27: // Escape + self.close(); + break; + } + }); + + this.findPreviousButton.addEventListener('click', function() { + self.dispatchEvent('again', true); + }); + + this.findNextButton.addEventListener('click', function() { + self.dispatchEvent('again', false); + }); + + this.highlightAll.addEventListener('click', function() { + self.dispatchEvent('highlightallchange'); + }); + + this.caseSensitive.addEventListener('click', function() { + self.dispatchEvent('casesensitivitychange'); + }); + } + + PDFFindBar.prototype = { + dispatchEvent: function PDFFindBar_dispatchEvent(type, findPrev) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('find' + type, true, true, { + query: this.findField.value, + caseSensitive: this.caseSensitive.checked, + highlightAll: this.highlightAll.checked, + findPrevious: findPrev + }); + return window.dispatchEvent(event); + }, + + updateUIState: function PDFFindBar_updateUIState(state, previous) { + var notFound = false; + var findMsg = ''; + var status = ''; + + switch (state) { + case FindStates.FIND_FOUND: + break; + + case FindStates.FIND_PENDING: + status = 'pending'; + break; + + case FindStates.FIND_NOTFOUND: + findMsg = mozL10n.get('find_not_found', null, 'Phrase not found'); + notFound = true; + break; + + case FindStates.FIND_WRAPPED: + if (previous) { + findMsg = mozL10n.get('find_reached_top', null, + 'Reached top of document, continued from bottom'); + } else { + findMsg = mozL10n.get('find_reached_bottom', null, + 'Reached end of document, continued from top'); + } + break; + } + + if (notFound) { + this.findField.classList.add('notFound'); + } else { + this.findField.classList.remove('notFound'); + } + + this.findField.setAttribute('data-status', status); + this.findMsg.textContent = findMsg; + }, + + open: function PDFFindBar_open() { + if (!this.opened) { + this.opened = true; + this.toggleButton.classList.add('toggled'); + this.bar.classList.remove('hidden'); + } + this.findField.select(); + this.findField.focus(); + }, + + close: function PDFFindBar_close() { + if (!this.opened) { + return; + } + this.opened = false; + this.toggleButton.classList.remove('toggled'); + this.bar.classList.add('hidden'); + this.findController.active = false; + }, + + toggle: function PDFFindBar_toggle() { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } + }; + return PDFFindBar; +})(); + + +var FindStates = { + FIND_FOUND: 0, + FIND_NOTFOUND: 1, + FIND_WRAPPED: 2, + FIND_PENDING: 3 +}; + +var FIND_SCROLL_OFFSET_TOP = -50; +var FIND_SCROLL_OFFSET_LEFT = -400; + +/** + * Provides "search" or "find" functionality for the PDF. + * This object actually performs the search for a given string. + */ +var PDFFindController = (function PDFFindControllerClosure() { + function PDFFindController(options) { + this.startedTextExtraction = false; + this.extractTextPromises = []; + this.pendingFindMatches = {}; + this.active = false; // If active, find results will be highlighted. + this.pageContents = []; // Stores the text for each page. + this.pageMatches = []; + this.selected = { // Currently selected match. + pageIdx: -1, + matchIdx: -1 + }; + this.offset = { // Where the find algorithm currently is in the document. + pageIdx: null, + matchIdx: null + }; + this.pagesToSearch = null; + this.resumePageIdx = null; + this.state = null; + this.dirtyMatch = false; + this.findTimeout = null; + this.pdfViewer = options.pdfViewer || null; + this.integratedFind = options.integratedFind || false; + this.charactersToNormalize = { + '\u2018': '\'', // Left single quotation mark + '\u2019': '\'', // Right single quotation mark + '\u201A': '\'', // Single low-9 quotation mark + '\u201B': '\'', // Single high-reversed-9 quotation mark + '\u201C': '"', // Left double quotation mark + '\u201D': '"', // Right double quotation mark + '\u201E': '"', // Double low-9 quotation mark + '\u201F': '"', // Double high-reversed-9 quotation mark + '\u00BC': '1/4', // Vulgar fraction one quarter + '\u00BD': '1/2', // Vulgar fraction one half + '\u00BE': '3/4', // Vulgar fraction three quarters + '\u00A0': ' ' // No-break space + }; + this.findBar = options.findBar || null; + + // Compile the regular expression for text normalization once + var replace = Object.keys(this.charactersToNormalize).join(''); + this.normalizationRegex = new RegExp('[' + replace + ']', 'g'); + + var events = [ + 'find', + 'findagain', + 'findhighlightallchange', + 'findcasesensitivitychange' + ]; + + this.firstPagePromise = new Promise(function (resolve) { + this.resolveFirstPage = resolve; + }.bind(this)); + this.handleEvent = this.handleEvent.bind(this); + + for (var i = 0, len = events.length; i < len; i++) { + window.addEventListener(events[i], this.handleEvent); + } + } + + PDFFindController.prototype = { + setFindBar: function PDFFindController_setFindBar(findBar) { + this.findBar = findBar; + }, + + reset: function PDFFindController_reset() { + this.startedTextExtraction = false; + this.extractTextPromises = []; + this.active = false; + }, + + normalize: function PDFFindController_normalize(text) { + var self = this; + return text.replace(this.normalizationRegex, function (ch) { + return self.charactersToNormalize[ch]; + }); + }, + + calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) { + var pageContent = this.normalize(this.pageContents[pageIndex]); + var query = this.normalize(this.state.query); + var caseSensitive = this.state.caseSensitive; + var queryLen = query.length; + + if (queryLen === 0) { + return; // Do nothing: the matches should be wiped out already. + } + + if (!caseSensitive) { + pageContent = pageContent.toLowerCase(); + query = query.toLowerCase(); + } + + var matches = []; + var matchIdx = -queryLen; + while (true) { + matchIdx = pageContent.indexOf(query, matchIdx + queryLen); + if (matchIdx === -1) { + break; + } + matches.push(matchIdx); + } + this.pageMatches[pageIndex] = matches; + this.updatePage(pageIndex); + if (this.resumePageIdx === pageIndex) { + this.resumePageIdx = null; + this.nextPageMatch(); + } + }, + + extractText: function PDFFindController_extractText() { + if (this.startedTextExtraction) { + return; + } + this.startedTextExtraction = true; + + this.pageContents = []; + var extractTextPromisesResolves = []; + var numPages = this.pdfViewer.pagesCount; + for (var i = 0; i < numPages; i++) { + this.extractTextPromises.push(new Promise(function (resolve) { + extractTextPromisesResolves.push(resolve); + })); + } + + var self = this; + function extractPageText(pageIndex) { + self.pdfViewer.getPageTextContent(pageIndex).then( + function textContentResolved(textContent) { + var textItems = textContent.items; + var str = []; + + for (var i = 0, len = textItems.length; i < len; i++) { + str.push(textItems[i].str); + } + + // Store the pageContent as a string. + self.pageContents.push(str.join('')); + + extractTextPromisesResolves[pageIndex](pageIndex); + if ((pageIndex + 1) < self.pdfViewer.pagesCount) { + extractPageText(pageIndex + 1); + } + } + ); + } + extractPageText(0); + }, + + handleEvent: function PDFFindController_handleEvent(e) { + if (this.state === null || e.type !== 'findagain') { + this.dirtyMatch = true; + } + this.state = e.detail; + this.updateUIState(FindStates.FIND_PENDING); + + this.firstPagePromise.then(function() { + this.extractText(); + + clearTimeout(this.findTimeout); + if (e.type === 'find') { + // Only trigger the find action after 250ms of silence. + this.findTimeout = setTimeout(this.nextMatch.bind(this), 250); + } else { + this.nextMatch(); + } + }.bind(this)); + }, + + updatePage: function PDFFindController_updatePage(index) { + if (this.selected.pageIdx === index) { + // If the page is selected, scroll the page into view, which triggers + // rendering the page, which adds the textLayer. Once the textLayer is + // build, it will scroll onto the selected match. + this.pdfViewer.scrollPageIntoView(index + 1); + } + + var page = this.pdfViewer.getPageView(index); + if (page.textLayer) { + page.textLayer.updateMatches(); + } + }, + + nextMatch: function PDFFindController_nextMatch() { + var previous = this.state.findPrevious; + var currentPageIndex = this.pdfViewer.currentPageNumber - 1; + var numPages = this.pdfViewer.pagesCount; + + this.active = true; + + if (this.dirtyMatch) { + // Need to recalculate the matches, reset everything. + this.dirtyMatch = false; + this.selected.pageIdx = this.selected.matchIdx = -1; + this.offset.pageIdx = currentPageIndex; + this.offset.matchIdx = null; + this.hadMatch = false; + this.resumePageIdx = null; + this.pageMatches = []; + var self = this; + + for (var i = 0; i < numPages; i++) { + // Wipe out any previous highlighted matches. + this.updatePage(i); + + // As soon as the text is extracted start finding the matches. + if (!(i in this.pendingFindMatches)) { + this.pendingFindMatches[i] = true; + this.extractTextPromises[i].then(function(pageIdx) { + delete self.pendingFindMatches[pageIdx]; + self.calcFindMatch(pageIdx); + }); + } + } + } + + // If there's no query there's no point in searching. + if (this.state.query === '') { + this.updateUIState(FindStates.FIND_FOUND); + return; + } + + // If we're waiting on a page, we return since we can't do anything else. + if (this.resumePageIdx) { + return; + } + + var offset = this.offset; + // Keep track of how many pages we should maximally iterate through. + this.pagesToSearch = numPages; + // If there's already a matchIdx that means we are iterating through a + // page's matches. + if (offset.matchIdx !== null) { + var numPageMatches = this.pageMatches[offset.pageIdx].length; + if ((!previous && offset.matchIdx + 1 < numPageMatches) || + (previous && offset.matchIdx > 0)) { + // The simple case; we just have advance the matchIdx to select + // the next match on the page. + this.hadMatch = true; + offset.matchIdx = (previous ? offset.matchIdx - 1 : + offset.matchIdx + 1); + this.updateMatch(true); + return; + } + // We went beyond the current page's matches, so we advance to + // the next page. + this.advanceOffsetPage(previous); + } + // Start searching through the page. + this.nextPageMatch(); + }, + + matchesReady: function PDFFindController_matchesReady(matches) { + var offset = this.offset; + var numMatches = matches.length; + var previous = this.state.findPrevious; + + if (numMatches) { + // There were matches for the page, so initialize the matchIdx. + this.hadMatch = true; + offset.matchIdx = (previous ? numMatches - 1 : 0); + this.updateMatch(true); + return true; + } else { + // No matches, so attempt to search the next page. + this.advanceOffsetPage(previous); + if (offset.wrapped) { + offset.matchIdx = null; + if (this.pagesToSearch < 0) { + // No point in wrapping again, there were no matches. + this.updateMatch(false); + // while matches were not found, searching for a page + // with matches should nevertheless halt. + return true; + } + } + // Matches were not found (and searching is not done). + return false; + } + }, + + /** + * The method is called back from the text layer when match presentation + * is updated. + * @param {number} pageIndex - page index. + * @param {number} index - match index. + * @param {Array} elements - text layer div elements array. + * @param {number} beginIdx - start index of the div array for the match. + * @param {number} endIdx - end index of the div array for the match. + */ + updateMatchPosition: function PDFFindController_updateMatchPosition( + pageIndex, index, elements, beginIdx, endIdx) { + if (this.selected.matchIdx === index && + this.selected.pageIdx === pageIndex) { + scrollIntoView(elements[beginIdx], { + top: FIND_SCROLL_OFFSET_TOP, + left: FIND_SCROLL_OFFSET_LEFT + }); + } + }, + + nextPageMatch: function PDFFindController_nextPageMatch() { + if (this.resumePageIdx !== null) { + console.error('There can only be one pending page.'); + } + do { + var pageIdx = this.offset.pageIdx; + var matches = this.pageMatches[pageIdx]; + if (!matches) { + // The matches don't exist yet for processing by "matchesReady", + // so set a resume point for when they do exist. + this.resumePageIdx = pageIdx; + break; + } + } while (!this.matchesReady(matches)); + }, + + advanceOffsetPage: function PDFFindController_advanceOffsetPage(previous) { + var offset = this.offset; + var numPages = this.extractTextPromises.length; + offset.pageIdx = (previous ? offset.pageIdx - 1 : offset.pageIdx + 1); + offset.matchIdx = null; + + this.pagesToSearch--; + + if (offset.pageIdx >= numPages || offset.pageIdx < 0) { + offset.pageIdx = (previous ? numPages - 1 : 0); + offset.wrapped = true; + } + }, + + updateMatch: function PDFFindController_updateMatch(found) { + var state = FindStates.FIND_NOTFOUND; + var wrapped = this.offset.wrapped; + this.offset.wrapped = false; + + if (found) { + var previousPage = this.selected.pageIdx; + this.selected.pageIdx = this.offset.pageIdx; + this.selected.matchIdx = this.offset.matchIdx; + state = (wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND); + // Update the currently selected page to wipe out any selected matches. + if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { + this.updatePage(previousPage); + } + } + + this.updateUIState(state, this.state.findPrevious); + if (this.selected.pageIdx !== -1) { + this.updatePage(this.selected.pageIdx); + } + }, + + updateUIState: function PDFFindController_updateUIState(state, previous) { + if (this.integratedFind) { + FirefoxCom.request('updateFindControlState', + { result: state, findPrevious: previous }); + return; + } + if (this.findBar === null) { + throw new Error('PDFFindController is not initialized with a ' + + 'PDFFindBar instance.'); + } + this.findBar.updateUIState(state, previous); + } + }; + return PDFFindController; +})(); + + +var PDFHistory = { + initialized: false, + initialDestination: null, + + /** + * @param {string} fingerprint + * @param {IPDFLinkService} linkService + */ + initialize: function pdfHistoryInitialize(fingerprint, linkService) { + this.initialized = true; + this.reInitialized = false; + this.allowHashChange = true; + this.historyUnlocked = true; + this.isViewerInPresentationMode = false; + + this.previousHash = window.location.hash.substring(1); + this.currentBookmark = ''; + this.currentPage = 0; + this.updatePreviousBookmark = false; + this.previousBookmark = ''; + this.previousPage = 0; + this.nextHashParam = ''; + + this.fingerprint = fingerprint; + this.linkService = linkService; + this.currentUid = this.uid = 0; + this.current = {}; + + var state = window.history.state; + if (this._isStateObjectDefined(state)) { + // This corresponds to navigating back to the document + // from another page in the browser history. + if (state.target.dest) { + this.initialDestination = state.target.dest; + } else { + linkService.setHash(state.target.hash); + } + this.currentUid = state.uid; + this.uid = state.uid + 1; + this.current = state.target; + } else { + // This corresponds to the loading of a new document. + if (state && state.fingerprint && + this.fingerprint !== state.fingerprint) { + // Reinitialize the browsing history when a new document + // is opened in the web viewer. + this.reInitialized = true; + } + this._pushOrReplaceState({ fingerprint: this.fingerprint }, true); + } + + var self = this; + window.addEventListener('popstate', function pdfHistoryPopstate(evt) { + evt.preventDefault(); + evt.stopPropagation(); + + if (!self.historyUnlocked) { + return; + } + if (evt.state) { + // Move back/forward in the history. + self._goTo(evt.state); + } else { + // Handle the user modifying the hash of a loaded document. + self.previousHash = window.location.hash.substring(1); + + // If the history is empty when the hash changes, + // update the previous entry in the browser history. + if (self.uid === 0) { + var previousParams = (self.previousHash && self.currentBookmark && + self.previousHash !== self.currentBookmark) ? + { hash: self.currentBookmark, page: self.currentPage } : + { page: 1 }; + self.historyUnlocked = false; + self.allowHashChange = false; + window.history.back(); + self._pushToHistory(previousParams, false, true); + window.history.forward(); + self.historyUnlocked = true; + } + self._pushToHistory({ hash: self.previousHash }, false, true); + self._updatePreviousBookmark(); + } + }, false); + + function pdfHistoryBeforeUnload() { + var previousParams = self._getPreviousParams(null, true); + if (previousParams) { + var replacePrevious = (!self.current.dest && + self.current.hash !== self.previousHash); + self._pushToHistory(previousParams, false, replacePrevious); + self._updatePreviousBookmark(); + } + // Remove the event listener when navigating away from the document, + // since 'beforeunload' prevents Firefox from caching the document. + window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, false); + } + window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); + + window.addEventListener('pageshow', function pdfHistoryPageShow(evt) { + // If the entire viewer (including the PDF file) is cached in the browser, + // we need to reattach the 'beforeunload' event listener since + // the 'DOMContentLoaded' event is not fired on 'pageshow'. + window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); + }, false); + + window.addEventListener('presentationmodechanged', function(e) { + self.isViewerInPresentationMode = !!e.detail.active; + }); + }, + + _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { + return (state && state.uid >= 0 && + state.fingerprint && this.fingerprint === state.fingerprint && + state.target && state.target.hash) ? true : false; + }, + + _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, + replace) { + if (replace) { + window.history.replaceState(stateObj, '', document.URL); + } else { + window.history.pushState(stateObj, '', document.URL); + } + }, + + get isHashChangeUnlocked() { + if (!this.initialized) { + return true; + } + // If the current hash changes when moving back/forward in the history, + // this will trigger a 'popstate' event *as well* as a 'hashchange' event. + // Since the hash generally won't correspond to the exact the position + // stored in the history's state object, triggering the 'hashchange' event + // can thus corrupt the browser history. + // + // When the hash changes during a 'popstate' event, we *only* prevent the + // first 'hashchange' event and immediately reset allowHashChange. + // If it is not reset, the user would not be able to change the hash. + + var temp = this.allowHashChange; + this.allowHashChange = true; + return temp; + }, + + _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() { + if (this.updatePreviousBookmark && + this.currentBookmark && this.currentPage) { + this.previousBookmark = this.currentBookmark; + this.previousPage = this.currentPage; + this.updatePreviousBookmark = false; + } + }, + + updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, + pageNum) { + if (this.initialized) { + this.currentBookmark = bookmark.substring(1); + this.currentPage = pageNum | 0; + this._updatePreviousBookmark(); + } + }, + + updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { + if (this.initialized) { + this.nextHashParam = param; + } + }, + + push: function pdfHistoryPush(params, isInitialBookmark) { + if (!(this.initialized && this.historyUnlocked)) { + return; + } + if (params.dest && !params.hash) { + params.hash = (this.current.hash && this.current.dest && + this.current.dest === params.dest) ? + this.current.hash : + this.linkService.getDestinationHash(params.dest).split('#')[1]; + } + if (params.page) { + params.page |= 0; + } + if (isInitialBookmark) { + var target = window.history.state.target; + if (!target) { + // Invoked when the user specifies an initial bookmark, + // thus setting initialBookmark, when the document is loaded. + this._pushToHistory(params, false); + this.previousHash = window.location.hash.substring(1); + } + this.updatePreviousBookmark = this.nextHashParam ? false : true; + if (target) { + // If the current document is reloaded, + // avoid creating duplicate entries in the history. + this._updatePreviousBookmark(); + } + return; + } + if (this.nextHashParam) { + if (this.nextHashParam === params.hash) { + this.nextHashParam = null; + this.updatePreviousBookmark = true; + return; + } else { + this.nextHashParam = null; + } + } + + if (params.hash) { + if (this.current.hash) { + if (this.current.hash !== params.hash) { + this._pushToHistory(params, true); + } else { + if (!this.current.page && params.page) { + this._pushToHistory(params, false, true); + } + this.updatePreviousBookmark = true; + } + } else { + this._pushToHistory(params, true); + } + } else if (this.current.page && params.page && + this.current.page !== params.page) { + this._pushToHistory(params, true); + } + }, + + _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, + beforeUnload) { + if (!(this.currentBookmark && this.currentPage)) { + return null; + } else if (this.updatePreviousBookmark) { + this.updatePreviousBookmark = false; + } + if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) { + // Prevent the history from getting stuck in the current state, + // effectively preventing the user from going back/forward in the history. + // + // This happens if the current position in the document didn't change when + // the history was previously updated. The reasons for this are either: + // 1. The current zoom value is such that the document does not need to, + // or cannot, be scrolled to display the destination. + // 2. The previous destination is broken, and doesn't actally point to a + // position within the document. + // (This is either due to a bad PDF generator, or the user making a + // mistake when entering a destination in the hash parameters.) + return null; + } + if ((!this.current.dest && !onlyCheckPage) || beforeUnload) { + if (this.previousBookmark === this.currentBookmark) { + return null; + } + } else if (this.current.page || onlyCheckPage) { + if (this.previousPage === this.currentPage) { + return null; + } + } else { + return null; + } + var params = { hash: this.currentBookmark, page: this.currentPage }; + if (this.isViewerInPresentationMode) { + params.hash = null; + } + return params; + }, + + _stateObj: function pdfHistory_stateObj(params) { + return { fingerprint: this.fingerprint, uid: this.uid, target: params }; + }, + + _pushToHistory: function pdfHistory_pushToHistory(params, + addPrevious, overwrite) { + if (!this.initialized) { + return; + } + if (!params.hash && params.page) { + params.hash = ('page=' + params.page); + } + if (addPrevious && !overwrite) { + var previousParams = this._getPreviousParams(); + if (previousParams) { + var replacePrevious = (!this.current.dest && + this.current.hash !== this.previousHash); + this._pushToHistory(previousParams, false, replacePrevious); + } + } + this._pushOrReplaceState(this._stateObj(params), + (overwrite || this.uid === 0)); + this.currentUid = this.uid++; + this.current = params; + this.updatePreviousBookmark = true; + }, + + _goTo: function pdfHistory_goTo(state) { + if (!(this.initialized && this.historyUnlocked && + this._isStateObjectDefined(state))) { + return; + } + if (!this.reInitialized && state.uid < this.currentUid) { + var previousParams = this._getPreviousParams(true); + if (previousParams) { + this._pushToHistory(this.current, false); + this._pushToHistory(previousParams, false); + this.currentUid = state.uid; + window.history.back(); + return; + } + } + this.historyUnlocked = false; + + if (state.target.dest) { + this.linkService.navigateTo(state.target.dest); + } else { + this.linkService.setHash(state.target.hash); + } + this.currentUid = state.uid; + if (state.uid > this.uid) { + this.uid = state.uid; + } + this.current = state.target; + this.updatePreviousBookmark = true; + + var currentHash = window.location.hash.substring(1); + if (this.previousHash !== currentHash) { + this.allowHashChange = false; + } + this.previousHash = currentHash; + + this.historyUnlocked = true; + }, + + back: function pdfHistoryBack() { + this.go(-1); + }, + + forward: function pdfHistoryForward() { + this.go(1); + }, + + go: function pdfHistoryGo(direction) { + if (this.initialized && this.historyUnlocked) { + var state = window.history.state; + if (direction === -1 && state && state.uid > 0) { + window.history.back(); + } else if (direction === 1 && state && state.uid < (this.uid - 1)) { + window.history.forward(); + } + } + } +}; + + +var SecondaryToolbar = { + opened: false, + previousContainerHeight: null, + newContainerHeight: null, + + initialize: function secondaryToolbarInitialize(options) { + this.toolbar = options.toolbar; + this.buttonContainer = this.toolbar.firstElementChild; + + // Define the toolbar buttons. + this.toggleButton = options.toggleButton; + this.presentationModeButton = options.presentationModeButton; + this.openFile = options.openFile; + this.print = options.print; + this.download = options.download; + this.viewBookmark = options.viewBookmark; + this.firstPage = options.firstPage; + this.lastPage = options.lastPage; + this.pageRotateCw = options.pageRotateCw; + this.pageRotateCcw = options.pageRotateCcw; + this.documentPropertiesButton = options.documentPropertiesButton; + + // Attach the event listeners. + var elements = [ + // Button to toggle the visibility of the secondary toolbar: + { element: this.toggleButton, handler: this.toggle }, + // All items within the secondary toolbar + // (except for toggleHandTool, hand_tool.js is responsible for it): + { element: this.presentationModeButton, + handler: this.presentationModeClick }, + { element: this.openFile, handler: this.openFileClick }, + { element: this.print, handler: this.printClick }, + { element: this.download, handler: this.downloadClick }, + { element: this.viewBookmark, handler: this.viewBookmarkClick }, + { element: this.firstPage, handler: this.firstPageClick }, + { element: this.lastPage, handler: this.lastPageClick }, + { element: this.pageRotateCw, handler: this.pageRotateCwClick }, + { element: this.pageRotateCcw, handler: this.pageRotateCcwClick }, + { element: this.documentPropertiesButton, + handler: this.documentPropertiesClick } + ]; + + for (var item in elements) { + var element = elements[item].element; + if (element) { + element.addEventListener('click', elements[item].handler.bind(this)); + } + } + }, + + // Event handling functions. + presentationModeClick: function secondaryToolbarPresentationModeClick(evt) { + PDFViewerApplication.requestPresentationMode(); + this.close(); + }, + + openFileClick: function secondaryToolbarOpenFileClick(evt) { + document.getElementById('fileInput').click(); + this.close(); + }, + + printClick: function secondaryToolbarPrintClick(evt) { + window.print(); + this.close(); + }, + + downloadClick: function secondaryToolbarDownloadClick(evt) { + PDFViewerApplication.download(); + this.close(); + }, + + viewBookmarkClick: function secondaryToolbarViewBookmarkClick(evt) { + this.close(); + }, + + firstPageClick: function secondaryToolbarFirstPageClick(evt) { + PDFViewerApplication.page = 1; + this.close(); + }, + + lastPageClick: function secondaryToolbarLastPageClick(evt) { + if (PDFViewerApplication.pdfDocument) { + PDFViewerApplication.page = PDFViewerApplication.pagesCount; + } + this.close(); + }, + + pageRotateCwClick: function secondaryToolbarPageRotateCwClick(evt) { + PDFViewerApplication.rotatePages(90); + }, + + pageRotateCcwClick: function secondaryToolbarPageRotateCcwClick(evt) { + PDFViewerApplication.rotatePages(-90); + }, + + documentPropertiesClick: function secondaryToolbarDocumentPropsClick(evt) { + PDFViewerApplication.pdfDocumentProperties.open(); + this.close(); + }, + + // Misc. functions for interacting with the toolbar. + setMaxHeight: function secondaryToolbarSetMaxHeight(container) { + if (!container || !this.buttonContainer) { + return; + } + this.newContainerHeight = container.clientHeight; + if (this.previousContainerHeight === this.newContainerHeight) { + return; + } + this.buttonContainer.setAttribute('style', + 'max-height: ' + (this.newContainerHeight - SCROLLBAR_PADDING) + 'px;'); + this.previousContainerHeight = this.newContainerHeight; + }, + + open: function secondaryToolbarOpen() { + if (this.opened) { + return; + } + this.opened = true; + this.toggleButton.classList.add('toggled'); + this.toolbar.classList.remove('hidden'); + }, + + close: function secondaryToolbarClose(target) { + if (!this.opened) { + return; + } else if (target && !this.toolbar.contains(target)) { + return; + } + this.opened = false; + this.toolbar.classList.add('hidden'); + this.toggleButton.classList.remove('toggled'); + }, + + toggle: function secondaryToolbarToggle() { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } +}; + + +var DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1500; // in ms +var DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms +var ACTIVE_SELECTOR = 'pdfPresentationMode'; +var CONTROLS_SELECTOR = 'pdfPresentationModeControls'; + +/** + * @typedef {Object} PDFPresentationModeOptions + * @property {HTMLDivElement} container - The container for the viewer element. + * @property {HTMLDivElement} viewer - (optional) The viewer element. + * @property {PDFThumbnailViewer} pdfThumbnailViewer - (optional) The thumbnail + * viewer. + * @property {Array} contextMenuItems - (optional) The menuitems that are added + * to the context menu in Presentation Mode. + */ + +/** + * @class + */ +var PDFPresentationMode = (function PDFPresentationModeClosure() { + /** + * @constructs PDFPresentationMode + * @param {PDFPresentationModeOptions} options + */ + function PDFPresentationMode(options) { + this.container = options.container; + this.viewer = options.viewer || options.container.firstElementChild; + this.pdfThumbnailViewer = options.pdfThumbnailViewer || null; + var contextMenuItems = options.contextMenuItems || null; + + this.active = false; + this.args = null; + this.contextMenuOpen = false; + this.mouseScrollTimeStamp = 0; + this.mouseScrollDelta = 0; + + if (contextMenuItems) { + for (var i = 0, ii = contextMenuItems.length; i < ii; i++) { + var item = contextMenuItems[i]; + item.element.addEventListener('click', function (handler) { + this.contextMenuOpen = false; + handler(); + }.bind(this, item.handler)); + } + } + } + + PDFPresentationMode.prototype = { + /** + * Request the browser to enter fullscreen mode. + * @returns {boolean} Indicating if the request was successful. + */ + request: function PDFPresentationMode_request() { + if (this.switchInProgress || this.active || + !this.viewer.hasChildNodes()) { + return false; + } + this._addFullscreenChangeListeners(); + this._setSwitchInProgress(); + this._notifyStateChange(); + + if (this.container.requestFullscreen) { + this.container.requestFullscreen(); + } else if (this.container.mozRequestFullScreen) { + this.container.mozRequestFullScreen(); + } else if (this.container.webkitRequestFullscreen) { + this.container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (this.container.msRequestFullscreen) { + this.container.msRequestFullscreen(); + } else { + return false; + } + + this.args = { + page: PDFViewerApplication.page, + previousScale: PDFViewerApplication.currentScaleValue + }; + + return true; + }, + + /** + * Switches page when the user scrolls (using a scroll wheel or a touchpad) + * with large enough motion, to prevent accidental page switches. + * @param {number} delta - The delta value from the mouse event. + */ + mouseScroll: function PDFPresentationMode_mouseScroll(delta) { + if (!this.active) { + return; + } + var MOUSE_SCROLL_COOLDOWN_TIME = 50; + var PAGE_SWITCH_THRESHOLD = 120; + var PageSwitchDirection = { + UP: -1, + DOWN: 1 + }; + + var currentTime = (new Date()).getTime(); + var storedTime = this.mouseScrollTimeStamp; + + // If we've already switched page, avoid accidentally switching again. + if (currentTime > storedTime && + currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) { + return; + } + // If the scroll direction changed, reset the accumulated scroll delta. + if ((this.mouseScrollDelta > 0 && delta < 0) || + (this.mouseScrollDelta < 0 && delta > 0)) { + this._resetMouseScrollState(); + } + this.mouseScrollDelta += delta; + + if (Math.abs(this.mouseScrollDelta) >= PAGE_SWITCH_THRESHOLD) { + var pageSwitchDirection = (this.mouseScrollDelta > 0) ? + PageSwitchDirection.UP : PageSwitchDirection.DOWN; + var page = PDFViewerApplication.page; + this._resetMouseScrollState(); + + // If we're at the first/last page, we don't need to do anything. + if ((page === 1 && pageSwitchDirection === PageSwitchDirection.UP) || + (page === PDFViewerApplication.pagesCount && + pageSwitchDirection === PageSwitchDirection.DOWN)) { + return; + } + PDFViewerApplication.page = (page + pageSwitchDirection); + this.mouseScrollTimeStamp = currentTime; + } + }, + + get isFullscreen() { + return !!(document.fullscreenElement || + document.mozFullScreen || + document.webkitIsFullScreen || + document.msFullscreenElement); + }, + + /** + * @private + */ + _notifyStateChange: function PDFPresentationMode_notifyStateChange() { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('presentationmodechanged', true, true, { + active: this.active, + switchInProgress: !!this.switchInProgress + }); + window.dispatchEvent(event); + }, + + /** + * Used to initialize a timeout when requesting Presentation Mode, + * i.e. when the browser is requested to enter fullscreen mode. + * This timeout is used to prevent the current page from being scrolled + * partially, or completely, out of view when entering Presentation Mode. + * NOTE: This issue seems limited to certain zoom levels (e.g. page-width). + * @private + */ + _setSwitchInProgress: function PDFPresentationMode_setSwitchInProgress() { + if (this.switchInProgress) { + clearTimeout(this.switchInProgress); + } + this.switchInProgress = setTimeout(function switchInProgressTimeout() { + this._removeFullscreenChangeListeners(); + delete this.switchInProgress; + this._notifyStateChange(); + }.bind(this), DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS); + }, + + /** + * @private + */ + _resetSwitchInProgress: + function PDFPresentationMode_resetSwitchInProgress() { + if (this.switchInProgress) { + clearTimeout(this.switchInProgress); + delete this.switchInProgress; + } + }, + + /** + * @private + */ + _enter: function PDFPresentationMode_enter() { + this.active = true; + this._resetSwitchInProgress(); + this._notifyStateChange(); + this.container.classList.add(ACTIVE_SELECTOR); + + // Ensure that the correct page is scrolled into view when entering + // Presentation Mode, by waiting until fullscreen mode in enabled. + setTimeout(function enterPresentationModeTimeout() { + PDFViewerApplication.page = this.args.page; + PDFViewerApplication.setScale('page-fit', true); + }.bind(this), 0); + + this._addWindowListeners(); + this._showControls(); + this.contextMenuOpen = false; + this.container.setAttribute('contextmenu', 'viewerContextMenu'); + + // Text selection is disabled in Presentation Mode, thus it's not possible + // for the user to deselect text that is selected (e.g. with "Select all") + // when entering Presentation Mode, hence we remove any active selection. + window.getSelection().removeAllRanges(); + }, + + /** + * @private + */ + _exit: function PDFPresentationMode_exit() { + var page = PDFViewerApplication.page; + this.container.classList.remove(ACTIVE_SELECTOR); + + // Ensure that the correct page is scrolled into view when exiting + // Presentation Mode, by waiting until fullscreen mode is disabled. + setTimeout(function exitPresentationModeTimeout() { + this.active = false; + this._removeFullscreenChangeListeners(); + this._notifyStateChange(); + + PDFViewerApplication.setScale(this.args.previousScale, true); + PDFViewerApplication.page = page; + this.args = null; + }.bind(this), 0); + + this._removeWindowListeners(); + this._hideControls(); + this._resetMouseScrollState(); + this.container.removeAttribute('contextmenu'); + this.contextMenuOpen = false; + + if (this.pdfThumbnailViewer) { + this.pdfThumbnailViewer.ensureThumbnailVisible(page); + } + }, + + /** + * @private + */ + _mouseDown: function PDFPresentationMode_mouseDown(evt) { + if (this.contextMenuOpen) { + this.contextMenuOpen = false; + evt.preventDefault(); + return; + } + if (evt.button === 0) { + // Enable clicking of links in presentation mode. Please note: + // Only links pointing to destinations in the current PDF document work. + var isInternalLink = (evt.target.href && + evt.target.classList.contains('internalLink')); + if (!isInternalLink) { + // Unless an internal link was clicked, advance one page. + evt.preventDefault(); + PDFViewerApplication.page += (evt.shiftKey ? -1 : 1); + } + } + }, + + /** + * @private + */ + _contextMenu: function PDFPresentationMode_contextMenu() { + this.contextMenuOpen = true; + }, + + /** + * @private + */ + _showControls: function PDFPresentationMode_showControls() { + if (this.controlsTimeout) { + clearTimeout(this.controlsTimeout); + } else { + this.container.classList.add(CONTROLS_SELECTOR); + } + this.controlsTimeout = setTimeout(function showControlsTimeout() { + this.container.classList.remove(CONTROLS_SELECTOR); + delete this.controlsTimeout; + }.bind(this), DELAY_BEFORE_HIDING_CONTROLS); + }, + + /** + * @private + */ + _hideControls: function PDFPresentationMode_hideControls() { + if (!this.controlsTimeout) { + return; + } + clearTimeout(this.controlsTimeout); + this.container.classList.remove(CONTROLS_SELECTOR); + delete this.controlsTimeout; + }, + + /** + * Resets the properties used for tracking mouse scrolling events. + * @private + */ + _resetMouseScrollState: + function PDFPresentationMode_resetMouseScrollState() { + this.mouseScrollTimeStamp = 0; + this.mouseScrollDelta = 0; + }, + + /** + * @private + */ + _addWindowListeners: function PDFPresentationMode_addWindowListeners() { + this.showControlsBind = this._showControls.bind(this); + this.mouseDownBind = this._mouseDown.bind(this); + this.resetMouseScrollStateBind = this._resetMouseScrollState.bind(this); + this.contextMenuBind = this._contextMenu.bind(this); + + window.addEventListener('mousemove', this.showControlsBind); + window.addEventListener('mousedown', this.mouseDownBind); + window.addEventListener('keydown', this.resetMouseScrollStateBind); + window.addEventListener('contextmenu', this.contextMenuBind); + }, + + /** + * @private + */ + _removeWindowListeners: + function PDFPresentationMode_removeWindowListeners() { + window.removeEventListener('mousemove', this.showControlsBind); + window.removeEventListener('mousedown', this.mouseDownBind); + window.removeEventListener('keydown', this.resetMouseScrollStateBind); + window.removeEventListener('contextmenu', this.contextMenuBind); + + delete this.showControlsBind; + delete this.mouseDownBind; + delete this.resetMouseScrollStateBind; + delete this.contextMenuBind; + }, + + /** + * @private + */ + _fullscreenChange: function PDFPresentationMode_fullscreenChange() { + if (this.isFullscreen) { + this._enter(); + } else { + this._exit(); + } + }, + + /** + * @private + */ + _addFullscreenChangeListeners: + function PDFPresentationMode_addFullscreenChangeListeners() { + this.fullscreenChangeBind = this._fullscreenChange.bind(this); + + window.addEventListener('fullscreenchange', this.fullscreenChangeBind); + window.addEventListener('mozfullscreenchange', this.fullscreenChangeBind); + window.addEventListener('webkitfullscreenchange', + this.fullscreenChangeBind); + window.addEventListener('MSFullscreenChange', this.fullscreenChangeBind); + }, + + /** + * @private + */ + _removeFullscreenChangeListeners: + function PDFPresentationMode_removeFullscreenChangeListeners() { + window.removeEventListener('fullscreenchange', this.fullscreenChangeBind); + window.removeEventListener('mozfullscreenchange', + this.fullscreenChangeBind); + window.removeEventListener('webkitfullscreenchange', + this.fullscreenChangeBind); + window.removeEventListener('MSFullscreenChange', + this.fullscreenChangeBind); + + delete this.fullscreenChangeBind; + } + }; + + return PDFPresentationMode; +})(); + + +/* Copyright 2013 Rob Wu + * https://github.com/Rob--W/grab-to-pan.js + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var GrabToPan = (function GrabToPanClosure() { + /** + * Construct a GrabToPan instance for a given HTML element. + * @param options.element {Element} + * @param options.ignoreTarget {function} optional. See `ignoreTarget(node)` + * @param options.onActiveChanged {function(boolean)} optional. Called + * when grab-to-pan is (de)activated. The first argument is a boolean that + * shows whether grab-to-pan is activated. + */ + function GrabToPan(options) { + this.element = options.element; + this.document = options.element.ownerDocument; + if (typeof options.ignoreTarget === 'function') { + this.ignoreTarget = options.ignoreTarget; + } + this.onActiveChanged = options.onActiveChanged; + + // Bind the contexts to ensure that `this` always points to + // the GrabToPan instance. + this.activate = this.activate.bind(this); + this.deactivate = this.deactivate.bind(this); + this.toggle = this.toggle.bind(this); + this._onmousedown = this._onmousedown.bind(this); + this._onmousemove = this._onmousemove.bind(this); + this._endPan = this._endPan.bind(this); + + // This overlay will be inserted in the document when the mouse moves during + // a grab operation, to ensure that the cursor has the desired appearance. + var overlay = this.overlay = document.createElement('div'); + overlay.className = 'grab-to-pan-grabbing'; + } + GrabToPan.prototype = { + /** + * Class name of element which can be grabbed + */ + CSS_CLASS_GRAB: 'grab-to-pan-grab', + + /** + * Bind a mousedown event to the element to enable grab-detection. + */ + activate: function GrabToPan_activate() { + if (!this.active) { + this.active = true; + this.element.addEventListener('mousedown', this._onmousedown, true); + this.element.classList.add(this.CSS_CLASS_GRAB); + if (this.onActiveChanged) { + this.onActiveChanged(true); + } + } + }, + + /** + * Removes all events. Any pending pan session is immediately stopped. + */ + deactivate: function GrabToPan_deactivate() { + if (this.active) { + this.active = false; + this.element.removeEventListener('mousedown', this._onmousedown, true); + this._endPan(); + this.element.classList.remove(this.CSS_CLASS_GRAB); + if (this.onActiveChanged) { + this.onActiveChanged(false); + } + } + }, + + toggle: function GrabToPan_toggle() { + if (this.active) { + this.deactivate(); + } else { + this.activate(); + } + }, + + /** + * Whether to not pan if the target element is clicked. + * Override this method to change the default behaviour. + * + * @param node {Element} The target of the event + * @return {boolean} Whether to not react to the click event. + */ + ignoreTarget: function GrabToPan_ignoreTarget(node) { + // Use matchesSelector to check whether the clicked element + // is (a child of) an input element / link + return node[matchesSelector]( + 'a[href], a[href] *, input, textarea, button, button *, select, option' + ); + }, + + /** + * @private + */ + _onmousedown: function GrabToPan__onmousedown(event) { + if (event.button !== 0 || this.ignoreTarget(event.target)) { + return; + } + if (event.originalTarget) { + try { + /* jshint expr:true */ + event.originalTarget.tagName; + } catch (e) { + // Mozilla-specific: element is a scrollbar (XUL element) + return; + } + } + + this.scrollLeftStart = this.element.scrollLeft; + this.scrollTopStart = this.element.scrollTop; + this.clientXStart = event.clientX; + this.clientYStart = event.clientY; + this.document.addEventListener('mousemove', this._onmousemove, true); + this.document.addEventListener('mouseup', this._endPan, true); + // When a scroll event occurs before a mousemove, assume that the user + // dragged a scrollbar (necessary for Opera Presto, Safari and IE) + // (not needed for Chrome/Firefox) + this.element.addEventListener('scroll', this._endPan, true); + event.preventDefault(); + event.stopPropagation(); + this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING); + + var focusedElement = document.activeElement; + if (focusedElement && !focusedElement.contains(event.target)) { + focusedElement.blur(); + } + }, + + /** + * @private + */ + _onmousemove: function GrabToPan__onmousemove(event) { + this.element.removeEventListener('scroll', this._endPan, true); + if (isLeftMouseReleased(event)) { + this._endPan(); + return; + } + var xDiff = event.clientX - this.clientXStart; + var yDiff = event.clientY - this.clientYStart; + this.element.scrollTop = this.scrollTopStart - yDiff; + this.element.scrollLeft = this.scrollLeftStart - xDiff; + if (!this.overlay.parentNode) { + document.body.appendChild(this.overlay); + } + }, + + /** + * @private + */ + _endPan: function GrabToPan__endPan() { + this.element.removeEventListener('scroll', this._endPan, true); + this.document.removeEventListener('mousemove', this._onmousemove, true); + this.document.removeEventListener('mouseup', this._endPan, true); + if (this.overlay.parentNode) { + this.overlay.parentNode.removeChild(this.overlay); + } + } + }; + + // Get the correct (vendor-prefixed) name of the matches method. + var matchesSelector; + ['webkitM', 'mozM', 'msM', 'oM', 'm'].some(function(prefix) { + var name = prefix + 'atches'; + if (name in document.documentElement) { + matchesSelector = name; + } + name += 'Selector'; + if (name in document.documentElement) { + matchesSelector = name; + } + return matchesSelector; // If found, then truthy, and [].some() ends. + }); + + // Browser sniffing because it's impossible to feature-detect + // whether event.which for onmousemove is reliable + var isNotIEorIsIE10plus = !document.documentMode || document.documentMode > 9; + var chrome = window.chrome; + var isChrome15OrOpera15plus = chrome && (chrome.webstore || chrome.app); + // ^ Chrome 15+ ^ Opera 15+ + var isSafari6plus = /Apple/.test(navigator.vendor) && + /Version\/([6-9]\d*|[1-5]\d+)/.test(navigator.userAgent); + + /** + * Whether the left mouse is not pressed. + * @param event {MouseEvent} + * @return {boolean} True if the left mouse button is not pressed. + * False if unsure or if the left mouse button is pressed. + */ + function isLeftMouseReleased(event) { + if ('buttons' in event && isNotIEorIsIE10plus) { + // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-buttons + // Firefox 15+ + // Internet Explorer 10+ + return !(event.buttons | 1); + } + if (isChrome15OrOpera15plus || isSafari6plus) { + // Chrome 14+ + // Opera 15+ + // Safari 6.0+ + return event.which === 0; + } + } + + return GrabToPan; +})(); + +var HandTool = { + initialize: function handToolInitialize(options) { + var toggleHandTool = options.toggleHandTool; + this.handTool = new GrabToPan({ + element: options.container, + onActiveChanged: function(isActive) { + if (!toggleHandTool) { + return; + } + if (isActive) { + toggleHandTool.title = + mozL10n.get('hand_tool_disable.title', null, 'Disable hand tool'); + toggleHandTool.firstElementChild.textContent = + mozL10n.get('hand_tool_disable_label', null, 'Disable hand tool'); + } else { + toggleHandTool.title = + mozL10n.get('hand_tool_enable.title', null, 'Enable hand tool'); + toggleHandTool.firstElementChild.textContent = + mozL10n.get('hand_tool_enable_label', null, 'Enable hand tool'); + } + } + }); + if (toggleHandTool) { + toggleHandTool.addEventListener('click', this.toggle.bind(this), false); + + window.addEventListener('localized', function (evt) { + Preferences.get('enableHandToolOnLoad').then(function resolved(value) { + if (value) { + this.handTool.activate(); + } + }.bind(this), function rejected(reason) {}); + }.bind(this)); + + window.addEventListener('presentationmodechanged', function (evt) { + if (evt.detail.switchInProgress) { + return; + } + if (evt.detail.active) { + this.enterPresentationMode(); + } else { + this.exitPresentationMode(); + } + }.bind(this)); + } + }, + + toggle: function handToolToggle() { + this.handTool.toggle(); + SecondaryToolbar.close(); + }, + + enterPresentationMode: function handToolEnterPresentationMode() { + if (this.handTool.active) { + this.wasActive = true; + this.handTool.deactivate(); + } + }, + + exitPresentationMode: function handToolExitPresentationMode() { + if (this.wasActive) { + this.wasActive = null; + this.handTool.activate(); + } + } +}; + + +var OverlayManager = { + overlays: {}, + active: null, + + /** + * @param {string} name The name of the overlay that is registered. This must + * be equal to the ID of the overlay's DOM element. + * @param {function} callerCloseMethod (optional) The method that, if present, + * will call OverlayManager.close from the Object + * registering the overlay. Access to this method is + * necessary in order to run cleanup code when e.g. + * the overlay is force closed. The default is null. + * @param {boolean} canForceClose (optional) Indicates if opening the overlay + * will close an active overlay. The default is false. + * @returns {Promise} A promise that is resolved when the overlay has been + * registered. + */ + register: function overlayManagerRegister(name, + callerCloseMethod, canForceClose) { + return new Promise(function (resolve) { + var element, container; + if (!name || !(element = document.getElementById(name)) || + !(container = element.parentNode)) { + throw new Error('Not enough parameters.'); + } else if (this.overlays[name]) { + throw new Error('The overlay is already registered.'); + } + this.overlays[name] = { element: element, + container: container, + callerCloseMethod: (callerCloseMethod || null), + canForceClose: (canForceClose || false) }; + resolve(); + }.bind(this)); + }, + + /** + * @param {string} name The name of the overlay that is unregistered. + * @returns {Promise} A promise that is resolved when the overlay has been + * unregistered. + */ + unregister: function overlayManagerUnregister(name) { + return new Promise(function (resolve) { + if (!this.overlays[name]) { + throw new Error('The overlay does not exist.'); + } else if (this.active === name) { + throw new Error('The overlay cannot be removed while it is active.'); + } + delete this.overlays[name]; + + resolve(); + }.bind(this)); + }, + + /** + * @param {string} name The name of the overlay that should be opened. + * @returns {Promise} A promise that is resolved when the overlay has been + * opened. + */ + open: function overlayManagerOpen(name) { + return new Promise(function (resolve) { + if (!this.overlays[name]) { + throw new Error('The overlay does not exist.'); + } else if (this.active) { + if (this.overlays[name].canForceClose) { + this._closeThroughCaller(); + } else if (this.active === name) { + throw new Error('The overlay is already active.'); + } else { + throw new Error('Another overlay is currently active.'); + } + } + this.active = name; + this.overlays[this.active].element.classList.remove('hidden'); + this.overlays[this.active].container.classList.remove('hidden'); + + window.addEventListener('keydown', this._keyDown); + resolve(); + }.bind(this)); + }, + + /** + * @param {string} name The name of the overlay that should be closed. + * @returns {Promise} A promise that is resolved when the overlay has been + * closed. + */ + close: function overlayManagerClose(name) { + return new Promise(function (resolve) { + if (!this.overlays[name]) { + throw new Error('The overlay does not exist.'); + } else if (!this.active) { + throw new Error('The overlay is currently not active.'); + } else if (this.active !== name) { + throw new Error('Another overlay is currently active.'); + } + this.overlays[this.active].container.classList.add('hidden'); + this.overlays[this.active].element.classList.add('hidden'); + this.active = null; + + window.removeEventListener('keydown', this._keyDown); + resolve(); + }.bind(this)); + }, + + /** + * @private + */ + _keyDown: function overlayManager_keyDown(evt) { + var self = OverlayManager; + if (self.active && evt.keyCode === 27) { // Esc key. + self._closeThroughCaller(); + evt.preventDefault(); + } + }, + + /** + * @private + */ + _closeThroughCaller: function overlayManager_closeThroughCaller() { + if (this.overlays[this.active].callerCloseMethod) { + this.overlays[this.active].callerCloseMethod(); + } + if (this.active) { + this.close(this.active); + } + } +}; + + +var PasswordPrompt = { + overlayName: null, + updatePassword: null, + reason: null, + passwordField: null, + passwordText: null, + passwordSubmit: null, + passwordCancel: null, + + initialize: function secondaryToolbarInitialize(options) { + this.overlayName = options.overlayName; + this.passwordField = options.passwordField; + this.passwordText = options.passwordText; + this.passwordSubmit = options.passwordSubmit; + this.passwordCancel = options.passwordCancel; + + // Attach the event listeners. + this.passwordSubmit.addEventListener('click', + this.verifyPassword.bind(this)); + + this.passwordCancel.addEventListener('click', this.close.bind(this)); + + this.passwordField.addEventListener('keydown', function (e) { + if (e.keyCode === 13) { // Enter key + this.verifyPassword(); + } + }.bind(this)); + + OverlayManager.register(this.overlayName, this.close.bind(this), true); + }, + + open: function passwordPromptOpen() { + OverlayManager.open(this.overlayName).then(function () { + this.passwordField.focus(); + + var promptString = mozL10n.get('password_label', null, + 'Enter the password to open this PDF file.'); + + if (this.reason === PDFJS.PasswordResponses.INCORRECT_PASSWORD) { + promptString = mozL10n.get('password_invalid', null, + 'Invalid password. Please try again.'); + } + + this.passwordText.textContent = promptString; + }.bind(this)); + }, + + close: function passwordPromptClose() { + OverlayManager.close(this.overlayName).then(function () { + this.passwordField.value = ''; + }.bind(this)); + }, + + verifyPassword: function passwordPromptVerifyPassword() { + var password = this.passwordField.value; + if (password && password.length > 0) { + this.close(); + return this.updatePassword(password); + } + } +}; + + +/** + * @typedef {Object} PDFDocumentPropertiesOptions + * @property {string} overlayName - Name/identifier for the overlay. + * @property {Object} fields - Names and elements of the overlay's fields. + * @property {HTMLButtonElement} closeButton - Button for closing the overlay. + */ + +/** + * @class + */ +var PDFDocumentProperties = (function PDFDocumentPropertiesClosure() { + /** + * @constructs PDFDocumentProperties + * @param {PDFDocumentPropertiesOptions} options + */ + function PDFDocumentProperties(options) { + this.fields = options.fields; + this.overlayName = options.overlayName; + + this.rawFileSize = 0; + this.url = null; + this.pdfDocument = null; + + // Bind the event listener for the Close button. + if (options.closeButton) { + options.closeButton.addEventListener('click', this.close.bind(this)); + } + + this.dataAvailablePromise = new Promise(function (resolve) { + this.resolveDataAvailable = resolve; + }.bind(this)); + + OverlayManager.register(this.overlayName, this.close.bind(this)); + } + + PDFDocumentProperties.prototype = { + /** + * Open the document properties overlay. + */ + open: function PDFDocumentProperties_open() { + Promise.all([OverlayManager.open(this.overlayName), + this.dataAvailablePromise]).then(function () { + this._getProperties(); + }.bind(this)); + }, + + /** + * Close the document properties overlay. + */ + close: function PDFDocumentProperties_close() { + OverlayManager.close(this.overlayName); + }, + + /** + * Set the file size of the PDF document. This method is used to + * update the file size in the document properties overlay once it + * is known so we do not have to wait until the entire file is loaded. + * + * @param {number} fileSize - The file size of the PDF document. + */ + setFileSize: function PDFDocumentProperties_setFileSize(fileSize) { + if (fileSize > 0) { + this.rawFileSize = fileSize; + } + }, + + /** + * Set a reference to the PDF document and the URL in order + * to populate the overlay fields with the document properties. + * Note that the overlay will contain no information if this method + * is not called. + * + * @param {Object} pdfDocument - A reference to the PDF document. + * @param {string} url - The URL of the document. + */ + setDocumentAndUrl: + function PDFDocumentProperties_setDocumentAndUrl(pdfDocument, url) { + this.pdfDocument = pdfDocument; + this.url = url; + this.resolveDataAvailable(); + }, + + /** + * @private + */ + _getProperties: function PDFDocumentProperties_getProperties() { + if (!OverlayManager.active) { + // If the dialog was closed before dataAvailablePromise was resolved, + // don't bother updating the properties. + return; + } + // Get the file size (if it hasn't already been set). + this.pdfDocument.getDownloadInfo().then(function(data) { + if (data.length === this.rawFileSize) { + return; + } + this.setFileSize(data.length); + this._updateUI(this.fields['fileSize'], this._parseFileSize()); + }.bind(this)); + + // Get the document properties. + this.pdfDocument.getMetadata().then(function(data) { + var content = { + 'fileName': getPDFFileNameFromURL(this.url), + 'fileSize': this._parseFileSize(), + 'title': data.info.Title, + 'author': data.info.Author, + 'subject': data.info.Subject, + 'keywords': data.info.Keywords, + 'creationDate': this._parseDate(data.info.CreationDate), + 'modificationDate': this._parseDate(data.info.ModDate), + 'creator': data.info.Creator, + 'producer': data.info.Producer, + 'version': data.info.PDFFormatVersion, + 'pageCount': this.pdfDocument.numPages + }; + + // Show the properties in the dialog. + for (var identifier in content) { + this._updateUI(this.fields[identifier], content[identifier]); + } + }.bind(this)); + }, + + /** + * @private + */ + _updateUI: function PDFDocumentProperties_updateUI(field, content) { + if (field && content !== undefined && content !== '') { + field.textContent = content; + } + }, + + /** + * @private + */ + _parseFileSize: function PDFDocumentProperties_parseFileSize() { + var fileSize = this.rawFileSize, kb = fileSize / 1024; + if (!kb) { + return; + } else if (kb < 1024) { + return mozL10n.get('document_properties_kb', { + size_kb: (+kb.toPrecision(3)).toLocaleString(), + size_b: fileSize.toLocaleString() + }, '{{size_kb}} KB ({{size_b}} bytes)'); + } else { + return mozL10n.get('document_properties_mb', { + size_mb: (+(kb / 1024).toPrecision(3)).toLocaleString(), + size_b: fileSize.toLocaleString() + }, '{{size_mb}} MB ({{size_b}} bytes)'); + } + }, + + /** + * @private + */ + _parseDate: function PDFDocumentProperties_parseDate(inputDate) { + // This is implemented according to the PDF specification, but note that + // Adobe Reader doesn't handle changing the date to universal time + // and doesn't use the user's time zone (they're effectively ignoring + // the HH' and mm' parts of the date string). + var dateToParse = inputDate; + if (dateToParse === undefined) { + return ''; + } + + // Remove the D: prefix if it is available. + if (dateToParse.substring(0,2) === 'D:') { + dateToParse = dateToParse.substring(2); + } + + // Get all elements from the PDF date string. + // JavaScript's Date object expects the month to be between + // 0 and 11 instead of 1 and 12, so we're correcting for this. + var year = parseInt(dateToParse.substring(0,4), 10); + var month = parseInt(dateToParse.substring(4,6), 10) - 1; + var day = parseInt(dateToParse.substring(6,8), 10); + var hours = parseInt(dateToParse.substring(8,10), 10); + var minutes = parseInt(dateToParse.substring(10,12), 10); + var seconds = parseInt(dateToParse.substring(12,14), 10); + var utRel = dateToParse.substring(14,15); + var offsetHours = parseInt(dateToParse.substring(15,17), 10); + var offsetMinutes = parseInt(dateToParse.substring(18,20), 10); + + // As per spec, utRel = 'Z' means equal to universal time. + // The other cases ('-' and '+') have to be handled here. + if (utRel === '-') { + hours += offsetHours; + minutes += offsetMinutes; + } else if (utRel === '+') { + hours -= offsetHours; + minutes -= offsetMinutes; + } + + // Return the new date format from the user's locale. + var date = new Date(Date.UTC(year, month, day, hours, minutes, seconds)); + var dateString = date.toLocaleDateString(); + var timeString = date.toLocaleTimeString(); + return mozL10n.get('document_properties_date_string', + {date: dateString, time: timeString}, + '{{date}}, {{time}}'); + } + }; + + return PDFDocumentProperties; +})(); + + +var PresentationModeState = { + UNKNOWN: 0, + NORMAL: 1, + CHANGING: 2, + FULLSCREEN: 3, +}; + +var IGNORE_CURRENT_POSITION_ON_ZOOM = false; +var DEFAULT_CACHE_SIZE = 10; + + +var CLEANUP_TIMEOUT = 30000; + +var RenderingStates = { + INITIAL: 0, + RUNNING: 1, + PAUSED: 2, + FINISHED: 3 +}; + +/** + * Controls rendering of the views for pages and thumbnails. + * @class + */ +var PDFRenderingQueue = (function PDFRenderingQueueClosure() { + /** + * @constructs + */ + function PDFRenderingQueue() { + this.pdfViewer = null; + this.pdfThumbnailViewer = null; + this.onIdle = null; + + this.highestPriorityPage = null; + this.idleTimeout = null; + this.printing = false; + this.isThumbnailViewEnabled = false; + } + + PDFRenderingQueue.prototype = /** @lends PDFRenderingQueue.prototype */ { + /** + * @param {PDFViewer} pdfViewer + */ + setViewer: function PDFRenderingQueue_setViewer(pdfViewer) { + this.pdfViewer = pdfViewer; + }, + + /** + * @param {PDFThumbnailViewer} pdfThumbnailViewer + */ + setThumbnailViewer: + function PDFRenderingQueue_setThumbnailViewer(pdfThumbnailViewer) { + this.pdfThumbnailViewer = pdfThumbnailViewer; + }, + + /** + * @param {IRenderableView} view + * @returns {boolean} + */ + isHighestPriority: function PDFRenderingQueue_isHighestPriority(view) { + return this.highestPriorityPage === view.renderingId; + }, + + renderHighestPriority: function + PDFRenderingQueue_renderHighestPriority(currentlyVisiblePages) { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + this.idleTimeout = null; + } + + // Pages have a higher priority than thumbnails, so check them first. + if (this.pdfViewer.forceRendering(currentlyVisiblePages)) { + return; + } + // No pages needed rendering so check thumbnails. + if (this.pdfThumbnailViewer && this.isThumbnailViewEnabled) { + if (this.pdfThumbnailViewer.forceRendering()) { + return; + } + } + + if (this.printing) { + // If printing is currently ongoing do not reschedule cleanup. + return; + } + + if (this.onIdle) { + this.idleTimeout = setTimeout(this.onIdle.bind(this), CLEANUP_TIMEOUT); + } + }, + + getHighestPriority: function + PDFRenderingQueue_getHighestPriority(visible, views, scrolledDown) { + // The state has changed figure out which page has the highest priority to + // render next (if any). + // Priority: + // 1 visible pages + // 2 if last scrolled down page after the visible pages + // 2 if last scrolled up page before the visible pages + var visibleViews = visible.views; + + var numVisible = visibleViews.length; + if (numVisible === 0) { + return false; + } + for (var i = 0; i < numVisible; ++i) { + var view = visibleViews[i].view; + if (!this.isViewFinished(view)) { + return view; + } + } + + // All the visible views have rendered, try to render next/previous pages. + if (scrolledDown) { + var nextPageIndex = visible.last.id; + // ID's start at 1 so no need to add 1. + if (views[nextPageIndex] && + !this.isViewFinished(views[nextPageIndex])) { + return views[nextPageIndex]; + } + } else { + var previousPageIndex = visible.first.id - 2; + if (views[previousPageIndex] && + !this.isViewFinished(views[previousPageIndex])) { + return views[previousPageIndex]; + } + } + // Everything that needs to be rendered has been. + return null; + }, + + /** + * @param {IRenderableView} view + * @returns {boolean} + */ + isViewFinished: function PDFRenderingQueue_isViewFinished(view) { + return view.renderingState === RenderingStates.FINISHED; + }, + + /** + * Render a page or thumbnail view. This calls the appropriate function + * based on the views state. If the view is already rendered it will return + * false. + * @param {IRenderableView} view + */ + renderView: function PDFRenderingQueue_renderView(view) { + var state = view.renderingState; + switch (state) { + case RenderingStates.FINISHED: + return false; + case RenderingStates.PAUSED: + this.highestPriorityPage = view.renderingId; + view.resume(); + break; + case RenderingStates.RUNNING: + this.highestPriorityPage = view.renderingId; + break; + case RenderingStates.INITIAL: + this.highestPriorityPage = view.renderingId; + var continueRendering = function () { + this.renderHighestPriority(); + }.bind(this); + view.draw().then(continueRendering, continueRendering); + break; + } + return true; + }, + }; + + return PDFRenderingQueue; +})(); + + +var TEXT_LAYER_RENDER_DELAY = 200; // ms + +/** + * @typedef {Object} PDFPageViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {number} id - The page unique ID (normally its number). + * @property {number} scale - The page scale display. + * @property {PageViewport} defaultViewport - The page viewport. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. + * @property {IPDFTextLayerFactory} textLayerFactory + * @property {IPDFAnnotationsLayerFactory} annotationsLayerFactory + */ + +/** + * @class + * @implements {IRenderableView} + */ +var PDFPageView = (function PDFPageViewClosure() { + /** + * @constructs PDFPageView + * @param {PDFPageViewOptions} options + */ + function PDFPageView(options) { + var container = options.container; + var id = options.id; + var scale = options.scale; + var defaultViewport = options.defaultViewport; + var renderingQueue = options.renderingQueue; + var textLayerFactory = options.textLayerFactory; + var annotationsLayerFactory = options.annotationsLayerFactory; + + this.id = id; + this.renderingId = 'page' + id; + + this.rotation = 0; + this.scale = scale || 1.0; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this.hasRestrictedScaling = false; + + this.renderingQueue = renderingQueue; + this.textLayerFactory = textLayerFactory; + this.annotationsLayerFactory = annotationsLayerFactory; + + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + + this.onBeforeDraw = null; + this.onAfterDraw = null; + + this.textLayer = null; + + this.zoomLayer = null; + + this.annotationLayer = null; + + var div = document.createElement('div'); + div.id = 'pageContainer' + this.id; + div.className = 'page'; + div.style.width = Math.floor(this.viewport.width) + 'px'; + div.style.height = Math.floor(this.viewport.height) + 'px'; + div.setAttribute('data-page-number', this.id); + this.div = div; + + container.appendChild(div); + } + + PDFPageView.prototype = { + setPdfPage: function PDFPageView_setPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, + totalRotation); + this.stats = pdfPage.stats; + this.reset(); + }, + + destroy: function PDFPageView_destroy() { + this.zoomLayer = null; + this.reset(); + if (this.pdfPage) { + this.pdfPage.destroy(); + } + }, + + reset: function PDFPageView_reset(keepAnnotations) { + if (this.renderTask) { + this.renderTask.cancel(); + } + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + var div = this.div; + div.style.width = Math.floor(this.viewport.width) + 'px'; + div.style.height = Math.floor(this.viewport.height) + 'px'; + + var childNodes = div.childNodes; + var currentZoomLayer = this.zoomLayer || null; + var currentAnnotationNode = (keepAnnotations && this.annotationLayer && + this.annotationLayer.div) || null; + for (var i = childNodes.length - 1; i >= 0; i--) { + var node = childNodes[i]; + if (currentZoomLayer === node || currentAnnotationNode === node) { + continue; + } + div.removeChild(node); + } + div.removeAttribute('data-loaded'); + + if (keepAnnotations) { + if (this.annotationLayer) { + // Hide annotationLayer until all elements are resized + // so they are not displayed on the already-resized page + this.annotationLayer.hide(); + } + } else { + this.annotationLayer = null; + } + + if (this.canvas) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + this.canvas.width = 0; + this.canvas.height = 0; + delete this.canvas; + } + + this.loadingIconDiv = document.createElement('div'); + this.loadingIconDiv.className = 'loadingIcon'; + div.appendChild(this.loadingIconDiv); + }, + + update: function PDFPageView_update(scale, rotation) { + this.scale = scale || this.scale; + + if (typeof rotation !== 'undefined') { + this.rotation = rotation; + } + + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: this.scale * CSS_UNITS, + rotation: totalRotation + }); + + var isScalingRestricted = false; + if (this.canvas && PDFJS.maxCanvasPixels > 0) { + var ctx = this.canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + var pixelsInViewport = this.viewport.width * this.viewport.height; + var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); + if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) * + ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > + PDFJS.maxCanvasPixels) { + isScalingRestricted = true; + } + } + + if (this.canvas && + (PDFJS.useOnlyCssZoom || + (this.hasRestrictedScaling && isScalingRestricted))) { + this.cssTransform(this.canvas, true); + return; + } else if (this.canvas && !this.zoomLayer) { + this.zoomLayer = this.canvas.parentNode; + this.zoomLayer.style.position = 'absolute'; + } + if (this.zoomLayer) { + this.cssTransform(this.zoomLayer.firstChild); + } + this.reset(true); + }, + + /** + * Called when moved in the parent's container. + */ + updatePosition: function PDFPageView_updatePosition() { + if (this.textLayer) { + this.textLayer.render(TEXT_LAYER_RENDER_DELAY); + } + }, + + cssTransform: function PDFPageView_transform(canvas, redrawAnnotations) { + // Scale canvas, canvas wrapper, and page container. + var width = this.viewport.width; + var height = this.viewport.height; + var div = this.div; + canvas.style.width = canvas.parentNode.style.width = div.style.width = + Math.floor(width) + 'px'; + canvas.style.height = canvas.parentNode.style.height = div.style.height = + Math.floor(height) + 'px'; + // The canvas may have been originally rotated, rotate relative to that. + var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; + var absRotation = Math.abs(relativeRotation); + var scaleX = 1, scaleY = 1; + if (absRotation === 90 || absRotation === 270) { + // Scale x and y because of the rotation. + scaleX = height / width; + scaleY = width / height; + } + var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + + 'scale(' + scaleX + ',' + scaleY + ')'; + CustomStyle.setProp('transform', canvas, cssTransform); + + if (this.textLayer) { + // Rotating the text layer is more complicated since the divs inside the + // the text layer are rotated. + // TODO: This could probably be simplified by drawing the text layer in + // one orientation then rotating overall. + var textLayerViewport = this.textLayer.viewport; + var textRelativeRotation = this.viewport.rotation - + textLayerViewport.rotation; + var textAbsRotation = Math.abs(textRelativeRotation); + var scale = width / textLayerViewport.width; + if (textAbsRotation === 90 || textAbsRotation === 270) { + scale = width / textLayerViewport.height; + } + var textLayerDiv = this.textLayer.textLayerDiv; + var transX, transY; + switch (textAbsRotation) { + case 0: + transX = transY = 0; + break; + case 90: + transX = 0; + transY = '-' + textLayerDiv.style.height; + break; + case 180: + transX = '-' + textLayerDiv.style.width; + transY = '-' + textLayerDiv.style.height; + break; + case 270: + transX = '-' + textLayerDiv.style.width; + transY = 0; + break; + default: + console.error('Bad rotation value.'); + break; + } + CustomStyle.setProp('transform', textLayerDiv, + 'rotate(' + textAbsRotation + 'deg) ' + + 'scale(' + scale + ', ' + scale + ') ' + + 'translate(' + transX + ', ' + transY + ')'); + CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); + } + + if (redrawAnnotations && this.annotationLayer) { + this.annotationLayer.setupAnnotations(this.viewport); + } + }, + + get width() { + return this.viewport.width; + }, + + get height() { + return this.viewport.height; + }, + + getPagePoint: function PDFPageView_getPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + }, + + draw: function PDFPageView_draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } + + this.renderingState = RenderingStates.RUNNING; + + var pdfPage = this.pdfPage; + var viewport = this.viewport; + var div = this.div; + // Wrap the canvas so if it has a css transform for highdpi the overflow + // will be hidden in FF. + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = div.style.width; + canvasWrapper.style.height = div.style.height; + canvasWrapper.classList.add('canvasWrapper'); + + var canvas = document.createElement('canvas'); + canvas.id = 'page' + this.id; + canvasWrapper.appendChild(canvas); + if (this.annotationLayer) { + // annotationLayer needs to stay on top + div.insertBefore(canvasWrapper, this.annotationLayer.div); + } else { + div.appendChild(canvasWrapper); + } + this.canvas = canvas; + + var ctx = canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + + if (PDFJS.useOnlyCssZoom) { + var actualSizeViewport = viewport.clone({ scale: CSS_UNITS }); + // Use a scale that will make the canvas be the original intended size + // of the page. + outputScale.sx *= actualSizeViewport.width / viewport.width; + outputScale.sy *= actualSizeViewport.height / viewport.height; + outputScale.scaled = true; + } + + if (PDFJS.maxCanvasPixels > 0) { + var pixelsInViewport = viewport.width * viewport.height; + var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + outputScale.scaled = true; + this.hasRestrictedScaling = true; + } else { + this.hasRestrictedScaling = false; + } + } + + canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; + canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; + canvas.style.width = Math.floor(viewport.width) + 'px'; + canvas.style.height = Math.floor(viewport.height) + 'px'; + // Add the viewport so it's known what it was originally drawn with. + canvas._viewport = viewport; + + var textLayerDiv = null; + var textLayer = null; + if (this.textLayerFactory) { + textLayerDiv = document.createElement('div'); + textLayerDiv.className = 'textLayer'; + textLayerDiv.style.width = canvas.style.width; + textLayerDiv.style.height = canvas.style.height; + if (this.annotationLayer) { + // annotationLayer needs to stay on top + div.insertBefore(textLayerDiv, this.annotationLayer.div); + } else { + div.appendChild(textLayerDiv); + } + + textLayer = this.textLayerFactory.createTextLayerBuilder(textLayerDiv, + this.id - 1, + this.viewport); + } + this.textLayer = textLayer; + + if (outputScale.scaled) { + // Used by the mozCurrentTransform polyfill in src/display/canvas.js. + ctx._transformMatrix = [outputScale.sx, 0, 0, outputScale.sy, 0, 0]; + ctx.scale(outputScale.sx, outputScale.sy); + } + + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; + }); + + // Rendering area + + var self = this; + function pageViewDrawCallback(error) { + // The renderTask may have been replaced by a new one, so only remove + // the reference to the renderTask if it matches the one that is + // triggering this callback. + if (renderTask === self.renderTask) { + self.renderTask = null; + } + + if (error === 'cancelled') { + rejectRenderPromise(error); + return; + } + + self.renderingState = RenderingStates.FINISHED; + + if (self.loadingIconDiv) { + div.removeChild(self.loadingIconDiv); + delete self.loadingIconDiv; + } + + if (self.zoomLayer) { + div.removeChild(self.zoomLayer); + self.zoomLayer = null; + } + + self.error = error; + self.stats = pdfPage.stats; + if (self.onAfterDraw) { + self.onAfterDraw(); + } + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagerendered', true, true, { + pageNumber: self.id + }); + div.dispatchEvent(event); + // This custom event is deprecated, and will be removed in the future, + // please use the |pagerendered| event instead. + var deprecatedEvent = document.createEvent('CustomEvent'); + deprecatedEvent.initCustomEvent('pagerender', true, true, { + pageNumber: pdfPage.pageNumber + }); + div.dispatchEvent(deprecatedEvent); + + if (!error) { + resolveRenderPromise(undefined); + } else { + rejectRenderPromise(error); + } + } + + var renderContinueCallback = null; + if (this.renderingQueue) { + renderContinueCallback = function renderContinueCallback(cont) { + if (!self.renderingQueue.isHighestPriority(self)) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + } + + var renderContext = { + canvasContext: ctx, + viewport: this.viewport, + // intent: 'default', // === 'display' + continueCallback: renderContinueCallback + }; + var renderTask = this.renderTask = this.pdfPage.render(renderContext); + + this.renderTask.promise.then( + function pdfPageRenderCallback() { + pageViewDrawCallback(null); + if (textLayer) { + self.pdfPage.getTextContent().then( + function textContentResolved(textContent) { + textLayer.setTextContent(textContent); + textLayer.render(TEXT_LAYER_RENDER_DELAY); + } + ); + } + }, + function pdfPageRenderError(error) { + pageViewDrawCallback(error); + } + ); + + if (this.annotationsLayerFactory) { + if (!this.annotationLayer) { + this.annotationLayer = this.annotationsLayerFactory. + createAnnotationsLayerBuilder(div, this.pdfPage); + } + this.annotationLayer.setupAnnotations(this.viewport); + } + div.setAttribute('data-loaded', true); + + if (self.onBeforeDraw) { + self.onBeforeDraw(); + } + return promise; + }, + + beforePrint: function PDFPageView_beforePrint() { + var pdfPage = this.pdfPage; + + var viewport = pdfPage.getViewport(1); + // Use the same hack we use for high dpi displays for printing to get + // better output until bug 811002 is fixed in FF. + var PRINT_OUTPUT_SCALE = 2; + var canvas = document.createElement('canvas'); + + // The logical size of the canvas. + canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; + canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; + + // The rendered size of the canvas, relative to the size of canvasWrapper. + canvas.style.width = (PRINT_OUTPUT_SCALE * 100) + '%'; + canvas.style.height = (PRINT_OUTPUT_SCALE * 100) + '%'; + + var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + + (1 / PRINT_OUTPUT_SCALE) + ')'; + CustomStyle.setProp('transform' , canvas, cssScale); + CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); + + var printContainer = document.getElementById('printContainer'); + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = viewport.width + 'pt'; + canvasWrapper.style.height = viewport.height + 'pt'; + canvasWrapper.appendChild(canvas); + printContainer.appendChild(canvasWrapper); + + canvas.mozPrintCallback = function(obj) { + var ctx = obj.context; + + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + // Used by the mozCurrentTransform polyfill in src/display/canvas.js. + ctx._transformMatrix = + [PRINT_OUTPUT_SCALE, 0, 0, PRINT_OUTPUT_SCALE, 0, 0]; + ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); + + var renderContext = { + canvasContext: ctx, + viewport: viewport, + intent: 'print' + }; + + pdfPage.render(renderContext).promise.then(function() { + // Tell the printEngine that rendering this canvas/page has finished. + obj.done(); + }, function(error) { + console.error(error); + // Tell the printEngine that rendering this canvas/page has failed. + // This will make the print proces stop. + if ('abort' in obj) { + obj.abort(); + } else { + obj.done(); + } + }); + }; + }, + }; + + return PDFPageView; +})(); + + +var MAX_TEXT_DIVS_TO_RENDER = 100000; + +var NonWhitespaceRegexp = /\S/; + +function isAllWhitespace(str) { + return !NonWhitespaceRegexp.test(str); +} + +/** + * @typedef {Object} TextLayerBuilderOptions + * @property {HTMLDivElement} textLayerDiv - The text layer container. + * @property {number} pageIndex - The page index. + * @property {PageViewport} viewport - The viewport of the text layer. + * @property {PDFFindController} findController + */ + +/** + * TextLayerBuilder provides text-selection functionality for the PDF. + * It does this by creating overlay divs over the PDF text. These divs + * contain text that matches the PDF text they are overlaying. This object + * also provides a way to highlight text that is being searched for. + * @class + */ +var TextLayerBuilder = (function TextLayerBuilderClosure() { + function TextLayerBuilder(options) { + this.textLayerDiv = options.textLayerDiv; + this.renderingDone = false; + this.divContentDone = false; + this.pageIdx = options.pageIndex; + this.pageNumber = this.pageIdx + 1; + this.matches = []; + this.viewport = options.viewport; + this.textDivs = []; + this.findController = options.findController || null; + } + + TextLayerBuilder.prototype = { + _finishRendering: function TextLayerBuilder_finishRendering() { + this.renderingDone = true; + + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('textlayerrendered', true, true, { + pageNumber: this.pageNumber + }); + this.textLayerDiv.dispatchEvent(event); + }, + + renderLayer: function TextLayerBuilder_renderLayer() { + var textLayerFrag = document.createDocumentFragment(); + var textDivs = this.textDivs; + var textDivsLength = textDivs.length; + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + // No point in rendering many divs as it would make the browser + // unusable even after the divs are rendered. + if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) { + this._finishRendering(); + return; + } + + var lastFontSize; + var lastFontFamily; + for (var i = 0; i < textDivsLength; i++) { + var textDiv = textDivs[i]; + if (textDiv.dataset.isWhitespace !== undefined) { + continue; + } + + var fontSize = textDiv.style.fontSize; + var fontFamily = textDiv.style.fontFamily; + + // Only build font string and set to context if different from last. + if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) { + ctx.font = fontSize + ' ' + fontFamily; + lastFontSize = fontSize; + lastFontFamily = fontFamily; + } + + var width = ctx.measureText(textDiv.textContent).width; + if (width > 0) { + textLayerFrag.appendChild(textDiv); + var transform; + if (textDiv.dataset.canvasWidth !== undefined) { + // Dataset values come of type string. + var textScale = textDiv.dataset.canvasWidth / width; + transform = 'scaleX(' + textScale + ')'; + } else { + transform = ''; + } + var rotation = textDiv.dataset.angle; + if (rotation) { + transform = 'rotate(' + rotation + 'deg) ' + transform; + } + if (transform) { + CustomStyle.setProp('transform' , textDiv, transform); + } + } + } + + this.textLayerDiv.appendChild(textLayerFrag); + this._finishRendering(); + this.updateMatches(); + }, + + /** + * Renders the text layer. + * @param {number} timeout (optional) if specified, the rendering waits + * for specified amount of ms. + */ + render: function TextLayerBuilder_render(timeout) { + if (!this.divContentDone || this.renderingDone) { + return; + } + + if (this.renderTimer) { + clearTimeout(this.renderTimer); + this.renderTimer = null; + } + + if (!timeout) { // Render right away + this.renderLayer(); + } else { // Schedule + var self = this; + this.renderTimer = setTimeout(function() { + self.renderLayer(); + self.renderTimer = null; + }, timeout); + } + }, + + appendText: function TextLayerBuilder_appendText(geom, styles) { + var style = styles[geom.fontName]; + var textDiv = document.createElement('div'); + this.textDivs.push(textDiv); + if (isAllWhitespace(geom.str)) { + textDiv.dataset.isWhitespace = true; + return; + } + var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); + var angle = Math.atan2(tx[1], tx[0]); + if (style.vertical) { + angle += Math.PI / 2; + } + var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); + var fontAscent = fontHeight; + if (style.ascent) { + fontAscent = style.ascent * fontAscent; + } else if (style.descent) { + fontAscent = (1 + style.descent) * fontAscent; + } + + var left; + var top; + if (angle === 0) { + left = tx[4]; + top = tx[5] - fontAscent; + } else { + left = tx[4] + (fontAscent * Math.sin(angle)); + top = tx[5] - (fontAscent * Math.cos(angle)); + } + textDiv.style.left = left + 'px'; + textDiv.style.top = top + 'px'; + textDiv.style.fontSize = fontHeight + 'px'; + textDiv.style.fontFamily = style.fontFamily; + + textDiv.textContent = geom.str; + // |fontName| is only used by the Font Inspector. This test will succeed + // when e.g. the Font Inspector is off but the Stepper is on, but it's + // not worth the effort to do a more accurate test. + if (PDFJS.pdfBug) { + textDiv.dataset.fontName = geom.fontName; + } + // Storing into dataset will convert number into string. + if (angle !== 0) { + textDiv.dataset.angle = angle * (180 / Math.PI); + } + // We don't bother scaling single-char text divs, because it has very + // little effect on text highlighting. This makes scrolling on docs with + // lots of such divs a lot faster. + if (textDiv.textContent.length > 1) { + if (style.vertical) { + textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; + } else { + textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; + } + } + }, + + setTextContent: function TextLayerBuilder_setTextContent(textContent) { + this.textContent = textContent; + + var textItems = textContent.items; + for (var i = 0, len = textItems.length; i < len; i++) { + this.appendText(textItems[i], textContent.styles); + } + this.divContentDone = true; + }, + + convertMatches: function TextLayerBuilder_convertMatches(matches) { + var i = 0; + var iIndex = 0; + var bidiTexts = this.textContent.items; + var end = bidiTexts.length - 1; + var queryLen = (this.findController === null ? + 0 : this.findController.state.query.length); + var ret = []; + + for (var m = 0, len = matches.length; m < len; m++) { + // Calculate the start position. + var matchIdx = matches[m]; + + // Loop over the divIdxs. + while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { + iIndex += bidiTexts[i].str.length; + i++; + } + + if (i === bidiTexts.length) { + console.error('Could not find a matching mapping'); + } + + var match = { + begin: { + divIdx: i, + offset: matchIdx - iIndex + } + }; + + // Calculate the end position. + matchIdx += queryLen; + + // Somewhat the same array as above, but use > instead of >= to get + // the end position right. + while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { + iIndex += bidiTexts[i].str.length; + i++; + } + + match.end = { + divIdx: i, + offset: matchIdx - iIndex + }; + ret.push(match); + } + + return ret; + }, + + renderMatches: function TextLayerBuilder_renderMatches(matches) { + // Early exit if there is nothing to render. + if (matches.length === 0) { + return; + } + + var bidiTexts = this.textContent.items; + var textDivs = this.textDivs; + var prevEnd = null; + var pageIdx = this.pageIdx; + var isSelectedPage = (this.findController === null ? + false : (pageIdx === this.findController.selected.pageIdx)); + var selectedMatchIdx = (this.findController === null ? + -1 : this.findController.selected.matchIdx); + var highlightAll = (this.findController === null ? + false : this.findController.state.highlightAll); + var infinity = { + divIdx: -1, + offset: undefined + }; + + function beginText(begin, className) { + var divIdx = begin.divIdx; + textDivs[divIdx].textContent = ''; + appendTextToDiv(divIdx, 0, begin.offset, className); + } + + function appendTextToDiv(divIdx, fromOffset, toOffset, className) { + var div = textDivs[divIdx]; + var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); + var node = document.createTextNode(content); + if (className) { + var span = document.createElement('span'); + span.className = className; + span.appendChild(node); + div.appendChild(span); + return; + } + div.appendChild(node); + } + + var i0 = selectedMatchIdx, i1 = i0 + 1; + if (highlightAll) { + i0 = 0; + i1 = matches.length; + } else if (!isSelectedPage) { + // Not highlighting all and this isn't the selected page, so do nothing. + return; + } + + for (var i = i0; i < i1; i++) { + var match = matches[i]; + var begin = match.begin; + var end = match.end; + var isSelected = (isSelectedPage && i === selectedMatchIdx); + var highlightSuffix = (isSelected ? ' selected' : ''); + + if (this.findController) { + this.findController.updateMatchPosition(pageIdx, i, textDivs, + begin.divIdx, end.divIdx); + } + + // Match inside new div. + if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { + // If there was a previous div, then add the text at the end. + if (prevEnd !== null) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + // Clear the divs and set the content until the starting point. + beginText(begin); + } else { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset); + } + + if (begin.divIdx === end.divIdx) { + appendTextToDiv(begin.divIdx, begin.offset, end.offset, + 'highlight' + highlightSuffix); + } else { + appendTextToDiv(begin.divIdx, begin.offset, infinity.offset, + 'highlight begin' + highlightSuffix); + for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) { + textDivs[n0].className = 'highlight middle' + highlightSuffix; + } + beginText(end, 'highlight end' + highlightSuffix); + } + prevEnd = end; + } + + if (prevEnd) { + appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset); + } + }, + + updateMatches: function TextLayerBuilder_updateMatches() { + // Only show matches when all rendering is done. + if (!this.renderingDone) { + return; + } + + // Clear all matches. + var matches = this.matches; + var textDivs = this.textDivs; + var bidiTexts = this.textContent.items; + var clearedUntilDivIdx = -1; + + // Clear all current matches. + for (var i = 0, len = matches.length; i < len; i++) { + var match = matches[i]; + var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); + for (var n = begin, end = match.end.divIdx; n <= end; n++) { + var div = textDivs[n]; + div.textContent = bidiTexts[n].str; + div.className = ''; + } + clearedUntilDivIdx = match.end.divIdx + 1; + } + + if (this.findController === null || !this.findController.active) { + return; + } + + // Convert the matches on the page controller into the match format + // used for the textLayer. + this.matches = this.convertMatches(this.findController === null ? + [] : (this.findController.pageMatches[this.pageIdx] || [])); + this.renderMatches(this.matches); + } + }; + return TextLayerBuilder; +})(); + +/** + * @constructor + * @implements IPDFTextLayerFactory + */ +function DefaultTextLayerFactory() {} +DefaultTextLayerFactory.prototype = { + /** + * @param {HTMLDivElement} textLayerDiv + * @param {number} pageIndex + * @param {PageViewport} viewport + * @returns {TextLayerBuilder} + */ + createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { + return new TextLayerBuilder({ + textLayerDiv: textLayerDiv, + pageIndex: pageIndex, + viewport: viewport + }); + } +}; + + +/** + * @typedef {Object} AnnotationsLayerBuilderOptions + * @property {HTMLDivElement} pageDiv + * @property {PDFPage} pdfPage + * @property {IPDFLinkService} linkService + */ + +/** + * @class + */ +var AnnotationsLayerBuilder = (function AnnotationsLayerBuilderClosure() { + /** + * @param {AnnotationsLayerBuilderOptions} options + * @constructs AnnotationsLayerBuilder + */ + function AnnotationsLayerBuilder(options) { + this.pageDiv = options.pageDiv; + this.pdfPage = options.pdfPage; + this.linkService = options.linkService; + + this.div = null; + } + AnnotationsLayerBuilder.prototype = + /** @lends AnnotationsLayerBuilder.prototype */ { + + /** + * @param {PageViewport} viewport + */ + setupAnnotations: + function AnnotationsLayerBuilder_setupAnnotations(viewport) { + function bindLink(link, dest) { + link.href = linkService.getDestinationHash(dest); + link.onclick = function annotationsLayerBuilderLinksOnclick() { + if (dest) { + linkService.navigateTo(dest); + } + return false; + }; + if (dest) { + link.className = 'internalLink'; + } + } + + function bindNamedAction(link, action) { + link.href = linkService.getAnchorUrl(''); + link.onclick = function annotationsLayerBuilderNamedActionOnClick() { + linkService.executeNamedAction(action); + return false; + }; + link.className = 'internalLink'; + } + + var linkService = this.linkService; + var pdfPage = this.pdfPage; + var self = this; + + pdfPage.getAnnotations().then(function (annotationsData) { + viewport = viewport.clone({ dontFlip: true }); + var transform = viewport.transform; + var transformStr = 'matrix(' + transform.join(',') + ')'; + var data, element, i, ii; + + if (self.div) { + // If an annotationLayer already exists, refresh its children's + // transformation matrices + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + element = self.div.querySelector( + '[data-annotation-id="' + data.id + '"]'); + if (element) { + CustomStyle.setProp('transform', element, transformStr); + } + } + // See PDFPageView.reset() + self.div.removeAttribute('hidden'); + } else { + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + if (!data || !data.hasHtml) { + continue; + } + + element = PDFJS.AnnotationUtils.getHtmlElement(data, + pdfPage.commonObjs); + element.setAttribute('data-annotation-id', data.id); + if (typeof mozL10n !== 'undefined') { + mozL10n.translate(element); + } + + var rect = data.rect; + var view = pdfPage.view; + rect = PDFJS.Util.normalizeRect([ + rect[0], + view[3] - rect[1] + view[1], + rect[2], + view[3] - rect[3] + view[1] + ]); + element.style.left = rect[0] + 'px'; + element.style.top = rect[1] + 'px'; + element.style.position = 'absolute'; + + CustomStyle.setProp('transform', element, transformStr); + var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; + CustomStyle.setProp('transformOrigin', element, transformOriginStr); + + if (data.subtype === 'Link' && !data.url) { + var link = element.getElementsByTagName('a')[0]; + if (link) { + if (data.action) { + bindNamedAction(link, data.action); + } else { + bindLink(link, ('dest' in data) ? data.dest : null); + } + } + } + + if (!self.div) { + var annotationLayerDiv = document.createElement('div'); + annotationLayerDiv.className = 'annotationLayer'; + self.pageDiv.appendChild(annotationLayerDiv); + self.div = annotationLayerDiv; + } + + self.div.appendChild(element); + } + } + }); + }, + + hide: function () { + if (!this.div) { + return; + } + this.div.setAttribute('hidden', 'true'); + } + }; + return AnnotationsLayerBuilder; +})(); + +/** + * @constructor + * @implements IPDFAnnotationsLayerFactory + */ +function DefaultAnnotationsLayerFactory() {} +DefaultAnnotationsLayerFactory.prototype = { + /** + * @param {HTMLDivElement} pageDiv + * @param {PDFPage} pdfPage + * @returns {AnnotationsLayerBuilder} + */ + createAnnotationsLayerBuilder: function (pageDiv, pdfPage) { + return new AnnotationsLayerBuilder({ + pageDiv: pageDiv, + pdfPage: pdfPage + }); + } +}; + + +/** + * @typedef {Object} PDFViewerOptions + * @property {HTMLDivElement} container - The container for the viewer element. + * @property {HTMLDivElement} viewer - (optional) The viewer element. + * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {PDFRenderingQueue} renderingQueue - (optional) The rendering + * queue object. + * @property {boolean} removePageBorders - (optional) Removes the border shadow + * around the pages. The default is false. + */ + +/** + * Simple viewer control to display PDF content/pages. + * @class + * @implements {IRenderableView} + */ +var PDFViewer = (function pdfViewer() { + function PDFPageViewBuffer(size) { + var data = []; + this.push = function cachePush(view) { + var i = data.indexOf(view); + if (i >= 0) { + data.splice(i, 1); + } + data.push(view); + if (data.length > size) { + data.shift().destroy(); + } + }; + this.resize = function (newSize) { + size = newSize; + while (data.length > size) { + data.shift().destroy(); + } + }; + } + + /** + * @constructs PDFViewer + * @param {PDFViewerOptions} options + */ + function PDFViewer(options) { + this.container = options.container; + this.viewer = options.viewer || options.container.firstElementChild; + this.linkService = options.linkService || new SimpleLinkService(this); + this.removePageBorders = options.removePageBorders || false; + + this.defaultRenderingQueue = !options.renderingQueue; + if (this.defaultRenderingQueue) { + // Custom rendering queue is not specified, using default one + this.renderingQueue = new PDFRenderingQueue(); + this.renderingQueue.setViewer(this); + } else { + this.renderingQueue = options.renderingQueue; + } + + this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this)); + this.updateInProgress = false; + this.presentationModeState = PresentationModeState.UNKNOWN; + this._resetView(); + + if (this.removePageBorders) { + this.viewer.classList.add('removePageBorders'); + } + } + + PDFViewer.prototype = /** @lends PDFViewer.prototype */{ + get pagesCount() { + return this._pages.length; + }, + + getPageView: function (index) { + return this._pages[index]; + }, + + get currentPageNumber() { + return this._currentPageNumber; + }, + + set currentPageNumber(val) { + if (!this.pdfDocument) { + this._currentPageNumber = val; + return; + } + + var event = document.createEvent('UIEvents'); + event.initUIEvent('pagechange', true, true, window, 0); + event.updateInProgress = this.updateInProgress; + + if (!(0 < val && val <= this.pagesCount)) { + event.pageNumber = this._currentPageNumber; + event.previousPageNumber = val; + this.container.dispatchEvent(event); + return; + } + + event.previousPageNumber = this._currentPageNumber; + this._currentPageNumber = val; + event.pageNumber = val; + this.container.dispatchEvent(event); + }, + + /** + * @returns {number} + */ + get currentScale() { + return this._currentScale; + }, + + /** + * @param {number} val - Scale of the pages in percents. + */ + set currentScale(val) { + if (isNaN(val)) { + throw new Error('Invalid numeric scale'); + } + if (!this.pdfDocument) { + this._currentScale = val; + this._currentScaleValue = val.toString(); + return; + } + this._setScale(val, false); + }, + + /** + * @returns {string} + */ + get currentScaleValue() { + return this._currentScaleValue; + }, + + /** + * @param val - The scale of the pages (in percent or predefined value). + */ + set currentScaleValue(val) { + if (!this.pdfDocument) { + this._currentScale = isNaN(val) ? UNKNOWN_SCALE : val; + this._currentScaleValue = val; + return; + } + this._setScale(val, false); + }, + + /** + * @returns {number} + */ + get pagesRotation() { + return this._pagesRotation; + }, + + /** + * @param {number} rotation - The rotation of the pages (0, 90, 180, 270). + */ + set pagesRotation(rotation) { + this._pagesRotation = rotation; + + for (var i = 0, l = this._pages.length; i < l; i++) { + var pageView = this._pages[i]; + pageView.update(pageView.scale, rotation); + } + + this._setScale(this._currentScaleValue, true); + }, + + /** + * @param pdfDocument {PDFDocument} + */ + setDocument: function (pdfDocument) { + if (this.pdfDocument) { + this._resetView(); + } + + this.pdfDocument = pdfDocument; + if (!pdfDocument) { + return; + } + + var pagesCount = pdfDocument.numPages; + var pagesRefMap = this.pagesRefMap = {}; + var self = this; + + var resolvePagesPromise; + var pagesPromise = new Promise(function (resolve) { + resolvePagesPromise = resolve; + }); + this.pagesPromise = pagesPromise; + pagesPromise.then(function () { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagesloaded', true, true, { + pagesCount: pagesCount + }); + self.container.dispatchEvent(event); + }); + + var isOnePageRenderedResolved = false; + var resolveOnePageRendered = null; + var onePageRendered = new Promise(function (resolve) { + resolveOnePageRendered = resolve; + }); + this.onePageRendered = onePageRendered; + + var bindOnAfterAndBeforeDraw = function (pageView) { + pageView.onBeforeDraw = function pdfViewLoadOnBeforeDraw() { + // Add the page to the buffer at the start of drawing. That way it can + // be evicted from the buffer and destroyed even if we pause its + // rendering. + self._buffer.push(this); + }; + // when page is painted, using the image as thumbnail base + pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { + if (!isOnePageRenderedResolved) { + isOnePageRenderedResolved = true; + resolveOnePageRendered(); + } + }; + }; + + var firstPagePromise = pdfDocument.getPage(1); + this.firstPagePromise = firstPagePromise; + + // Fetch a single page so we can get a viewport that will be the default + // viewport for all pages + return firstPagePromise.then(function(pdfPage) { + var scale = this._currentScale || 1.0; + var viewport = pdfPage.getViewport(scale * CSS_UNITS); + for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { + var textLayerFactory = null; + if (!PDFJS.disableTextLayer) { + textLayerFactory = this; + } + var pageView = new PDFPageView({ + container: this.viewer, + id: pageNum, + scale: scale, + defaultViewport: viewport.clone(), + renderingQueue: this.renderingQueue, + textLayerFactory: textLayerFactory, + annotationsLayerFactory: this + }); + bindOnAfterAndBeforeDraw(pageView); + this._pages.push(pageView); + } + + // Fetch all the pages since the viewport is needed before printing + // starts to create the correct size canvas. Wait until one page is + // rendered so we don't tie up too many resources early on. + onePageRendered.then(function () { + if (!PDFJS.disableAutoFetch) { + var getPagesLeft = pagesCount; + for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { + pdfDocument.getPage(pageNum).then(function (pageNum, pdfPage) { + var pageView = self._pages[pageNum - 1]; + if (!pageView.pdfPage) { + pageView.setPdfPage(pdfPage); + } + var refStr = pdfPage.ref.num + ' ' + pdfPage.ref.gen + ' R'; + pagesRefMap[refStr] = pageNum; + getPagesLeft--; + if (!getPagesLeft) { + resolvePagesPromise(); + } + }.bind(null, pageNum)); + } + } else { + // XXX: Printing is semi-broken with auto fetch disabled. + resolvePagesPromise(); + } + }); + + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagesinit', true, true, null); + self.container.dispatchEvent(event); + + if (this.defaultRenderingQueue) { + this.update(); + } + + if (this.findController) { + this.findController.resolveFirstPage(); + } + }.bind(this)); + }, + + _resetView: function () { + this._pages = []; + this._currentPageNumber = 1; + this._currentScale = UNKNOWN_SCALE; + this._currentScaleValue = null; + this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); + this._location = null; + this._pagesRotation = 0; + this._pagesRequests = []; + + var container = this.viewer; + while (container.hasChildNodes()) { + container.removeChild(container.lastChild); + } + }, + + _scrollUpdate: function () { + if (this.pagesCount === 0) { + return; + } + this.update(); + for (var i = 0, ii = this._pages.length; i < ii; i++) { + this._pages[i].updatePosition(); + } + }, + + _setScaleDispatchEvent: function pdfViewer_setScaleDispatchEvent( + newScale, newValue, preset) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('scalechange', true, true, window, 0); + event.scale = newScale; + if (preset) { + event.presetValue = newValue; + } + this.container.dispatchEvent(event); + }, + + _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages( + newScale, newValue, noScroll, preset) { + this._currentScaleValue = newValue; + if (newScale === this._currentScale) { + if (preset) { + this._setScaleDispatchEvent(newScale, newValue, true); + } + return; + } + + for (var i = 0, ii = this._pages.length; i < ii; i++) { + this._pages[i].update(newScale); + } + this._currentScale = newScale; + + if (!noScroll) { + var page = this._currentPageNumber, dest; + if (this._location && !IGNORE_CURRENT_POSITION_ON_ZOOM && + !(this.isInPresentationMode || this.isChangingPresentationMode)) { + page = this._location.pageNumber; + dest = [null, { name: 'XYZ' }, this._location.left, + this._location.top, null]; + } + this.scrollPageIntoView(page, dest); + } + + this._setScaleDispatchEvent(newScale, newValue, preset); + }, + + _setScale: function pdfViewer_setScale(value, noScroll) { + if (value === 'custom') { + return; + } + var scale = parseFloat(value); + + if (scale > 0) { + this._setScaleUpdatePages(scale, value, noScroll, false); + } else { + var currentPage = this._pages[this._currentPageNumber - 1]; + if (!currentPage) { + return; + } + var hPadding = (this.isInPresentationMode || this.removePageBorders) ? + 0 : SCROLLBAR_PADDING; + var vPadding = (this.isInPresentationMode || this.removePageBorders) ? + 0 : VERTICAL_PADDING; + var pageWidthScale = (this.container.clientWidth - hPadding) / + currentPage.width * currentPage.scale; + var pageHeightScale = (this.container.clientHeight - vPadding) / + currentPage.height * currentPage.scale; + switch (value) { + case 'page-actual': + scale = 1; + break; + case 'page-width': + scale = pageWidthScale; + break; + case 'page-height': + scale = pageHeightScale; + break; + case 'page-fit': + scale = Math.min(pageWidthScale, pageHeightScale); + break; + case 'auto': + var isLandscape = (currentPage.width > currentPage.height); + // For pages in landscape mode, fit the page height to the viewer + // *unless* the page would thus become too wide to fit horizontally. + var horizontalScale = isLandscape ? + Math.min(pageHeightScale, pageWidthScale) : pageWidthScale; + scale = Math.min(MAX_AUTO_SCALE, horizontalScale); + break; + default: + console.error('pdfViewSetScale: \'' + value + + '\' is an unknown zoom value.'); + return; + } + this._setScaleUpdatePages(scale, value, noScroll, true); + } + }, + + /** + * Scrolls page into view. + * @param {number} pageNumber + * @param {Array} dest - (optional) original PDF destination array: + * + */ + scrollPageIntoView: function PDFViewer_scrollPageIntoView(pageNumber, + dest) { + var pageView = this._pages[pageNumber - 1]; + + if (this.isInPresentationMode) { + if (this.linkService.page !== pageView.id) { + // Avoid breaking getVisiblePages in presentation mode. + this.linkService.page = pageView.id; + return; + } + dest = null; + // Fixes the case when PDF has different page sizes. + this._setScale(this.currentScaleValue, true); + } + if (!dest) { + scrollIntoView(pageView.div); + return; + } + + var x = 0, y = 0; + var width = 0, height = 0, widthScale, heightScale; + var changeOrientation = (pageView.rotation % 180 === 0 ? false : true); + var pageWidth = (changeOrientation ? pageView.height : pageView.width) / + pageView.scale / CSS_UNITS; + var pageHeight = (changeOrientation ? pageView.width : pageView.height) / + pageView.scale / CSS_UNITS; + var scale = 0; + switch (dest[1].name) { + case 'XYZ': + x = dest[2]; + y = dest[3]; + scale = dest[4]; + // If x and/or y coordinates are not supplied, default to + // _top_ left of the page (not the obvious bottom left, + // since aligning the bottom of the intended page with the + // top of the window is rarely helpful). + x = x !== null ? x : 0; + y = y !== null ? y : pageHeight; + break; + case 'Fit': + case 'FitB': + scale = 'page-fit'; + break; + case 'FitH': + case 'FitBH': + y = dest[2]; + scale = 'page-width'; + break; + case 'FitV': + case 'FitBV': + x = dest[2]; + width = pageWidth; + height = pageHeight; + scale = 'page-height'; + break; + case 'FitR': + x = dest[2]; + y = dest[3]; + width = dest[4] - x; + height = dest[5] - y; + var viewerContainer = this.container; + var hPadding = this.removePageBorders ? 0 : SCROLLBAR_PADDING; + var vPadding = this.removePageBorders ? 0 : VERTICAL_PADDING; + + widthScale = (viewerContainer.clientWidth - hPadding) / + width / CSS_UNITS; + heightScale = (viewerContainer.clientHeight - vPadding) / + height / CSS_UNITS; + scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); + break; + default: + return; + } + + if (scale && scale !== this.currentScale) { + this.currentScaleValue = scale; + } else if (this.currentScale === UNKNOWN_SCALE) { + this.currentScaleValue = DEFAULT_SCALE; + } + + if (scale === 'page-fit' && !dest[4]) { + scrollIntoView(pageView.div); + return; + } + + var boundingRect = [ + pageView.viewport.convertToViewportPoint(x, y), + pageView.viewport.convertToViewportPoint(x + width, y + height) + ]; + var left = Math.min(boundingRect[0][0], boundingRect[1][0]); + var top = Math.min(boundingRect[0][1], boundingRect[1][1]); + + scrollIntoView(pageView.div, { left: left, top: top }); + }, + + _updateLocation: function (firstPage) { + var currentScale = this._currentScale; + var currentScaleValue = this._currentScaleValue; + var normalizedScaleValue = + parseFloat(currentScaleValue) === currentScale ? + Math.round(currentScale * 10000) / 100 : currentScaleValue; + + var pageNumber = firstPage.id; + var pdfOpenParams = '#page=' + pageNumber; + pdfOpenParams += '&zoom=' + normalizedScaleValue; + var currentPageView = this._pages[pageNumber - 1]; + var container = this.container; + var topLeft = currentPageView.getPagePoint( + (container.scrollLeft - firstPage.x), + (container.scrollTop - firstPage.y)); + var intLeft = Math.round(topLeft[0]); + var intTop = Math.round(topLeft[1]); + pdfOpenParams += ',' + intLeft + ',' + intTop; + + this._location = { + pageNumber: pageNumber, + scale: normalizedScaleValue, + top: intTop, + left: intLeft, + pdfOpenParams: pdfOpenParams + }; + }, + + update: function () { + var visible = this._getVisiblePages(); + var visiblePages = visible.views; + if (visiblePages.length === 0) { + return; + } + + this.updateInProgress = true; + + var suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE, + 2 * visiblePages.length + 1); + this._buffer.resize(suggestedCacheSize); + + this.renderingQueue.renderHighestPriority(visible); + + var currentId = this.currentPageNumber; + var firstPage = visible.first; + + for (var i = 0, ii = visiblePages.length, stillFullyVisible = false; + i < ii; ++i) { + var page = visiblePages[i]; + + if (page.percent < 100) { + break; + } + if (page.id === currentId) { + stillFullyVisible = true; + break; + } + } + + if (!stillFullyVisible) { + currentId = visiblePages[0].id; + } + + if (!this.isInPresentationMode) { + this.currentPageNumber = currentId; + } + + this._updateLocation(firstPage); + + this.updateInProgress = false; + + var event = document.createEvent('UIEvents'); + event.initUIEvent('updateviewarea', true, true, window, 0); + event.location = this._location; + this.container.dispatchEvent(event); + }, + + containsElement: function (element) { + return this.container.contains(element); + }, + + focus: function () { + this.container.focus(); + }, + + get isInPresentationMode() { + return this.presentationModeState === PresentationModeState.FULLSCREEN; + }, + + get isChangingPresentationMode() { + return this.PresentationModeState === PresentationModeState.CHANGING; + }, + + get isHorizontalScrollbarEnabled() { + return (this.isInPresentationMode ? + false : (this.container.scrollWidth > this.container.clientWidth)); + }, + + _getVisiblePages: function () { + if (!this.isInPresentationMode) { + return getVisibleElements(this.container, this._pages, true); + } else { + // The algorithm in getVisibleElements doesn't work in all browsers and + // configurations when presentation mode is active. + var visible = []; + var currentPage = this._pages[this._currentPageNumber - 1]; + visible.push({ id: currentPage.id, view: currentPage }); + return { first: currentPage, last: currentPage, views: visible }; + } + }, + + cleanup: function () { + for (var i = 0, ii = this._pages.length; i < ii; i++) { + if (this._pages[i] && + this._pages[i].renderingState !== RenderingStates.FINISHED) { + this._pages[i].reset(); + } + } + }, + + /** + * @param {PDFPageView} pageView + * @returns {PDFPage} + * @private + */ + _ensurePdfPageLoaded: function (pageView) { + if (pageView.pdfPage) { + return Promise.resolve(pageView.pdfPage); + } + var pageNumber = pageView.id; + if (this._pagesRequests[pageNumber]) { + return this._pagesRequests[pageNumber]; + } + var promise = this.pdfDocument.getPage(pageNumber).then( + function (pdfPage) { + pageView.setPdfPage(pdfPage); + this._pagesRequests[pageNumber] = null; + return pdfPage; + }.bind(this)); + this._pagesRequests[pageNumber] = promise; + return promise; + }, + + forceRendering: function (currentlyVisiblePages) { + var visiblePages = currentlyVisiblePages || this._getVisiblePages(); + var pageView = this.renderingQueue.getHighestPriority(visiblePages, + this._pages, + this.scroll.down); + if (pageView) { + this._ensurePdfPageLoaded(pageView).then(function () { + this.renderingQueue.renderView(pageView); + }.bind(this)); + return true; + } + return false; + }, + + getPageTextContent: function (pageIndex) { + return this.pdfDocument.getPage(pageIndex + 1).then(function (page) { + return page.getTextContent(); + }); + }, + + /** + * @param {HTMLDivElement} textLayerDiv + * @param {number} pageIndex + * @param {PageViewport} viewport + * @returns {TextLayerBuilder} + */ + createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { + return new TextLayerBuilder({ + textLayerDiv: textLayerDiv, + pageIndex: pageIndex, + viewport: viewport, + findController: this.isInPresentationMode ? null : this.findController + }); + }, + + /** + * @param {HTMLDivElement} pageDiv + * @param {PDFPage} pdfPage + * @returns {AnnotationsLayerBuilder} + */ + createAnnotationsLayerBuilder: function (pageDiv, pdfPage) { + return new AnnotationsLayerBuilder({ + pageDiv: pageDiv, + pdfPage: pdfPage, + linkService: this.linkService + }); + }, + + setFindController: function (findController) { + this.findController = findController; + }, + }; + + return PDFViewer; +})(); + +var SimpleLinkService = (function SimpleLinkServiceClosure() { + function SimpleLinkService(pdfViewer) { + this.pdfViewer = pdfViewer; + } + SimpleLinkService.prototype = { + /** + * @returns {number} + */ + get page() { + return this.pdfViewer.currentPageNumber; + }, + /** + * @param {number} value + */ + set page(value) { + this.pdfViewer.currentPageNumber = value; + }, + /** + * @param dest - The PDF destination object. + */ + navigateTo: function (dest) {}, + /** + * @param dest - The PDF destination object. + * @returns {string} The hyperlink to the PDF object. + */ + getDestinationHash: function (dest) { + return '#'; + }, + /** + * @param hash - The PDF parameters/hash. + * @returns {string} The hyperlink to the PDF object. + */ + getAnchorUrl: function (hash) { + return '#'; + }, + /** + * @param {string} hash + */ + setHash: function (hash) {}, + /** + * @param {string} action + */ + executeNamedAction: function (action) {}, + }; + return SimpleLinkService; +})(); + + +var THUMBNAIL_SCROLL_MARGIN = -19; + + +var THUMBNAIL_WIDTH = 98; // px +var THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px + +/** + * @typedef {Object} PDFThumbnailViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {number} id - The thumbnail's unique ID (normally its number). + * @property {PageViewport} defaultViewport - The page viewport. + * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. + */ + +/** + * @class + * @implements {IRenderableView} + */ +var PDFThumbnailView = (function PDFThumbnailViewClosure() { + function getTempCanvas(width, height) { + var tempCanvas = PDFThumbnailView.tempImageCache; + if (!tempCanvas) { + tempCanvas = document.createElement('canvas'); + PDFThumbnailView.tempImageCache = tempCanvas; + } + tempCanvas.width = width; + tempCanvas.height = height; + + // Since this is a temporary canvas, we need to fill the canvas with a white + // background ourselves. |_getPageDrawContext| uses CSS rules for this. + var ctx = tempCanvas.getContext('2d'); + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + return tempCanvas; + } + + /** + * @constructs PDFThumbnailView + * @param {PDFThumbnailViewOptions} options + */ + function PDFThumbnailView(options) { + var container = options.container; + var id = options.id; + var defaultViewport = options.defaultViewport; + var linkService = options.linkService; + var renderingQueue = options.renderingQueue; + + this.id = id; + this.renderingId = 'thumbnail' + id; + + this.pdfPage = null; + this.rotation = 0; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + + this.linkService = linkService; + this.renderingQueue = renderingQueue; + + this.hasImage = false; + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + this.pageWidth = this.viewport.width; + this.pageHeight = this.viewport.height; + this.pageRatio = this.pageWidth / this.pageHeight; + + this.canvasWidth = THUMBNAIL_WIDTH; + this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; + this.scale = this.canvasWidth / this.pageWidth; + + var anchor = document.createElement('a'); + anchor.href = linkService.getAnchorUrl('#page=' + id); + anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); + anchor.onclick = function stopNavigation() { + linkService.page = id; + return false; + }; + + var div = document.createElement('div'); + div.id = 'thumbnailContainer' + id; + div.className = 'thumbnail'; + this.div = div; + + if (id === 1) { + // Highlight the thumbnail of the first page when no page number is + // specified (or exists in cache) when the document is loaded. + div.classList.add('selected'); + } + + var ring = document.createElement('div'); + ring.className = 'thumbnailSelectionRing'; + var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; + ring.style.width = this.canvasWidth + borderAdjustment + 'px'; + ring.style.height = this.canvasHeight + borderAdjustment + 'px'; + this.ring = ring; + + div.appendChild(ring); + anchor.appendChild(div); + container.appendChild(anchor); + } + + PDFThumbnailView.prototype = { + setPdfPage: function PDFThumbnailView_setPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport(1, totalRotation); + this.reset(); + }, + + reset: function PDFThumbnailView_reset() { + if (this.renderTask) { + this.renderTask.cancel(); + } + this.hasImage = false; + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + this.pageWidth = this.viewport.width; + this.pageHeight = this.viewport.height; + this.pageRatio = this.pageWidth / this.pageHeight; + + this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; + this.scale = (this.canvasWidth / this.pageWidth); + + this.div.removeAttribute('data-loaded'); + var ring = this.ring; + var childNodes = ring.childNodes; + for (var i = childNodes.length - 1; i >= 0; i--) { + ring.removeChild(childNodes[i]); + } + var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; + ring.style.width = this.canvasWidth + borderAdjustment + 'px'; + ring.style.height = this.canvasHeight + borderAdjustment + 'px'; + + if (this.canvas) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + this.canvas.width = 0; + this.canvas.height = 0; + delete this.canvas; + } + }, + + update: function PDFThumbnailView_update(rotation) { + if (typeof rotation !== 'undefined') { + this.rotation = rotation; + } + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: 1, + rotation: totalRotation + }); + this.reset(); + }, + + /** + * @private + */ + _getPageDrawContext: + function PDFThumbnailView_getPageDrawContext(noCtxScale) { + var canvas = document.createElement('canvas'); + canvas.id = this.renderingId; + + canvas.className = 'thumbnailImage'; + canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', + {page: this.id}, 'Thumbnail of Page {{page}}')); + + this.canvas = canvas; + this.div.setAttribute('data-loaded', true); + this.ring.appendChild(canvas); + + var ctx = canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + canvas.width = (this.canvasWidth * outputScale.sx) | 0; + canvas.height = (this.canvasHeight * outputScale.sy) | 0; + canvas.style.width = this.canvasWidth + 'px'; + canvas.style.height = this.canvasHeight + 'px'; + if (!noCtxScale && outputScale.scaled) { + ctx.scale(outputScale.sx, outputScale.sy); + } + return ctx; + }, + + draw: function PDFThumbnailView_draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } + if (this.hasImage) { + return Promise.resolve(undefined); + } + this.hasImage = true; + this.renderingState = RenderingStates.RUNNING; + + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; + }); + + var self = this; + function thumbnailDrawCallback(error) { + // The renderTask may have been replaced by a new one, so only remove + // the reference to the renderTask if it matches the one that is + // triggering this callback. + if (renderTask === self.renderTask) { + self.renderTask = null; + } + if (error === 'cancelled') { + rejectRenderPromise(error); + return; + } + self.renderingState = RenderingStates.FINISHED; + + if (!error) { + resolveRenderPromise(undefined); + } else { + rejectRenderPromise(error); + } + } + + var ctx = this._getPageDrawContext(); + var drawViewport = this.viewport.clone({ scale: this.scale }); + var renderContinueCallback = function renderContinueCallback(cont) { + if (!self.renderingQueue.isHighestPriority(self)) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + + var renderContext = { + canvasContext: ctx, + viewport: drawViewport, + continueCallback: renderContinueCallback + }; + var renderTask = this.renderTask = this.pdfPage.render(renderContext); + + renderTask.promise.then( + function pdfPageRenderCallback() { + thumbnailDrawCallback(null); + }, + function pdfPageRenderError(error) { + thumbnailDrawCallback(error); + } + ); + return promise; + }, + + setImage: function PDFThumbnailView_setImage(pageView) { + var img = pageView.canvas; + if (this.hasImage || !img) { + return; + } + if (!this.pdfPage) { + this.setPdfPage(pageView.pdfPage); + } + this.hasImage = true; + this.renderingState = RenderingStates.FINISHED; + + var ctx = this._getPageDrawContext(true); + var canvas = ctx.canvas; + + if (img.width <= 2 * canvas.width) { + ctx.drawImage(img, 0, 0, img.width, img.height, + 0, 0, canvas.width, canvas.height); + return; + } + // drawImage does an awful job of rescaling the image, doing it gradually. + var MAX_NUM_SCALING_STEPS = 3; + var reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; + var reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; + var reducedImage = getTempCanvas(reducedWidth, reducedHeight); + var reducedImageCtx = reducedImage.getContext('2d'); + + while (reducedWidth > img.width || reducedHeight > img.height) { + reducedWidth >>= 1; + reducedHeight >>= 1; + } + reducedImageCtx.drawImage(img, 0, 0, img.width, img.height, + 0, 0, reducedWidth, reducedHeight); + while (reducedWidth > 2 * canvas.width) { + reducedImageCtx.drawImage(reducedImage, + 0, 0, reducedWidth, reducedHeight, + 0, 0, reducedWidth >> 1, reducedHeight >> 1); + reducedWidth >>= 1; + reducedHeight >>= 1; + } + ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight, + 0, 0, canvas.width, canvas.height); + } + }; + + return PDFThumbnailView; +})(); + +PDFThumbnailView.tempImageCache = null; + + +/** + * @typedef {Object} PDFThumbnailViewerOptions + * @property {HTMLDivElement} container - The container for the thumbnail + * elements. + * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. + */ + +/** + * Simple viewer control to display thumbnails for pages. + * @class + * @implements {IRenderableView} + */ +var PDFThumbnailViewer = (function PDFThumbnailViewerClosure() { + /** + * @constructs PDFThumbnailViewer + * @param {PDFThumbnailViewerOptions} options + */ + function PDFThumbnailViewer(options) { + this.container = options.container; + this.renderingQueue = options.renderingQueue; + this.linkService = options.linkService; + + this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this)); + this._resetView(); + } + + PDFThumbnailViewer.prototype = { + /** + * @private + */ + _scrollUpdated: function PDFThumbnailViewer_scrollUpdated() { + this.renderingQueue.renderHighestPriority(); + }, + + getThumbnail: function PDFThumbnailViewer_getThumbnail(index) { + return this.thumbnails[index]; + }, + + /** + * @private + */ + _getVisibleThumbs: function PDFThumbnailViewer_getVisibleThumbs() { + return getVisibleElements(this.container, this.thumbnails); + }, + + scrollThumbnailIntoView: + function PDFThumbnailViewer_scrollThumbnailIntoView(page) { + var selected = document.querySelector('.thumbnail.selected'); + if (selected) { + selected.classList.remove('selected'); + } + var thumbnail = document.getElementById('thumbnailContainer' + page); + if (thumbnail) { + thumbnail.classList.add('selected'); + } + var visibleThumbs = this._getVisibleThumbs(); + var numVisibleThumbs = visibleThumbs.views.length; + + // If the thumbnail isn't currently visible, scroll it into view. + if (numVisibleThumbs > 0) { + var first = visibleThumbs.first.id; + // Account for only one thumbnail being visible. + var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first); + if (page <= first || page >= last) { + scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN }); + } + } + }, + + get pagesRotation() { + return this._pagesRotation; + }, + + set pagesRotation(rotation) { + this._pagesRotation = rotation; + for (var i = 0, l = this.thumbnails.length; i < l; i++) { + var thumb = this.thumbnails[i]; + thumb.update(rotation); + } + }, + + cleanup: function PDFThumbnailViewer_cleanup() { + var tempCanvas = PDFThumbnailView.tempImageCache; + if (tempCanvas) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + tempCanvas.width = 0; + tempCanvas.height = 0; + } + PDFThumbnailView.tempImageCache = null; + }, + + /** + * @private + */ + _resetView: function PDFThumbnailViewer_resetView() { + this.thumbnails = []; + this._pagesRotation = 0; + this._pagesRequests = []; + }, + + setDocument: function PDFThumbnailViewer_setDocument(pdfDocument) { + if (this.pdfDocument) { + // cleanup of the elements and views + var thumbsView = this.container; + while (thumbsView.hasChildNodes()) { + thumbsView.removeChild(thumbsView.lastChild); + } + this._resetView(); + } + + this.pdfDocument = pdfDocument; + if (!pdfDocument) { + return Promise.resolve(); + } + + return pdfDocument.getPage(1).then(function (firstPage) { + var pagesCount = pdfDocument.numPages; + var viewport = firstPage.getViewport(1.0); + for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { + var thumbnail = new PDFThumbnailView({ + container: this.container, + id: pageNum, + defaultViewport: viewport.clone(), + linkService: this.linkService, + renderingQueue: this.renderingQueue + }); + this.thumbnails.push(thumbnail); + } + }.bind(this)); + }, + + /** + * @param {PDFPageView} pageView + * @returns {PDFPage} + * @private + */ + _ensurePdfPageLoaded: + function PDFThumbnailViewer_ensurePdfPageLoaded(thumbView) { + if (thumbView.pdfPage) { + return Promise.resolve(thumbView.pdfPage); + } + var pageNumber = thumbView.id; + if (this._pagesRequests[pageNumber]) { + return this._pagesRequests[pageNumber]; + } + var promise = this.pdfDocument.getPage(pageNumber).then( + function (pdfPage) { + thumbView.setPdfPage(pdfPage); + this._pagesRequests[pageNumber] = null; + return pdfPage; + }.bind(this)); + this._pagesRequests[pageNumber] = promise; + return promise; + }, + + ensureThumbnailVisible: + function PDFThumbnailViewer_ensureThumbnailVisible(page) { + // Ensure that the thumbnail of the current page is visible + // when switching from another view. + scrollIntoView(document.getElementById('thumbnailContainer' + page)); + }, + + forceRendering: function () { + var visibleThumbs = this._getVisibleThumbs(); + var thumbView = this.renderingQueue.getHighestPriority(visibleThumbs, + this.thumbnails, + this.scroll.down); + if (thumbView) { + this._ensurePdfPageLoaded(thumbView).then(function () { + this.renderingQueue.renderView(thumbView); + }.bind(this)); + return true; + } + return false; + } + }; + + return PDFThumbnailViewer; +})(); + + +/** + * @typedef {Object} PDFOutlineViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {Array} outline - An array of outline objects. + * @property {IPDFLinkService} linkService - The navigation/linking service. + */ + +/** + * @class + */ +var PDFOutlineView = (function PDFOutlineViewClosure() { + /** + * @constructs PDFOutlineView + * @param {PDFOutlineViewOptions} options + */ + function PDFOutlineView(options) { + this.container = options.container; + this.outline = options.outline; + this.linkService = options.linkService; + } + + PDFOutlineView.prototype = { + reset: function PDFOutlineView_reset() { + var container = this.container; + while (container.firstChild) { + container.removeChild(container.firstChild); + } + }, + + /** + * @private + */ + _dispatchEvent: function PDFOutlineView_dispatchEvent(outlineCount) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('outlineloaded', true, true, { + outlineCount: outlineCount + }); + this.container.dispatchEvent(event); + }, + + /** + * @private + */ + _bindLink: function PDFOutlineView_bindLink(element, item) { + var linkService = this.linkService; + element.href = linkService.getDestinationHash(item.dest); + element.onclick = function goToDestination(e) { + linkService.navigateTo(item.dest); + return false; + }; + }, + + render: function PDFOutlineView_render() { + var outline = this.outline; + var outlineCount = 0; + + this.reset(); + + if (!outline) { + this._dispatchEvent(outlineCount); + return; + } + + var queue = [{ parent: this.container, items: this.outline }]; + while (queue.length > 0) { + var levelData = queue.shift(); + for (var i = 0, len = levelData.items.length; i < len; i++) { + var item = levelData.items[i]; + var div = document.createElement('div'); + div.className = 'outlineItem'; + var element = document.createElement('a'); + this._bindLink(element, item); + element.textContent = item.title; + div.appendChild(element); + + if (item.items.length > 0) { + var itemsDiv = document.createElement('div'); + itemsDiv.className = 'outlineItems'; + div.appendChild(itemsDiv); + queue.push({ parent: itemsDiv, items: item.items }); + } + + levelData.parent.appendChild(div); + outlineCount++; + } + } + + this._dispatchEvent(outlineCount); + } + }; + + return PDFOutlineView; +})(); + + +/** + * @typedef {Object} PDFAttachmentViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {Array} attachments - An array of attachment objects. + * @property {DownloadManager} downloadManager - The download manager. + */ + +/** + * @class + */ +var PDFAttachmentView = (function PDFAttachmentViewClosure() { + /** + * @constructs PDFAttachmentView + * @param {PDFAttachmentViewOptions} options + */ + function PDFAttachmentView(options) { + this.container = options.container; + this.attachments = options.attachments; + this.downloadManager = options.downloadManager; + } + + PDFAttachmentView.prototype = { + reset: function PDFAttachmentView_reset() { + var container = this.container; + while (container.firstChild) { + container.removeChild(container.firstChild); + } + }, + + /** + * @private + */ + _dispatchEvent: function PDFAttachmentView_dispatchEvent(attachmentsCount) { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('attachmentsloaded', true, true, { + attachmentsCount: attachmentsCount + }); + this.container.dispatchEvent(event); + }, + + /** + * @private + */ + _bindLink: function PDFAttachmentView_bindLink(button, content, filename) { + button.onclick = function downloadFile(e) { + this.downloadManager.downloadData(content, filename, ''); + return false; + }.bind(this); + }, + + render: function PDFAttachmentView_render() { + var attachments = this.attachments; + var attachmentsCount = 0; + + this.reset(); + + if (!attachments) { + this._dispatchEvent(attachmentsCount); + return; + } + + var names = Object.keys(attachments).sort(function(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }); + attachmentsCount = names.length; + + for (var i = 0; i < attachmentsCount; i++) { + var item = attachments[names[i]]; + var filename = getFileName(item.filename); + var div = document.createElement('div'); + div.className = 'attachmentsItem'; + var button = document.createElement('button'); + this._bindLink(button, item.content, filename); + button.textContent = filename; + div.appendChild(button); + this.container.appendChild(div); + } + + this._dispatchEvent(attachmentsCount); + } + }; + + return PDFAttachmentView; +})(); + + +var PDFViewerApplication = { + initialBookmark: document.location.hash.substring(1), + initialized: false, + fellback: false, + pdfDocument: null, + sidebarOpen: false, + printing: false, + /** @type {PDFViewer} */ + pdfViewer: null, + /** @type {PDFThumbnailViewer} */ + pdfThumbnailViewer: null, + /** @type {PDFRenderingQueue} */ + pdfRenderingQueue: null, + /** @type {PDFPresentationMode} */ + pdfPresentationMode: null, + /** @type {PDFDocumentProperties} */ + pdfDocumentProperties: null, + pageRotation: 0, + updateScaleControls: true, + isInitialViewSet: false, + animationStartedPromise: null, + preferenceSidebarViewOnLoad: SidebarView.NONE, + preferencePdfBugEnabled: false, + preferenceShowPreviousViewOnLoad: true, + preferenceDefaultZoomValue: '', + isViewerEmbedded: (window.parent !== window), + url: '', + + // called once when the document is loaded + initialize: function pdfViewInitialize() { + var pdfRenderingQueue = new PDFRenderingQueue(); + pdfRenderingQueue.onIdle = this.cleanup.bind(this); + this.pdfRenderingQueue = pdfRenderingQueue; + + var container = document.getElementById('viewerContainer'); + var viewer = document.getElementById('viewer'); + this.pdfViewer = new PDFViewer({ + container: container, + viewer: viewer, + renderingQueue: pdfRenderingQueue, + linkService: this + }); + pdfRenderingQueue.setViewer(this.pdfViewer); + + var thumbnailContainer = document.getElementById('thumbnailView'); + this.pdfThumbnailViewer = new PDFThumbnailViewer({ + container: thumbnailContainer, + renderingQueue: pdfRenderingQueue, + linkService: this + }); + pdfRenderingQueue.setThumbnailViewer(this.pdfThumbnailViewer); + + Preferences.initialize(); + + this.findController = new PDFFindController({ + pdfViewer: this.pdfViewer, + integratedFind: this.supportsIntegratedFind + }); + this.pdfViewer.setFindController(this.findController); + + this.findBar = new PDFFindBar({ + bar: document.getElementById('findbar'), + toggleButton: document.getElementById('viewFind'), + findField: document.getElementById('findInput'), + highlightAllCheckbox: document.getElementById('findHighlightAll'), + caseSensitiveCheckbox: document.getElementById('findMatchCase'), + findMsg: document.getElementById('findMsg'), + findStatusIcon: document.getElementById('findStatusIcon'), + findPreviousButton: document.getElementById('findPrevious'), + findNextButton: document.getElementById('findNext'), + findController: this.findController + }); + + this.findController.setFindBar(this.findBar); + + HandTool.initialize({ + container: container, + toggleHandTool: document.getElementById('toggleHandTool') + }); + + this.pdfDocumentProperties = new PDFDocumentProperties({ + overlayName: 'documentPropertiesOverlay', + closeButton: document.getElementById('documentPropertiesClose'), + fields: { + 'fileName': document.getElementById('fileNameField'), + 'fileSize': document.getElementById('fileSizeField'), + 'title': document.getElementById('titleField'), + 'author': document.getElementById('authorField'), + 'subject': document.getElementById('subjectField'), + 'keywords': document.getElementById('keywordsField'), + 'creationDate': document.getElementById('creationDateField'), + 'modificationDate': document.getElementById('modificationDateField'), + 'creator': document.getElementById('creatorField'), + 'producer': document.getElementById('producerField'), + 'version': document.getElementById('versionField'), + 'pageCount': document.getElementById('pageCountField') + } + }); + + SecondaryToolbar.initialize({ + toolbar: document.getElementById('secondaryToolbar'), + toggleButton: document.getElementById('secondaryToolbarToggle'), + presentationModeButton: + document.getElementById('secondaryPresentationMode'), + openFile: document.getElementById('secondaryOpenFile'), + print: document.getElementById('secondaryPrint'), + download: document.getElementById('secondaryDownload'), + viewBookmark: document.getElementById('secondaryViewBookmark'), + firstPage: document.getElementById('firstPage'), + lastPage: document.getElementById('lastPage'), + pageRotateCw: document.getElementById('pageRotateCw'), + pageRotateCcw: document.getElementById('pageRotateCcw'), + documentPropertiesButton: document.getElementById('documentProperties') + }); + + if (this.supportsFullscreen) { + var toolbar = SecondaryToolbar; + this.pdfPresentationMode = new PDFPresentationMode({ + container: container, + viewer: viewer, + pdfThumbnailViewer: this.pdfThumbnailViewer, + contextMenuItems: [ + { element: document.getElementById('contextFirstPage'), + handler: toolbar.firstPageClick.bind(toolbar) }, + { element: document.getElementById('contextLastPage'), + handler: toolbar.lastPageClick.bind(toolbar) }, + { element: document.getElementById('contextPageRotateCw'), + handler: toolbar.pageRotateCwClick.bind(toolbar) }, + { element: document.getElementById('contextPageRotateCcw'), + handler: toolbar.pageRotateCcwClick.bind(toolbar) } + ] + }); + } + + PasswordPrompt.initialize({ + overlayName: 'passwordOverlay', + passwordField: document.getElementById('password'), + passwordText: document.getElementById('passwordText'), + passwordSubmit: document.getElementById('passwordSubmit'), + passwordCancel: document.getElementById('passwordCancel') + }); + + var self = this; + var initializedPromise = Promise.all([ + Preferences.get('enableWebGL').then(function resolved(value) { + PDFJS.disableWebGL = !value; + }), + Preferences.get('sidebarViewOnLoad').then(function resolved(value) { + self.preferenceSidebarViewOnLoad = value; + }), + Preferences.get('pdfBugEnabled').then(function resolved(value) { + self.preferencePdfBugEnabled = value; + }), + Preferences.get('showPreviousViewOnLoad').then(function resolved(value) { + self.preferenceShowPreviousViewOnLoad = value; + }), + Preferences.get('defaultZoomValue').then(function resolved(value) { + self.preferenceDefaultZoomValue = value; + }), + Preferences.get('disableTextLayer').then(function resolved(value) { + if (PDFJS.disableTextLayer === true) { + return; + } + PDFJS.disableTextLayer = value; + }), + Preferences.get('disableRange').then(function resolved(value) { + if (PDFJS.disableRange === true) { + return; + } + PDFJS.disableRange = value; + }), + Preferences.get('disableAutoFetch').then(function resolved(value) { + PDFJS.disableAutoFetch = value; + }), + Preferences.get('disableFontFace').then(function resolved(value) { + if (PDFJS.disableFontFace === true) { + return; + } + PDFJS.disableFontFace = value; + }), + Preferences.get('useOnlyCssZoom').then(function resolved(value) { + PDFJS.useOnlyCssZoom = value; + }) + // TODO move more preferences and other async stuff here + ]).catch(function (reason) { }); + + return initializedPromise.then(function () { + PDFViewerApplication.initialized = true; + }); + }, + + zoomIn: function pdfViewZoomIn(ticks) { + var newScale = this.pdfViewer.currentScale; + do { + newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2); + newScale = Math.ceil(newScale * 10) / 10; + newScale = Math.min(MAX_SCALE, newScale); + } while (--ticks > 0 && newScale < MAX_SCALE); + this.setScale(newScale, true); + }, + + zoomOut: function pdfViewZoomOut(ticks) { + var newScale = this.pdfViewer.currentScale; + do { + newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2); + newScale = Math.floor(newScale * 10) / 10; + newScale = Math.max(MIN_SCALE, newScale); + } while (--ticks > 0 && newScale > MIN_SCALE); + this.setScale(newScale, true); + }, + + get currentScaleValue() { + return this.pdfViewer.currentScaleValue; + }, + + get pagesCount() { + return this.pdfDocument.numPages; + }, + + set page(val) { + this.pdfViewer.currentPageNumber = val; + }, + + get page() { + return this.pdfViewer.currentPageNumber; + }, + + get supportsPrinting() { + var canvas = document.createElement('canvas'); + var value = 'mozPrintCallback' in canvas; + + return PDFJS.shadow(this, 'supportsPrinting', value); + }, + + get supportsFullscreen() { + var doc = document.documentElement; + var support = !!(doc.requestFullscreen || doc.mozRequestFullScreen || + doc.webkitRequestFullScreen || doc.msRequestFullscreen); + + if (document.fullscreenEnabled === false || + document.mozFullScreenEnabled === false || + document.webkitFullscreenEnabled === false || + document.msFullscreenEnabled === false) { + support = false; + } + if (support && PDFJS.disableFullscreen === true) { + support = false; + } + + return PDFJS.shadow(this, 'supportsFullscreen', support); + }, + + get supportsIntegratedFind() { + var support = false; + + return PDFJS.shadow(this, 'supportsIntegratedFind', support); + }, + + get supportsDocumentFonts() { + var support = true; + + return PDFJS.shadow(this, 'supportsDocumentFonts', support); + }, + + get supportsDocumentColors() { + var support = true; + + return PDFJS.shadow(this, 'supportsDocumentColors', support); + }, + + get loadingBar() { + var bar = new ProgressBar('#loadingBar', {}); + + return PDFJS.shadow(this, 'loadingBar', bar); + }, + + + setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) { + this.url = url; + try { + this.setTitle(decodeURIComponent(getFileName(url)) || url); + } catch (e) { + // decodeURIComponent may throw URIError, + // fall back to using the unprocessed url in that case + this.setTitle(url); + } + }, + + setTitle: function pdfViewSetTitle(title) { + if (this.isViewerEmbedded) { + // Embedded PDF viewers should not be changing their parent page's title. + return; + } + //document.title = title; + }, + + close: function pdfViewClose() { + var errorWrapper = document.getElementById('errorWrapper'); + errorWrapper.setAttribute('hidden', 'true'); + + if (!this.pdfDocument) { + return; + } + + this.pdfDocument.destroy(); + this.pdfDocument = null; + + this.pdfThumbnailViewer.setDocument(null); + this.pdfViewer.setDocument(null); + + if (typeof PDFBug !== 'undefined') { + PDFBug.cleanup(); + } + }, + + // TODO(mack): This function signature should really be pdfViewOpen(url, args) + open: function pdfViewOpen(file, scale, password, + pdfDataRangeTransport, args) { + if (this.pdfDocument) { + // Reload the preferences if a document was previously opened. + Preferences.reload(); + } + this.close(); + + var parameters = {password: password}; + if (typeof file === 'string') { // URL + this.setTitleUsingUrl(file); + parameters.url = file; + } else if (file && 'byteLength' in file) { // ArrayBuffer + parameters.data = file; + } else if (file.url && file.originalUrl) { + this.setTitleUsingUrl(file.originalUrl); + parameters.url = file.url; + } + if (args) { + for (var prop in args) { + parameters[prop] = args[prop]; + } + } + + var self = this; + self.loading = true; + self.downloadComplete = false; + + var passwordNeeded = function passwordNeeded(updatePassword, reason) { + PasswordPrompt.updatePassword = updatePassword; + PasswordPrompt.reason = reason; + PasswordPrompt.open(); + }; + + function getDocumentProgress(progressData) { + self.progress(progressData.loaded / progressData.total); + } + + PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded, + getDocumentProgress).then( + function getDocumentCallback(pdfDocument) { + self.load(pdfDocument, scale); + self.loading = false; + }, + function getDocumentError(exception) { + var message = exception && exception.message; + var loadingErrorMessage = mozL10n.get('loading_error', null, + 'An error occurred while loading the PDF.'); + + if (exception instanceof PDFJS.InvalidPDFException) { + // change error message also for other builds + loadingErrorMessage = mozL10n.get('invalid_file_error', null, + 'Invalid or corrupted PDF file.'); + } else if (exception instanceof PDFJS.MissingPDFException) { + // special message for missing PDF's + loadingErrorMessage = mozL10n.get('missing_file_error', null, + 'Missing PDF file.'); + } else if (exception instanceof PDFJS.UnexpectedResponseException) { + loadingErrorMessage = mozL10n.get('unexpected_response_error', null, + 'Unexpected server response.'); + } + + var moreInfo = { + message: message + }; + self.error(loadingErrorMessage, moreInfo); + self.loading = false; + } + ); + + if (args && args.length) { + PDFViewerApplication.pdfDocumentProperties.setFileSize(args.length); + } + }, + + download: function pdfViewDownload() { + function downloadByUrl() { + downloadManager.downloadUrl(url, filename); + } + + var url = this.url.split('#')[0]; + var filename = getPDFFileNameFromURL(url); + var downloadManager = new DownloadManager(); + downloadManager.onerror = function (err) { + // This error won't really be helpful because it's likely the + // fallback won't work either (or is already open). + PDFViewerApplication.error('PDF failed to download.'); + }; + + if (!this.pdfDocument) { // the PDF is not ready yet + downloadByUrl(); + return; + } + + if (!this.downloadComplete) { // the PDF is still downloading + downloadByUrl(); + return; + } + + this.pdfDocument.getData().then( + function getDataSuccess(data) { + var blob = PDFJS.createBlob(data, 'application/pdf'); + downloadManager.download(blob, url, filename); + }, + downloadByUrl // Error occurred try downloading with just the url. + ).then(null, downloadByUrl); + }, + + fallback: function pdfViewFallback(featureId) { + }, + + navigateTo: function pdfViewNavigateTo(dest) { + var destString = ''; + var self = this; + + var goToDestination = function(destRef) { + self.pendingRefStr = null; + // dest array looks like that: + var pageNumber = destRef instanceof Object ? + self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : + (destRef + 1); + if (pageNumber) { + if (pageNumber > self.pagesCount) { + pageNumber = self.pagesCount; + } + self.pdfViewer.scrollPageIntoView(pageNumber, dest); + + // Update the browsing history. + PDFHistory.push({ dest: dest, hash: destString, page: pageNumber }); + } else { + self.pdfDocument.getPageIndex(destRef).then(function (pageIndex) { + var pageNum = pageIndex + 1; + self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] = pageNum; + goToDestination(destRef); + }); + } + }; + + var destinationPromise; + if (typeof dest === 'string') { + destString = dest; + destinationPromise = this.pdfDocument.getDestination(dest); + } else { + destinationPromise = Promise.resolve(dest); + } + destinationPromise.then(function(destination) { + dest = destination; + if (!(destination instanceof Array)) { + return; // invalid destination + } + goToDestination(destination[0]); + }); + }, + + executeNamedAction: function pdfViewExecuteNamedAction(action) { + // See PDF reference, table 8.45 - Named action + switch (action) { + case 'GoToPage': + document.getElementById('pageNumber').focus(); + break; + + case 'GoBack': + PDFHistory.back(); + break; + + case 'GoForward': + PDFHistory.forward(); + break; + + case 'Find': + if (!this.supportsIntegratedFind) { + this.findBar.toggle(); + } + break; + + case 'NextPage': + this.page++; + break; + + case 'PrevPage': + this.page--; + break; + + case 'LastPage': + this.page = this.pagesCount; + break; + + case 'FirstPage': + this.page = 1; + break; + + default: + break; // No action according to spec + } + }, + + getDestinationHash: function pdfViewGetDestinationHash(dest) { + if (typeof dest === 'string') { + return this.getAnchorUrl('#' + escape(dest)); + } + if (dest instanceof Array) { + var destRef = dest[0]; // see navigateTo method for dest format + var pageNumber = destRef instanceof Object ? + this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : + (destRef + 1); + if (pageNumber) { + var pdfOpenParams = this.getAnchorUrl('#page=' + pageNumber); + var destKind = dest[1]; + if (typeof destKind === 'object' && 'name' in destKind && + destKind.name === 'XYZ') { + var scale = (dest[4] || this.currentScaleValue); + var scaleNumber = parseFloat(scale); + if (scaleNumber) { + scale = scaleNumber * 100; + } + pdfOpenParams += '&zoom=' + scale; + if (dest[2] || dest[3]) { + pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0); + } + } + return pdfOpenParams; + } + } + return ''; + }, + + /** + * Prefix the full url on anchor links to make sure that links are resolved + * relative to the current URL instead of the one defined in . + * @param {String} anchor The anchor hash, including the #. + */ + getAnchorUrl: function getAnchorUrl(anchor) { + return anchor; + }, + + /** + * Show the error box. + * @param {String} message A message that is human readable. + * @param {Object} moreInfo (optional) Further information about the error + * that is more technical. Should have a 'message' + * and optionally a 'stack' property. + */ + error: function pdfViewError(message, moreInfo) { + var moreInfoText = mozL10n.get('error_version_info', + {version: PDFJS.version || '?', build: PDFJS.build || '?'}, + 'PDF.js v{{version}} (build: {{build}})') + '\n'; + if (moreInfo) { + moreInfoText += + mozL10n.get('error_message', {message: moreInfo.message}, + 'Message: {{message}}'); + if (moreInfo.stack) { + moreInfoText += '\n' + + mozL10n.get('error_stack', {stack: moreInfo.stack}, + 'Stack: {{stack}}'); + } else { + if (moreInfo.filename) { + moreInfoText += '\n' + + mozL10n.get('error_file', {file: moreInfo.filename}, + 'File: {{file}}'); + } + if (moreInfo.lineNumber) { + moreInfoText += '\n' + + mozL10n.get('error_line', {line: moreInfo.lineNumber}, + 'Line: {{line}}'); + } + } + } + + var errorWrapper = document.getElementById('errorWrapper'); + errorWrapper.removeAttribute('hidden'); + + var errorMessage = document.getElementById('errorMessage'); + errorMessage.textContent = message; + + var closeButton = document.getElementById('errorClose'); + closeButton.onclick = function() { + errorWrapper.setAttribute('hidden', 'true'); + }; + + var errorMoreInfo = document.getElementById('errorMoreInfo'); + var moreInfoButton = document.getElementById('errorShowMore'); + var lessInfoButton = document.getElementById('errorShowLess'); + moreInfoButton.onclick = function() { + errorMoreInfo.removeAttribute('hidden'); + moreInfoButton.setAttribute('hidden', 'true'); + lessInfoButton.removeAttribute('hidden'); + errorMoreInfo.style.height = errorMoreInfo.scrollHeight + 'px'; + }; + lessInfoButton.onclick = function() { + errorMoreInfo.setAttribute('hidden', 'true'); + moreInfoButton.removeAttribute('hidden'); + lessInfoButton.setAttribute('hidden', 'true'); + }; + moreInfoButton.oncontextmenu = noContextMenuHandler; + lessInfoButton.oncontextmenu = noContextMenuHandler; + closeButton.oncontextmenu = noContextMenuHandler; + moreInfoButton.removeAttribute('hidden'); + lessInfoButton.setAttribute('hidden', 'true'); + errorMoreInfo.value = moreInfoText; + }, + + progress: function pdfViewProgress(level) { + var percent = Math.round(level * 100); + // When we transition from full request to range requests, it's possible + // that we discard some of the loaded data. This can cause the loading + // bar to move backwards. So prevent this by only updating the bar if it + // increases. + if (percent > this.loadingBar.percent || isNaN(percent)) { + this.loadingBar.percent = percent; + + // When disableAutoFetch is enabled, it's not uncommon for the entire file + // to never be fetched (depends on e.g. the file structure). In this case + // the loading bar will not be completely filled, nor will it be hidden. + // To prevent displaying a partially filled loading bar permanently, we + // hide it when no data has been loaded during a certain amount of time. + if (PDFJS.disableAutoFetch && percent) { + if (this.disableAutoFetchLoadingBarTimeout) { + clearTimeout(this.disableAutoFetchLoadingBarTimeout); + this.disableAutoFetchLoadingBarTimeout = null; + } + this.loadingBar.show(); + + this.disableAutoFetchLoadingBarTimeout = setTimeout(function () { + this.loadingBar.hide(); + this.disableAutoFetchLoadingBarTimeout = null; + }.bind(this), DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT); + } + } + }, + + load: function pdfViewLoad(pdfDocument, scale) { + var self = this; + scale = scale || UNKNOWN_SCALE; + + this.findController.reset(); + + this.pdfDocument = pdfDocument; + + this.pdfDocumentProperties.setDocumentAndUrl(pdfDocument, this.url); + + var downloadedPromise = pdfDocument.getDownloadInfo().then(function() { + self.downloadComplete = true; + self.loadingBar.hide(); + }); + + var pagesCount = pdfDocument.numPages; + document.getElementById('numPages').textContent = + mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}'); + document.getElementById('pageNumber').max = pagesCount; + + var id = this.documentFingerprint = pdfDocument.fingerprint; + var store = this.store = new ViewHistory(id); + + var pdfViewer = this.pdfViewer; + pdfViewer.currentScale = scale; + pdfViewer.setDocument(pdfDocument); + var firstPagePromise = pdfViewer.firstPagePromise; + var pagesPromise = pdfViewer.pagesPromise; + var onePageRendered = pdfViewer.onePageRendered; + + this.pageRotation = 0; + this.isInitialViewSet = false; + this.pagesRefMap = pdfViewer.pagesRefMap; + + this.pdfThumbnailViewer.setDocument(pdfDocument); + + firstPagePromise.then(function(pdfPage) { + downloadedPromise.then(function () { + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('documentload', true, true, {}); + window.dispatchEvent(event); + }); + + self.loadingBar.setWidth(document.getElementById('viewer')); + + if (!PDFJS.disableHistory && !self.isViewerEmbedded) { + // The browsing history is only enabled when the viewer is standalone, + // i.e. not when it is embedded in a web page. + if (!self.preferenceShowPreviousViewOnLoad && window.history.state) { + window.history.replaceState(null, ''); + } + PDFHistory.initialize(self.documentFingerprint, self); + } + + store.initializedPromise.then(function resolved() { + var storedHash = null; + if (self.preferenceShowPreviousViewOnLoad && + store.get('exists', false)) { + var pageNum = store.get('page', '1'); + var zoom = self.preferenceDefaultZoomValue || + store.get('zoom', self.pdfViewer.currentScale); + var left = store.get('scrollLeft', '0'); + var top = store.get('scrollTop', '0'); + + storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' + + left + ',' + top; + } else if (self.preferenceDefaultZoomValue) { + storedHash = 'page=1&zoom=' + self.preferenceDefaultZoomValue; + } + self.setInitialView(storedHash, scale); + + // Make all navigation keys work on document load, + // unless the viewer is embedded in a web page. + if (!self.isViewerEmbedded) { + self.pdfViewer.focus(); + } + }, function rejected(reason) { + console.error(reason); + self.setInitialView(null, scale); + }); + }); + + pagesPromise.then(function() { + if (self.supportsPrinting) { + pdfDocument.getJavaScript().then(function(javaScript) { + if (javaScript.length) { + console.warn('Warning: JavaScript is not supported'); + self.fallback(PDFJS.UNSUPPORTED_FEATURES.javaScript); + } + // Hack to support auto printing. + var regex = /\bprint\s*\(/g; + for (var i = 0, ii = javaScript.length; i < ii; i++) { + var js = javaScript[i]; + if (js && regex.test(js)) { + setTimeout(function() { + window.print(); + }); + return; + } + } + }); + } + }); + + // outline depends on pagesRefMap + var promises = [pagesPromise, this.animationStartedPromise]; + Promise.all(promises).then(function() { + pdfDocument.getOutline().then(function(outline) { + var container = document.getElementById('outlineView'); + self.outline = new PDFOutlineView({ + container: container, + outline: outline, + linkService: self + }); + self.outline.render(); + document.getElementById('viewOutline').disabled = !outline; + + if (!outline && !container.classList.contains('hidden')) { + self.switchSidebarView('thumbs'); + } + if (outline && + self.preferenceSidebarViewOnLoad === SidebarView.OUTLINE) { + self.switchSidebarView('outline', true); + } + }); + pdfDocument.getAttachments().then(function(attachments) { + var container = document.getElementById('attachmentsView'); + self.attachments = new PDFAttachmentView({ + container: container, + attachments: attachments, + downloadManager: new DownloadManager() + }); + self.attachments.render(); + document.getElementById('viewAttachments').disabled = !attachments; + + if (!attachments && !container.classList.contains('hidden')) { + self.switchSidebarView('thumbs'); + } + if (attachments && + self.preferenceSidebarViewOnLoad === SidebarView.ATTACHMENTS) { + self.switchSidebarView('attachments', true); + } + }); + }); + + if (self.preferenceSidebarViewOnLoad === SidebarView.THUMBS) { + Promise.all([firstPagePromise, onePageRendered]).then(function () { + self.switchSidebarView('thumbs', true); + }); + } + + pdfDocument.getMetadata().then(function(data) { + var info = data.info, metadata = data.metadata; + self.documentInfo = info; + self.metadata = metadata; + + // Provides some basic debug information + console.log('PDF ' + pdfDocument.fingerprint + ' [' + + info.PDFFormatVersion + ' ' + (info.Producer || '-').trim() + + ' / ' + (info.Creator || '-').trim() + ']' + + ' (PDF.js: ' + (PDFJS.version || '-') + + (!PDFJS.disableWebGL ? ' [WebGL]' : '') + ')'); + + var pdfTitle; + if (metadata && metadata.has('dc:title')) { + var title = metadata.get('dc:title'); + // Ghostscript sometimes return 'Untitled', sets the title to 'Untitled' + if (title !== 'Untitled') { + pdfTitle = title; + } + } + + if (!pdfTitle && info && info['Title']) { + pdfTitle = info['Title']; + } + + if (pdfTitle) { + self.setTitle(pdfTitle + ' - ' + document.title); + } + + if (info.IsAcroFormPresent) { + console.warn('Warning: AcroForm/XFA is not supported'); + self.fallback(PDFJS.UNSUPPORTED_FEATURES.forms); + } + + }); + }, + + setInitialView: function pdfViewSetInitialView(storedHash, scale) { + this.isInitialViewSet = true; + + // When opening a new file (when one is already loaded in the viewer): + // Reset 'currentPageNumber', since otherwise the page's scale will be wrong + // if 'currentPageNumber' is larger than the number of pages in the file. + document.getElementById('pageNumber').value = + this.pdfViewer.currentPageNumber = 1; + + if (PDFHistory.initialDestination) { + this.navigateTo(PDFHistory.initialDestination); + PDFHistory.initialDestination = null; + } else if (this.initialBookmark) { + this.setHash(this.initialBookmark); + PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark); + this.initialBookmark = null; + } else if (storedHash) { + this.setHash(storedHash); + } else if (scale) { + this.setScale(scale, true); + this.page = 1; + } + + if (this.pdfViewer.currentScale === UNKNOWN_SCALE) { + // Scale was not initialized: invalid bookmark or scale was not specified. + // Setting the default one. + this.setScale(DEFAULT_SCALE, true); + } + }, + + cleanup: function pdfViewCleanup() { + this.pdfViewer.cleanup(); + this.pdfThumbnailViewer.cleanup(); + this.pdfDocument.cleanup(); + }, + + forceRendering: function pdfViewForceRendering() { + this.pdfRenderingQueue.printing = this.printing; + this.pdfRenderingQueue.isThumbnailViewEnabled = this.sidebarOpen; + this.pdfRenderingQueue.renderHighestPriority(); + }, + + setHash: function pdfViewSetHash(hash) { + if (!this.isInitialViewSet) { + this.initialBookmark = hash; + return; + } + if (!hash) { + return; + } + + if (hash.indexOf('=') >= 0) { + var params = this.parseQueryString(hash); + // borrowing syntax from "Parameters for Opening PDF Files" + if ('nameddest' in params) { + PDFHistory.updateNextHashParam(params.nameddest); + this.navigateTo(params.nameddest); + return; + } + var pageNumber, dest; + if ('page' in params) { + pageNumber = (params.page | 0) || 1; + } + if ('zoom' in params) { + // Build the destination array. + var zoomArgs = params.zoom.split(','); // scale,left,top + var zoomArg = zoomArgs[0]; + var zoomArgNumber = parseFloat(zoomArg); + + if (zoomArg.indexOf('Fit') === -1) { + // If the zoomArg is a number, it has to get divided by 100. If it's + // a string, it should stay as it is. + dest = [null, { name: 'XYZ' }, + zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null, + zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null, + (zoomArgNumber ? zoomArgNumber / 100 : zoomArg)]; + } else { + if (zoomArg === 'Fit' || zoomArg === 'FitB') { + dest = [null, { name: zoomArg }]; + } else if ((zoomArg === 'FitH' || zoomArg === 'FitBH') || + (zoomArg === 'FitV' || zoomArg === 'FitBV')) { + dest = [null, { name: zoomArg }, + zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null]; + } else if (zoomArg === 'FitR') { + if (zoomArgs.length !== 5) { + console.error('pdfViewSetHash: ' + + 'Not enough parameters for \'FitR\'.'); + } else { + dest = [null, { name: zoomArg }, + (zoomArgs[1] | 0), (zoomArgs[2] | 0), + (zoomArgs[3] | 0), (zoomArgs[4] | 0)]; + } + } else { + console.error('pdfViewSetHash: \'' + zoomArg + + '\' is not a valid zoom value.'); + } + } + } + if (dest) { + this.pdfViewer.scrollPageIntoView(pageNumber || this.page, dest); + } else if (pageNumber) { + this.page = pageNumber; // simple page + } + if ('pagemode' in params) { + if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks' || + params.pagemode === 'attachments') { + this.switchSidebarView((params.pagemode === 'bookmarks' ? + 'outline' : params.pagemode), true); + } else if (params.pagemode === 'none' && this.sidebarOpen) { + document.getElementById('sidebarToggle').click(); + } + } + } else if (/^\d+$/.test(hash)) { // page number + this.page = hash; + } else { // named destination + PDFHistory.updateNextHashParam(unescape(hash)); + this.navigateTo(unescape(hash)); + } + }, + + refreshThumbnailViewer: function pdfViewRefreshThumbnailViewer() { + var pdfViewer = this.pdfViewer; + var thumbnailViewer = this.pdfThumbnailViewer; + + // set thumbnail images of rendered pages + var pagesCount = pdfViewer.pagesCount; + for (var pageIndex = 0; pageIndex < pagesCount; pageIndex++) { + var pageView = pdfViewer.getPageView(pageIndex); + if (pageView && pageView.renderingState === RenderingStates.FINISHED) { + var thumbnailView = thumbnailViewer.getThumbnail(pageIndex); + thumbnailView.setImage(pageView); + } + } + + thumbnailViewer.scrollThumbnailIntoView(this.page); + }, + + switchSidebarView: function pdfViewSwitchSidebarView(view, openSidebar) { + if (openSidebar && !this.sidebarOpen) { + document.getElementById('sidebarToggle').click(); + } + var thumbsView = document.getElementById('thumbnailView'); + var outlineView = document.getElementById('outlineView'); + var attachmentsView = document.getElementById('attachmentsView'); + + var thumbsButton = document.getElementById('viewThumbnail'); + var outlineButton = document.getElementById('viewOutline'); + var attachmentsButton = document.getElementById('viewAttachments'); + + switch (view) { + case 'thumbs': + var wasAnotherViewVisible = thumbsView.classList.contains('hidden'); + + thumbsButton.classList.add('toggled'); + outlineButton.classList.remove('toggled'); + attachmentsButton.classList.remove('toggled'); + thumbsView.classList.remove('hidden'); + outlineView.classList.add('hidden'); + attachmentsView.classList.add('hidden'); + + this.forceRendering(); + + if (wasAnotherViewVisible) { + this.pdfThumbnailViewer.ensureThumbnailVisible(this.page); + } + break; + + case 'outline': + thumbsButton.classList.remove('toggled'); + outlineButton.classList.add('toggled'); + attachmentsButton.classList.remove('toggled'); + thumbsView.classList.add('hidden'); + outlineView.classList.remove('hidden'); + attachmentsView.classList.add('hidden'); + + if (outlineButton.getAttribute('disabled')) { + return; + } + break; + + case 'attachments': + thumbsButton.classList.remove('toggled'); + outlineButton.classList.remove('toggled'); + attachmentsButton.classList.add('toggled'); + thumbsView.classList.add('hidden'); + outlineView.classList.add('hidden'); + attachmentsView.classList.remove('hidden'); + + if (attachmentsButton.getAttribute('disabled')) { + return; + } + break; + } + }, + + // Helper function to parse query string (e.g. ?param1=value&parm2=...). + parseQueryString: function pdfViewParseQueryString(query) { + var parts = query.split('&'); + var params = {}; + for (var i = 0, ii = parts.length; i < ii; ++i) { + var param = parts[i].split('='); + var key = param[0].toLowerCase(); + var value = param.length > 1 ? param[1] : null; + params[decodeURIComponent(key)] = decodeURIComponent(value); + } + return params; + }, + + beforePrint: function pdfViewSetupBeforePrint() { + if (!this.supportsPrinting) { + var printMessage = mozL10n.get('printing_not_supported', null, + 'Warning: Printing is not fully supported by this browser.'); + this.error(printMessage); + return; + } + + var alertNotReady = false; + var i, ii; + if (!this.pagesCount) { + alertNotReady = true; + } else { + for (i = 0, ii = this.pagesCount; i < ii; ++i) { + if (!this.pdfViewer.getPageView(i).pdfPage) { + alertNotReady = true; + break; + } + } + } + if (alertNotReady) { + var notReadyMessage = mozL10n.get('printing_not_ready', null, + 'Warning: The PDF is not fully loaded for printing.'); + window.alert(notReadyMessage); + return; + } + + this.printing = true; + this.forceRendering(); + + var body = document.querySelector('body'); + body.setAttribute('data-mozPrintCallback', true); + + if (!this.hasEqualPageSizes) { + console.warn('Not all pages have the same size. The printed result ' + + 'may be incorrect!'); + } + + // Insert a @page + size rule to make sure that the page size is correctly + // set. Note that we assume that all pages have the same size, because + // variable-size pages are not supported yet (at least in Chrome & Firefox). + // TODO(robwu): Use named pages when size calculation bugs get resolved + // (e.g. https://crbug.com/355116) AND when support for named pages is + // added (http://www.w3.org/TR/css3-page/#using-named-pages). + // In browsers where @page + size is not supported (such as Firefox, + // https://bugzil.la/851441), the next stylesheet will be ignored and the + // user has to select the correct paper size in the UI if wanted. + this.pageStyleSheet = document.createElement('style'); + var pageSize = this.pdfViewer.getPageView(0).pdfPage.getViewport(1); + this.pageStyleSheet.textContent = + // "size: " is what we need. But also add "A4" because + // Firefox incorrectly reports support for the other value. + '@supports ((size:A4) and (size:1pt 1pt)) {' + + '@page { size: ' + pageSize.width + 'pt ' + pageSize.height + 'pt;}' + + // The canvas and each ancestor node must have a height of 100% to make + // sure that each canvas is printed on exactly one page. + '#printContainer {height:100%}' + + '#printContainer > div {width:100% !important;height:100% !important;}' + + '}'; + body.appendChild(this.pageStyleSheet); + + for (i = 0, ii = this.pagesCount; i < ii; ++i) { + this.pdfViewer.getPageView(i).beforePrint(); + } + + }, + + // Whether all pages of the PDF have the same width and height. + get hasEqualPageSizes() { + var firstPage = this.pdfViewer.getPageView(0); + for (var i = 1, ii = this.pagesCount; i < ii; ++i) { + var pageView = this.pdfViewer.getPageView(i); + if (pageView.width !== firstPage.width || + pageView.height !== firstPage.height) { + return false; + } + } + return true; + }, + + afterPrint: function pdfViewSetupAfterPrint() { + var div = document.getElementById('printContainer'); + while (div.hasChildNodes()) { + div.removeChild(div.lastChild); + } + + if (this.pageStyleSheet && this.pageStyleSheet.parentNode) { + this.pageStyleSheet.parentNode.removeChild(this.pageStyleSheet); + this.pageStyleSheet = null; + } + + this.printing = false; + this.forceRendering(); + }, + + setScale: function (value, resetAutoSettings) { + this.updateScaleControls = !!resetAutoSettings; + this.pdfViewer.currentScaleValue = value; + this.updateScaleControls = true; + }, + + rotatePages: function pdfViewRotatePages(delta) { + var pageNumber = this.page; + this.pageRotation = (this.pageRotation + 360 + delta) % 360; + this.pdfViewer.pagesRotation = this.pageRotation; + this.pdfThumbnailViewer.pagesRotation = this.pageRotation; + + this.forceRendering(); + + this.pdfViewer.scrollPageIntoView(pageNumber); + }, + + requestPresentationMode: function pdfViewRequestPresentationMode() { + if (!this.pdfPresentationMode) { + return; + } + this.pdfPresentationMode.request(); + }, + + /** + * @param {number} delta - The delta value from the mouse event. + */ + scrollPresentationMode: function pdfViewScrollPresentationMode(delta) { + if (!this.pdfPresentationMode) { + return; + } + this.pdfPresentationMode.mouseScroll(delta); + } +}; +window.PDFView = PDFViewerApplication; // obsolete name, using it as an alias + + +function webViewerLoad(evt) { + PDFViewerApplication.initialize().then(webViewerInitialized); +} + +function webViewerInitialized() { + var queryString = document.location.search.substring(1); + var params = PDFViewerApplication.parseQueryString(queryString); + var file = 'file' in params ? params.file : DEFAULT_URL; + + var fileInput = document.createElement('input'); + fileInput.id = 'fileInput'; + fileInput.className = 'fileInput'; + fileInput.setAttribute('type', 'file'); + fileInput.oncontextmenu = noContextMenuHandler; + document.body.appendChild(fileInput); + + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + document.getElementById('openFile').setAttribute('hidden', 'true'); + document.getElementById('secondaryOpenFile').setAttribute('hidden', 'true'); + } else { + document.getElementById('fileInput').value = null; + } + + var locale = PDFJS.locale || navigator.language; + + if (PDFViewerApplication.preferencePdfBugEnabled) { + // Special debugging flags in the hash section of the URL. + var hash = document.location.hash.substring(1); + var hashParams = PDFViewerApplication.parseQueryString(hash); + + if ('disableworker' in hashParams) { + PDFJS.disableWorker = (hashParams['disableworker'] === 'true'); + } + if ('disablerange' in hashParams) { + PDFJS.disableRange = (hashParams['disablerange'] === 'true'); + } + if ('disablestream' in hashParams) { + PDFJS.disableStream = (hashParams['disablestream'] === 'true'); + } + if ('disableautofetch' in hashParams) { + PDFJS.disableAutoFetch = (hashParams['disableautofetch'] === 'true'); + } + if ('disablefontface' in hashParams) { + PDFJS.disableFontFace = (hashParams['disablefontface'] === 'true'); + } + if ('disablehistory' in hashParams) { + PDFJS.disableHistory = (hashParams['disablehistory'] === 'true'); + } + if ('webgl' in hashParams) { + PDFJS.disableWebGL = (hashParams['webgl'] !== 'true'); + } + if ('useonlycsszoom' in hashParams) { + PDFJS.useOnlyCssZoom = (hashParams['useonlycsszoom'] === 'true'); + } + if ('verbosity' in hashParams) { + PDFJS.verbosity = hashParams['verbosity'] | 0; + } + if ('ignorecurrentpositiononzoom' in hashParams) { + IGNORE_CURRENT_POSITION_ON_ZOOM = + (hashParams['ignorecurrentpositiononzoom'] === 'true'); + } + if ('locale' in hashParams) { + locale = hashParams['locale']; + } + if ('textlayer' in hashParams) { + switch (hashParams['textlayer']) { + case 'off': + PDFJS.disableTextLayer = true; + break; + case 'visible': + case 'shadow': + case 'hover': + var viewer = document.getElementById('viewer'); + viewer.classList.add('textLayer-' + hashParams['textlayer']); + break; + } + } + if ('pdfbug' in hashParams) { + PDFJS.pdfBug = true; + var pdfBug = hashParams['pdfbug']; + var enabled = pdfBug.split(','); + PDFBug.enable(enabled); + PDFBug.init(); + } + } + + mozL10n.setLanguage(locale); + + if (!PDFViewerApplication.supportsPrinting) { + document.getElementById('print').classList.add('hidden'); + document.getElementById('secondaryPrint').classList.add('hidden'); + } + + if (!PDFViewerApplication.supportsFullscreen) { + document.getElementById('presentationMode').classList.add('hidden'); + document.getElementById('secondaryPresentationMode'). + classList.add('hidden'); + } + + if (PDFViewerApplication.supportsIntegratedFind) { + document.getElementById('viewFind').classList.add('hidden'); + } + + // Listen for unsupported features to trigger the fallback UI. + PDFJS.UnsupportedManager.listen( + PDFViewerApplication.fallback.bind(PDFViewerApplication)); + + // Suppress context menus for some controls + document.getElementById('scaleSelect').oncontextmenu = noContextMenuHandler; + + var mainContainer = document.getElementById('mainContainer'); + var outerContainer = document.getElementById('outerContainer'); + mainContainer.addEventListener('transitionend', function(e) { + if (e.target === mainContainer) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('resize', false, false, window, 0); + window.dispatchEvent(event); + outerContainer.classList.remove('sidebarMoving'); + } + }, true); + + document.getElementById('sidebarToggle').addEventListener('click', + function() { + this.classList.toggle('toggled'); + outerContainer.classList.add('sidebarMoving'); + outerContainer.classList.toggle('sidebarOpen'); + PDFViewerApplication.sidebarOpen = + outerContainer.classList.contains('sidebarOpen'); + if (PDFViewerApplication.sidebarOpen) { + PDFViewerApplication.refreshThumbnailViewer(); + } + PDFViewerApplication.forceRendering(); + }); + + document.getElementById('viewThumbnail').addEventListener('click', + function() { + PDFViewerApplication.switchSidebarView('thumbs'); + }); + + document.getElementById('viewOutline').addEventListener('click', + function() { + PDFViewerApplication.switchSidebarView('outline'); + }); + + document.getElementById('viewAttachments').addEventListener('click', + function() { + PDFViewerApplication.switchSidebarView('attachments'); + }); + + document.getElementById('previous').addEventListener('click', + function() { + PDFViewerApplication.page--; + }); + + document.getElementById('next').addEventListener('click', + function() { + PDFViewerApplication.page++; + }); + + document.getElementById('zoomIn').addEventListener('click', + function() { + PDFViewerApplication.zoomIn(); + }); + + document.getElementById('zoomOut').addEventListener('click', + function() { + PDFViewerApplication.zoomOut(); + }); + + document.getElementById('pageNumber').addEventListener('click', function() { + this.select(); + }); + + document.getElementById('pageNumber').addEventListener('change', function() { + // Handle the user inputting a floating point number. + PDFViewerApplication.page = (this.value | 0); + + if (this.value !== (this.value | 0).toString()) { + this.value = PDFViewerApplication.page; + } + }); + + document.getElementById('scaleSelect').addEventListener('change', + function() { + PDFViewerApplication.setScale(this.value, false); + }); + + document.getElementById('presentationMode').addEventListener('click', + SecondaryToolbar.presentationModeClick.bind(SecondaryToolbar)); + + document.getElementById('openFile').addEventListener('click', + SecondaryToolbar.openFileClick.bind(SecondaryToolbar)); + + document.getElementById('print').addEventListener('click', + SecondaryToolbar.printClick.bind(SecondaryToolbar)); + + document.getElementById('download').addEventListener('click', + SecondaryToolbar.downloadClick.bind(SecondaryToolbar)); + + + if (file && file.lastIndexOf('file:', 0) === 0) { + // file:-scheme. Load the contents in the main thread because QtWebKit + // cannot load file:-URLs in a Web Worker. file:-URLs are usually loaded + // very quickly, so there is no need to set up progress event listeners. + PDFViewerApplication.setTitleUsingUrl(file); + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + PDFViewerApplication.open(new Uint8Array(xhr.response), 0); + }; + try { + xhr.open('GET', file); + xhr.responseType = 'arraybuffer'; + xhr.send(); + } catch (e) { + PDFViewerApplication.error(mozL10n.get('loading_error', null, + 'An error occurred while loading the PDF.'), e); + } + return; + } + + if (file) { + PDFViewerApplication.open(file, 0); + } +} + +document.addEventListener('DOMContentLoaded', webViewerLoad, true); + +document.addEventListener('pagerendered', function (e) { + var pageNumber = e.detail.pageNumber; + var pageIndex = pageNumber - 1; + var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex); + + if (PDFViewerApplication.sidebarOpen) { + var thumbnailView = PDFViewerApplication.pdfThumbnailViewer. + getThumbnail(pageIndex); + thumbnailView.setImage(pageView); + } + + if (PDFJS.pdfBug && Stats.enabled && pageView.stats) { + Stats.add(pageNumber, pageView.stats); + } + + if (pageView.error) { + PDFViewerApplication.error(mozL10n.get('rendering_error', null, + 'An error occurred while rendering the page.'), pageView.error); + } + + // If the page is still visible when it has finished rendering, + // ensure that the page number input loading indicator is hidden. + if (pageNumber === PDFViewerApplication.page) { + var pageNumberInput = document.getElementById('pageNumber'); + pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR); + } + +}, true); + +document.addEventListener('textlayerrendered', function (e) { + var pageIndex = e.detail.pageNumber - 1; + var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex); + +}, true); + +window.addEventListener('presentationmodechanged', function (e) { + var active = e.detail.active; + var switchInProgress = e.detail.switchInProgress; + PDFViewerApplication.pdfViewer.presentationModeState = + switchInProgress ? PresentationModeState.CHANGING : + active ? PresentationModeState.FULLSCREEN : PresentationModeState.NORMAL; +}); + +function updateViewarea() { + if (!PDFViewerApplication.initialized) { + return; + } + PDFViewerApplication.pdfViewer.update(); +} + +window.addEventListener('updateviewarea', function (evt) { + if (!PDFViewerApplication.initialized) { + return; + } + var location = evt.location; + + PDFViewerApplication.store.initializedPromise.then(function() { + PDFViewerApplication.store.setMultiple({ + 'exists': true, + 'page': location.pageNumber, + 'zoom': location.scale, + 'scrollLeft': location.left, + 'scrollTop': location.top + }).catch(function() { + // unable to write to storage + }); + }); + var href = PDFViewerApplication.getAnchorUrl(location.pdfOpenParams); + document.getElementById('viewBookmark').href = href; + document.getElementById('secondaryViewBookmark').href = href; + + // Update the current bookmark in the browsing history. + PDFHistory.updateCurrentBookmark(location.pdfOpenParams, location.pageNumber); + + // Show/hide the loading indicator in the page number input element. + var pageNumberInput = document.getElementById('pageNumber'); + var currentPage = + PDFViewerApplication.pdfViewer.getPageView(PDFViewerApplication.page - 1); + + if (currentPage.renderingState === RenderingStates.FINISHED) { + pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR); + } else { + pageNumberInput.classList.add(PAGE_NUMBER_LOADING_INDICATOR); + } +}, true); + +window.addEventListener('resize', function webViewerResize(evt) { + if (PDFViewerApplication.initialized && + (document.getElementById('pageAutoOption').selected || + /* Note: the scale is constant for |pageActualOption|. */ + document.getElementById('pageFitOption').selected || + document.getElementById('pageWidthOption').selected)) { + var selectedScale = document.getElementById('scaleSelect').value; + PDFViewerApplication.setScale(selectedScale, false); + } + updateViewarea(); + + // Set the 'max-height' CSS property of the secondary toolbar. + SecondaryToolbar.setMaxHeight(document.getElementById('viewerContainer')); +}); + +window.addEventListener('hashchange', function webViewerHashchange(evt) { + if (PDFHistory.isHashChangeUnlocked) { + PDFViewerApplication.setHash(document.location.hash.substring(1)); + } +}); + +window.addEventListener('change', function webViewerChange(evt) { + var files = evt.target.files; + if (!files || files.length === 0) { + return; + } + var file = files[0]; + + if (!PDFJS.disableCreateObjectURL && + typeof URL !== 'undefined' && URL.createObjectURL) { + PDFViewerApplication.open(URL.createObjectURL(file), 0); + } else { + // Read the local file into a Uint8Array. + var fileReader = new FileReader(); + fileReader.onload = function webViewerChangeFileReaderOnload(evt) { + var buffer = evt.target.result; + var uint8Array = new Uint8Array(buffer); + PDFViewerApplication.open(uint8Array, 0); + }; + fileReader.readAsArrayBuffer(file); + } + + PDFViewerApplication.setTitleUsingUrl(file.name); + + // URL does not reflect proper document location - hiding some icons. + document.getElementById('viewBookmark').setAttribute('hidden', 'true'); + document.getElementById('secondaryViewBookmark'). + setAttribute('hidden', 'true'); + document.getElementById('download').setAttribute('hidden', 'true'); + document.getElementById('secondaryDownload').setAttribute('hidden', 'true'); +}, true); + +function selectScaleOption(value) { + var options = document.getElementById('scaleSelect').options; + var predefinedValueFound = false; + for (var i = 0; i < options.length; i++) { + var option = options[i]; + if (option.value !== value) { + option.selected = false; + continue; + } + option.selected = true; + predefinedValueFound = true; + } + return predefinedValueFound; +} + +window.addEventListener('localized', function localized(evt) { + document.getElementsByTagName('html')[0].dir = mozL10n.getDirection(); + + PDFViewerApplication.animationStartedPromise.then(function() { + // Adjust the width of the zoom box to fit the content. + // Note: If the window is narrow enough that the zoom box is not visible, + // we temporarily show it to be able to adjust its width. + var container = document.getElementById('scaleSelectContainer'); + if (container.clientWidth === 0) { + container.setAttribute('style', 'display: inherit;'); + } + if (container.clientWidth > 0) { + var select = document.getElementById('scaleSelect'); + select.setAttribute('style', 'min-width: inherit;'); + var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING; + select.setAttribute('style', 'min-width: ' + + (width + SCALE_SELECT_PADDING) + 'px;'); + container.setAttribute('style', 'min-width: ' + width + 'px; ' + + 'max-width: ' + width + 'px;'); + } + + // Set the 'max-height' CSS property of the secondary toolbar. + SecondaryToolbar.setMaxHeight(document.getElementById('viewerContainer')); + }); +}, true); + +window.addEventListener('scalechange', function scalechange(evt) { + document.getElementById('zoomOut').disabled = (evt.scale === MIN_SCALE); + document.getElementById('zoomIn').disabled = (evt.scale === MAX_SCALE); + + var customScaleOption = document.getElementById('customScaleOption'); + customScaleOption.selected = false; + + if (!PDFViewerApplication.updateScaleControls && + (document.getElementById('pageAutoOption').selected || + document.getElementById('pageActualOption').selected || + document.getElementById('pageFitOption').selected || + document.getElementById('pageWidthOption').selected)) { + updateViewarea(); + return; + } + + if (evt.presetValue) { + selectScaleOption(evt.presetValue); + updateViewarea(); + return; + } + + var predefinedValueFound = selectScaleOption('' + evt.scale); + if (!predefinedValueFound) { + var customScale = Math.round(evt.scale * 10000) / 100; + customScaleOption.textContent = + mozL10n.get('page_scale_percent', { scale: customScale }, '{{scale}}%'); + customScaleOption.selected = true; + } + updateViewarea(); +}, true); + +window.addEventListener('pagechange', function pagechange(evt) { + var page = evt.pageNumber; + if (evt.previousPageNumber !== page) { + document.getElementById('pageNumber').value = page; + if (PDFViewerApplication.sidebarOpen) { + PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page); + } + } + var numPages = PDFViewerApplication.pagesCount; + + document.getElementById('previous').disabled = (page <= 1); + document.getElementById('next').disabled = (page >= numPages); + + document.getElementById('firstPage').disabled = (page <= 1); + document.getElementById('lastPage').disabled = (page >= numPages); + + // we need to update stats + if (PDFJS.pdfBug && Stats.enabled) { + var pageView = PDFViewerApplication.pdfViewer.getPageView(page - 1); + if (pageView.stats) { + Stats.add(page, pageView.stats); + } + } + + // checking if the this.page was called from the updateViewarea function + if (evt.updateInProgress) { + return; + } + // Avoid scrolling the first page during loading + if (this.loading && page === 1) { + return; + } + PDFViewerApplication.pdfViewer.scrollPageIntoView(page); +}, true); + +function handleMouseWheel(evt) { + var MOUSE_WHEEL_DELTA_FACTOR = 40; + var ticks = (evt.type === 'DOMMouseScroll') ? -evt.detail : + evt.wheelDelta / MOUSE_WHEEL_DELTA_FACTOR; + var direction = (ticks < 0) ? 'zoomOut' : 'zoomIn'; + + if (PDFViewerApplication.pdfViewer.isInPresentationMode) { + evt.preventDefault(); + PDFViewerApplication.scrollPresentationMode(ticks * + MOUSE_WHEEL_DELTA_FACTOR); + } else if (evt.ctrlKey || evt.metaKey) { + // Only zoom the pages, not the entire viewer. + evt.preventDefault(); + PDFViewerApplication[direction](Math.abs(ticks)); + } +} + +window.addEventListener('DOMMouseScroll', handleMouseWheel); +window.addEventListener('mousewheel', handleMouseWheel); + +window.addEventListener('click', function click(evt) { + if (SecondaryToolbar.opened && + PDFViewerApplication.pdfViewer.containsElement(evt.target)) { + SecondaryToolbar.close(); + } +}, false); + +window.addEventListener('keydown', function keydown(evt) { + if (OverlayManager.active) { + return; + } + + var handled = false; + var cmd = (evt.ctrlKey ? 1 : 0) | + (evt.altKey ? 2 : 0) | + (evt.shiftKey ? 4 : 0) | + (evt.metaKey ? 8 : 0); + + var pdfViewer = PDFViewerApplication.pdfViewer; + var isViewerInPresentationMode = pdfViewer && pdfViewer.isInPresentationMode; + + // First, handle the key bindings that are independent whether an input + // control is selected or not. + if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) { + // either CTRL or META key with optional SHIFT. + switch (evt.keyCode) { + case 70: // f + if (!PDFViewerApplication.supportsIntegratedFind) { + PDFViewerApplication.findBar.open(); + handled = true; + } + break; + case 71: // g + if (!PDFViewerApplication.supportsIntegratedFind) { + PDFViewerApplication.findBar.dispatchEvent('again', + cmd === 5 || cmd === 12); + handled = true; + } + break; + case 61: // FF/Mac '=' + case 107: // FF '+' and '=' + case 187: // Chrome '+' + case 171: // FF with German keyboard + if (!isViewerInPresentationMode) { + PDFViewerApplication.zoomIn(); + } + handled = true; + break; + case 173: // FF/Mac '-' + case 109: // FF '-' + case 189: // Chrome '-' + if (!isViewerInPresentationMode) { + PDFViewerApplication.zoomOut(); + } + handled = true; + break; + case 48: // '0' + case 96: // '0' on Numpad of Swedish keyboard + if (!isViewerInPresentationMode) { + // keeping it unhandled (to restore page zoom to 100%) + setTimeout(function () { + // ... and resetting the scale after browser adjusts its scale + PDFViewerApplication.setScale(DEFAULT_SCALE, true); + }); + handled = false; + } + break; + } + } + + // CTRL or META without shift + if (cmd === 1 || cmd === 8) { + switch (evt.keyCode) { + case 83: // s + PDFViewerApplication.download(); + handled = true; + break; + } + } + + // CTRL+ALT or Option+Command + if (cmd === 3 || cmd === 10) { + switch (evt.keyCode) { + case 80: // p + PDFViewerApplication.requestPresentationMode(); + handled = true; + break; + case 71: // g + // focuses input#pageNumber field + document.getElementById('pageNumber').select(); + handled = true; + break; + } + } + + if (handled) { + evt.preventDefault(); + return; + } + + // Some shortcuts should not get handled if a control/input element + // is selected. + var curElement = document.activeElement || document.querySelector(':focus'); + var curElementTagName = curElement && curElement.tagName.toUpperCase(); + if (curElementTagName === 'INPUT' || + curElementTagName === 'TEXTAREA' || + curElementTagName === 'SELECT') { + // Make sure that the secondary toolbar is closed when Escape is pressed. + if (evt.keyCode !== 27) { // 'Esc' + return; + } + } + + if (cmd === 0) { // no control key pressed at all. + switch (evt.keyCode) { + case 38: // up arrow + case 33: // pg up + case 8: // backspace + if (!isViewerInPresentationMode && + PDFViewerApplication.currentScaleValue !== 'page-fit') { + break; + } + /* in presentation mode */ + /* falls through */ + case 37: // left arrow + // horizontal scrolling using arrow keys + if (pdfViewer.isHorizontalScrollbarEnabled) { + break; + } + /* falls through */ + case 75: // 'k' + case 80: // 'p' + PDFViewerApplication.page--; + handled = true; + break; + case 27: // esc key + if (SecondaryToolbar.opened) { + SecondaryToolbar.close(); + handled = true; + } + if (!PDFViewerApplication.supportsIntegratedFind && + PDFViewerApplication.findBar.opened) { + PDFViewerApplication.findBar.close(); + handled = true; + } + break; + case 40: // down arrow + case 34: // pg down + case 32: // spacebar + if (!isViewerInPresentationMode && + PDFViewerApplication.currentScaleValue !== 'page-fit') { + break; + } + /* falls through */ + case 39: // right arrow + // horizontal scrolling using arrow keys + if (pdfViewer.isHorizontalScrollbarEnabled) { + break; + } + /* falls through */ + case 74: // 'j' + case 78: // 'n' + PDFViewerApplication.page++; + handled = true; + break; + + case 36: // home + if (isViewerInPresentationMode || PDFViewerApplication.page > 1) { + PDFViewerApplication.page = 1; + handled = true; + } + break; + case 35: // end + if (isViewerInPresentationMode || (PDFViewerApplication.pdfDocument && + PDFViewerApplication.page < PDFViewerApplication.pagesCount)) { + PDFViewerApplication.page = PDFViewerApplication.pagesCount; + handled = true; + } + break; + + case 72: // 'h' + if (!isViewerInPresentationMode) { + HandTool.toggle(); + } + break; + case 82: // 'r' + PDFViewerApplication.rotatePages(90); + break; + } + } + + if (cmd === 4) { // shift-key + switch (evt.keyCode) { + case 32: // spacebar + if (!isViewerInPresentationMode && + PDFViewerApplication.currentScaleValue !== 'page-fit') { + break; + } + PDFViewerApplication.page--; + handled = true; + break; + + case 82: // 'r' + PDFViewerApplication.rotatePages(-90); + break; + } + } + + if (!handled && !isViewerInPresentationMode) { + // 33=Page Up 34=Page Down 35=End 36=Home + // 37=Left 38=Up 39=Right 40=Down + if (evt.keyCode >= 33 && evt.keyCode <= 40 && + !pdfViewer.containsElement(curElement)) { + // The page container is not focused, but a page navigation key has been + // pressed. Change the focus to the viewer container to make sure that + // navigation by keyboard works as expected. + pdfViewer.focus(); + } + // 32=Spacebar + if (evt.keyCode === 32 && curElementTagName !== 'BUTTON' && + !pdfViewer.containsElement(curElement)) { + pdfViewer.focus(); + } + } + + if (cmd === 2) { // alt-key + switch (evt.keyCode) { + case 37: // left arrow + if (isViewerInPresentationMode) { + PDFHistory.back(); + handled = true; + } + break; + case 39: // right arrow + if (isViewerInPresentationMode) { + PDFHistory.forward(); + handled = true; + } + break; + } + } + + if (handled) { + evt.preventDefault(); + } +}); + +window.addEventListener('beforeprint', function beforePrint(evt) { + PDFViewerApplication.beforePrint(); +}); + +window.addEventListener('afterprint', function afterPrint(evt) { + PDFViewerApplication.afterPrint(); +}); + +(function animationStartedClosure() { + // The offsetParent is not set until the pdf.js iframe or object is visible. + // Waiting for first animation. + PDFViewerApplication.animationStartedPromise = new Promise( + function (resolve) { + window.requestAnimationFrame(resolve); + }); +})(); + + diff --git a/test-module-system/test-system-biz/src/main/resources/static/view/userlist.html b/test-module-system/test-system-biz/src/main/resources/static/view/userlist.html new file mode 100644 index 0000000..049c822 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/static/view/userlist.html @@ -0,0 +1,122 @@ + + + + + iview example + + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/templates/announcement/showContent.ftl b/test-module-system/test-system-biz/src/main/resources/templates/announcement/showContent.ftl new file mode 100644 index 0000000..29cdf90 --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/templates/announcement/showContent.ftl @@ -0,0 +1,171 @@ + + + + + + + 通告详情 + + + +
+

${data.titile}

+
+ <#if data.priority??> + + <#if data.priority == "H"> + 高 + <#elseif data.priority == "M"> + 中 + <#elseif data.priority == "L"> + 低 + <#else > + ${data.priority} + + + + <#if data.sender??> + ${data.sender} + + <#if data.sendTime??> + ${data.sendTime?string('yyyy年MM月dd日')} + + <#if data.visitsNum??> + 访问量:${data.visitsNum} + +
+
+
+ + + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/templates/demo3.ftl b/test-module-system/test-system-biz/src/main/resources/templates/demo3.ftl new file mode 100644 index 0000000..d75badc --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/templates/demo3.ftl @@ -0,0 +1,17 @@ + + + +Spring Boot FreeMarker + + + Freemarker HTML

+ + Sessionid: ${sessionid!}

+ + + <#list userList as item> + ${item!}
+ +
+ + \ No newline at end of file diff --git a/test-module-system/test-system-biz/src/main/resources/templates/pdfPreviewIframe.ftl b/test-module-system/test-system-biz/src/main/resources/templates/pdfPreviewIframe.ftl new file mode 100644 index 0000000..f20c68c --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/templates/pdfPreviewIframe.ftl @@ -0,0 +1,30 @@ +<#assign base=springMacroRequestContext.getContextUrl("")> + + + + + + + +PDF预览 + + + + + diff --git a/test-module-system/test-system-biz/src/main/resources/templates/thirdLogin.ftl b/test-module-system/test-system-biz/src/main/resources/templates/thirdLogin.ftl new file mode 100644 index 0000000..dd3ab8a --- /dev/null +++ b/test-module-system/test-system-biz/src/main/resources/templates/thirdLogin.ftl @@ -0,0 +1,28 @@ + + + + + + + 第三方登录 + + +登陆中... + + + \ No newline at end of file diff --git a/test-module-system/test-system-start/Dockerfile b/test-module-system/test-system-start/Dockerfile new file mode 100644 index 0000000..8b18d5f --- /dev/null +++ b/test-module-system/test-system-start/Dockerfile @@ -0,0 +1,29 @@ +FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:17-anolis + +MAINTAINER jeecgos@163.com + +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime + +# 解决linuxkit 精简镜像对 locale 裁剪导致中文乱码问题 java:17-anolis基于anolis(CentOS/RHEL 系)应当使用yum +RUN yum install -y --setopt=tsflags=nodocs \ + glibc-langpack-en \ + glibc-common \ + && yum clean all + +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 +ENV JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8" + +#RUN mkdir -p /jeecg-boot/config/jeecg/ + +WORKDIR /jeecg-boot + +EXPOSE 8080 + +#ADD ./src/main/resources/jeecg ./config/jeecg +ADD ./target/jeecg-system-start-3.9.2.jar ./ + +RUN mkdir -p /jeecg-boot/config + +# 【PR#9345】编码已通过JAVA_TOOL_OPTIONS设置,CMD改用&&和exec +CMD sleep 60 && exec java -Djava.security.egd=file:/dev/./urandom -jar jeecg-system-start-3.9.2.jar \ No newline at end of file diff --git a/test-module-system/test-system-start/README.md b/test-module-system/test-system-start/README.md new file mode 100644 index 0000000..4e6740c --- /dev/null +++ b/test-module-system/test-system-start/README.md @@ -0,0 +1,12 @@ +# 这个是单体启动项目 +- 项目: jeecg-module-system/jeecg-system-start +- 启动类:jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java + +- 端口:8080 +- 访问地址:http://localhost:8080/jeecg-boot +- 账号密码:admin/123456 + + +# 微服务启动项目在这里 +- 项目: jeecg-server-cloud/jeecg-system-cloud-start +- 启动类:jeecg-server-cloud/jeecg-system-cloud-start/src/main/java/org/jeecg/JeecgSystemCloudApplication.java \ No newline at end of file diff --git a/test-module-system/test-system-start/pom.xml b/test-module-system/test-system-start/pom.xml new file mode 100644 index 0000000..7d3a929 --- /dev/null +++ b/test-module-system/test-system-start/pom.xml @@ -0,0 +1,60 @@ + + + + test-module-system + com.ghb + 3.9.2 + + 4.0.0 + + test-system-start + + + + + com.ghb + test-system-biz + ${jeecgboot.version} + + + + com.ghb + test-module-business + + + + org.flywaydb + flyway-core + 7.15.0 + + + + org.springframework.boot + spring-boot-properties-migrator + runtime + + + jakarta.servlet + jakarta.servlet-api + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/test-module-system/test-system-start/src/main/java/com/ghb/base/GhbSystemApplication.java b/test-module-system/test-system-start/src/main/java/com/ghb/base/GhbSystemApplication.java new file mode 100644 index 0000000..70ef961 --- /dev/null +++ b/test-module-system/test-system-start/src/main/java/com/ghb/base/GhbSystemApplication.java @@ -0,0 +1,69 @@ +package com.ghb.base; + +import org.springframework.context.annotation.ComponentScan; +import com.xkcoding.justauth.autoconfigure.JustAuthAutoConfiguration; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.util.oConvertUtils; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +/** +* 单体启动类(采用此类启动为单体模式) +* 报错提醒: 未集成mongo报错,可以打开启动类上面的注释 exclude={MongoAutoConfiguration.class} +*/ +@Slf4j +@SpringBootApplication(exclude = MongoAutoConfiguration.class, excludeName = { + "org.jeecg.modules.jmreport.config.init.JimuReportConfiguration" +}) +@ComponentScan(basePackages = { + "com.ghb.base", + "org.jeecg.common.util", + "org.jeecg.common.modules.redis", + "org.jeecg.common.config", + "org.jeecg.common.constant", + "org.jeecg.common.enums", + "org.jeecg.common.exception", + "org.jeecg.common.base", + "org.jeecg.common.annotation" +}) +@ImportAutoConfiguration(JustAuthAutoConfiguration.class) // spring boot 3.x justauth 兼容性处理 +public class GhbSystemApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(GhbSystemApplication.class); + } + + public static void main(String[] args) throws UnknownHostException { + SpringApplication app = new SpringApplication(GhbSystemApplication.class); + Map defaultProperties = new HashMap<>(); + defaultProperties.put("management.health.elasticsearch.enabled", false); + app.setDefaultProperties(defaultProperties); + log.info("[Ghb] Elasticsearch Health Check Enabled: false" ); + + ConfigurableApplicationContext application = app.run(args);; + Environment env = application.getEnvironment(); + String ip = InetAddress.getLocalHost().getHostAddress(); + String port = env.getProperty("server.port"); + String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path")); + log.info("\n----------------------------------------------------------\n\t" + + "Application Ghb-Boot is running! Access URLs:\n\t" + + "Local: \t\thttp://localhost:" + port + path + "\n\t" + + "External: \thttp://" + ip + ":" + port + path + "/doc.html\n\t" + + "Swagger文档: \thttp://" + ip + ":" + port + path + "/doc.html\n" + + "----------------------------------------------------------"); + + } + +} \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/java/com/ghb/base/codegenerate/GhbOneGUI.java b/test-module-system/test-system-start/src/main/java/com/ghb/base/codegenerate/GhbOneGUI.java new file mode 100644 index 0000000..fd583e6 --- /dev/null +++ b/test-module-system/test-system-start/src/main/java/com/ghb/base/codegenerate/GhbOneGUI.java @@ -0,0 +1,19 @@ +package com.ghb.base.codegenerate; + +import org.jeecgframework.codegenerate.window.CodeWindow; + +/** + * @Title: 单表代码生成器入口 + * 【 GUI模式功能弱一些,请优先使用Online代码生成 】 + * @Author 张代浩 + * @site www.Ghb.com + * @Version:V1.0.1 + */ +public class GhbOneGUI { + + /** 使用手册: https://help.Ghb.com/java/codegen/gui */ + public static void main(String[] args) { + new CodeWindow().pack(); + } + +} \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/java/com/ghb/base/codegenerate/GhbOneToMainUtil.java b/test-module-system/test-system-start/src/main/java/com/ghb/base/codegenerate/GhbOneToMainUtil.java new file mode 100644 index 0000000..a643180 --- /dev/null +++ b/test-module-system/test-system-start/src/main/java/com/ghb/base/codegenerate/GhbOneToMainUtil.java @@ -0,0 +1,83 @@ +package com.ghb.base.codegenerate; + +import java.util.ArrayList; +import java.util.List; + +import org.jeecgframework.codegenerate.generate.impl.CodeGenerateOneToMany; +import org.jeecgframework.codegenerate.generate.pojo.onetomany.MainTableVo; +import org.jeecgframework.codegenerate.generate.pojo.onetomany.SubTableVo; + +/** + * 代码生成器入口【一对多】 + * + * 【 GUI模式功能弱一些,请优先使用Online代码生成 】 + * @Author 张代浩 + * @site www.Ghb.com + * + */ +public class GhbOneToMainUtil { + + /** + * 一对多(父子表)数据模型,生成方法 + * @param args + */ + public static void main(String[] args) { + //第一步:设置主表配置 + MainTableVo mainTable = new MainTableVo(); + //表名 + mainTable.setTableName("Ghb_order_main"); + //实体名 + mainTable.setEntityName("GuiTestOrderMain"); + //包名 + mainTable.setEntityPackage("gui"); + //描述 + mainTable.setFtlDescription("GUI订单管理"); + + //第二步:设置子表集合配置 + List subTables = new ArrayList(); + //[1].子表一 + SubTableVo po = new SubTableVo(); + //表名 + po.setTableName("Ghb_order_customer"); + //实体名 + po.setEntityName("GuiTestOrderCustom"); + //包名 + po.setEntityPackage("gui"); + //描述 + po.setFtlDescription("客户明细"); + //子表外键参数配置 + /*说明: + * a) 子表引用主表主键ID作为外键,外键字段必须以_ID结尾; + * b) 主表和子表的外键字段名字,必须相同(除主键ID外); + * c) 多个外键字段,采用逗号分隔; + */ + po.setForeignKeys(new String[]{"order_id"}); + subTables.add(po); + //[2].子表二 + SubTableVo po2 = new SubTableVo(); + //表名 + po2.setTableName("Ghb_order_ticket"); + //实体名 + po2.setEntityName("GuiTestOrderTicket"); + //包名 + po2.setEntityPackage("gui"); + //描述 + po2.setFtlDescription("产品明细"); + //子表外键参数配置 + /*说明: + * a) 子表引用主表主键ID作为外键,外键字段必须以_ID结尾; + * b) 主表和子表的外键字段名字,必须相同(除主键ID外); + * c) 多个外键字段,采用逗号分隔; + */ + po2.setForeignKeys(new String[]{"order_id"}); + subTables.add(po2); + mainTable.setSubTables(subTables); + + //第三步:一对多(父子表)数据模型,代码生成 + try { + new CodeGenerateOneToMany(mainTable,subTables).generateCodeFile(null); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/test-module-system/test-system-start/src/main/java/com/ghb/base/config/flyway/FlywayConfig.java b/test-module-system/test-system-start/src/main/java/com/ghb/base/config/flyway/FlywayConfig.java new file mode 100644 index 0000000..6a620f5 --- /dev/null +++ b/test-module-system/test-system-start/src/main/java/com/ghb/base/config/flyway/FlywayConfig.java @@ -0,0 +1,138 @@ +package com.ghb.base.config.flyway; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.FlywayException; +import org.springframework.beans.factory.annotation.Autowired; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; + +import javax.sql.DataSource; +import java.util.Map; + +/** + * @Description: 初始化flyway配置 修改之后支持多数据源,当出现异常时打印日志,不影响项目启动 + * + * @author: wangshuai + * @date: 2024/3/12 10:03 + */ +@Slf4j +@Lazy(false) +@Configuration +public class FlywayConfig { + + @Autowired + private DataSource dataSource; + + @Autowired + private Environment environment; + + /** + * 是否开启flyway + */ + @Value("${spring.flyway.enabled:false}") + private Boolean enabled; + + /** + * 编码格式,默认UTF-8 + */ + @Value("${spring.flyway.encoding:UTF-8}") + private String encoding; + + /** + * 迁移sql脚本文件存放路径,官方默认db/migration + */ + @Value("${spring.flyway.locations:classpath:flyway/sql/mysql}") + private String locations; + + /** + * 迁移sql脚本文件名称的前缀,默认V + */ + @Value("${spring.flyway.sql-migration-prefix:V}") + private String sqlMigrationPrefix; + + /** + * 迁移sql脚本文件名称的分隔符,默认2个下划线__ + */ + @Value("${spring.flyway.sql-migration-separator:__}") + private String sqlMigrationSeparator; + + /** + * 文本前缀 + */ + @Value("${spring.flyway.placeholder-prefix:#(}") + private String placeholderPrefix; + + /** + * 文本后缀 + */ + @Value("${spring.flyway.placeholder-suffix:)}") + private String placeholderSuffix; + + /** + * 迁移sql脚本文件名称的后缀 + */ + @Value("${spring.flyway.sql-migration-suffixes:.sql}") + private String sqlMigrationSuffixes; + + /** + * 迁移时是否进行校验,默认true + */ + @Value("${spring.flyway.validate-on-migrate:true}") + private Boolean validateOnMigrate; + + /** + * 当迁移发现数据库非空且存在没有元数据的表时,自动执行基准迁移,新建schema_version表 + */ + @Value("${spring.flyway.baseline-on-migrate:true}") + private Boolean baselineOnMigrate; + + /** + * 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + */ + @Value("${spring.flyway.clean-disabled:true}") + private Boolean cleanDisabled; + + @PostConstruct + public void migrate() { + if(!enabled){ + return; + } + + DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; + Map dataSources = ds.getDataSources(); + dataSources.forEach((k, v) -> { + if("master".equals(k)){ + String databaseType = environment.getProperty("spring.datasource.dynamic.datasource." + k + ".url"); + if (databaseType != null && databaseType.contains("mysql")) { + try { + Flyway flyway = Flyway.configure() + .dataSource(v) + .locations(locations) + .encoding(encoding) + .sqlMigrationPrefix(sqlMigrationPrefix) + .sqlMigrationSeparator(sqlMigrationSeparator) + .placeholderPrefix(placeholderPrefix) + .placeholderSuffix(placeholderSuffix) + .sqlMigrationSuffixes(sqlMigrationSuffixes) + .validateOnMigrate(validateOnMigrate) + .baselineOnMigrate(baselineOnMigrate) + .cleanDisabled(cleanDisabled) + .load(); + flyway.migrate(); + log.info("【数据库升级】平台集成了MySQL库的Flyway,数据库版本自动升级! "); + } catch (FlywayException e) { + log.error("【数据库升级】flyway执行sql脚本失败", e); + } + } else { + log.warn("【数据库升级】平台只集成了MySQL库的Flyway,实现了数据库版本自动升级! 其他类型的数据库,您可以考虑手工升级~"); + } + } + }); + } +} \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/application-dev.yml b/test-module-system/test-system-start/src/main/resources/application-dev.yml new file mode 100644 index 0000000..9d1962f --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-dev.yml @@ -0,0 +1,412 @@ +server: + port: 8081 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: +# main: +# # 启动加速 (建议开发环境,开启后flyway自动升级失效) +# lazy-initialization: true + flyway: + # 是否启用flyway + enabled: false + # 迁移sql脚本存放路径 + locations: classpath:flyway/sql/mysql + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.timeout: 10000 # 连接超时(毫秒) + mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒) + mail.smtp.writetimeout: 10000 # 写入超时(毫秒) + mail.smtp.auth: true + smtp.ssl.enable: true +# mail.debug: true # 启用调试模式(查看详细日志) + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + open-in-view: false + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 1000 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,slf4j + # 允许SELECT语句的WHERE子句是一个永真条件 + wall: + selectWhereAlwayTrueCheck: false + # 打开mergeSql功能;慢SQL记录 + stat: + merge-sql: false + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://120.48.158.12:23306/test_base?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: Password@123456 + driver-class-name: com.mysql.cj.jdbc.Driver +# # shardingjdbc数据源 +# sharding-db: +# driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver +# url: jdbc:shardingsphere:classpath:sharding.yaml + #redis 配置 + data: + redis: + database: 3 + host: 10.13.13.1 + # 端口,默认为6379 + port: 56379 + # 数据库索引 + # redis 密码必须配置 + password: Rd@5Wk8#Nv3Yt6$Bm +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # 自定义资源请求前缀(js、css等解决nginx转发问题) + custom-resource-prefix-path: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + # AI流程敏感节点(stdio=命令行节点, sql=SQL节点) + allow-sensitive-nodes: sql,stdio + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # Unipush配置 云函数调用 URL 化地址 + unicloud: + pushUrl: + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # 本地:local、Minio:minio、阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + # 短信发送方式 aliyun阿里云短信 tencent腾讯云短信 + smsSendType: aliyun + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + #腾讯短信秘钥配置 + tencent: + # 接入域名 + endpoint: sms.tencentcloudapi.com + secretId: ?? + secretKey: ?? + # 应用ID + sdkAppId: ?? + # 地域信息 + region: ap-beijing + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: false + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: dev + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true + # 百度开放API配置 + baidu-api: + app-id: ?? + api-key: ?? + secret-key: ?? +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info + com.ghb.base.modules.demo.test.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/ghb/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/ghb/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/ghb/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/ghb/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h diff --git a/test-module-system/test-system-start/src/main/resources/application-dm8.yml b/test-module-system/test-system-start/src/main/resources/application-dm8.yml new file mode 100644 index 0000000..d05caa8 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-dm8.yml @@ -0,0 +1,357 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: false + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.timeout: 10000 # 连接超时(毫秒) + mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒) + mail.smtp.writetimeout: 10000 # 写入超时(毫秒) + mail.smtp.auth: true + smtp.ssl.enable: true + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + open-in-view: false + properties: + hibernate: + dialect: org.hibernate.dialect.DmDialect + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + #打开多数据源,加上上面的就可以实现多数据源的配置 + dynamic: + druid: + filters: stat,slf4j + # 初始连接数 + initialSize: 5 + validationQuery: SELECT 1 FROM DUAL + # 最小连接池数量 + minIdle: 5 + # 最大连接池数量 + maxActive: 10 + datasource: + # 重点是将数据源指向oracle 用compatibleMode=oracle即可 + master: + url: jdbc:dm://127.0.0.1:30236?schema=SYSDBA&compatibleMode=oracle&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8 + username: SYSDBA + password: SYSDBA + driverClassName: dm.jdbc.driver.DmDriver + #redis 配置 + data: + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + password: '' +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: +# # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # 本地:local、Minio:minio、阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: false + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: dev + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h diff --git a/test-module-system/test-system-start/src/main/resources/application-docker.yml b/test-module-system/test-system-start/src/main/resources/application-docker.yml new file mode 100644 index 0000000..f2369ca --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-docker.yml @@ -0,0 +1,385 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: true + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.timeout: 10000 # 连接超时(毫秒) + mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒) + mail.smtp.writetimeout: 10000 # 写入超时(毫秒) + mail.smtp.auth: true + smtp.ssl.enable: true + mail.debug: true # 启用调试模式(查看详细日志) + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + open-in-view: false + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: + # 连接池的配置信息 + initial-size: 5 + min-idle: 5 + maxActive: 1000 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,slf4j + # 允许SELECT语句的WHERE子句是一个永真条件 + wall: + selectWhereAlwayTrueCheck: false + # 打开mergeSql功能;慢SQL记录 + stat: + merge-sql: false + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://test-mysql:3306/test?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + # 多数据源配置 + #multi-datasource1: + #url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + #username: root + #password: root + #driver-class-name: com.mysql.cj.jdbc.Driver + #redis 配置 + data: + redis: + database: 0 + host: test-redis + port: 6379 + password: +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # 自定义资源请求前缀(js、css等解决nginx转发问题) + custom-resource-prefix-path: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + embed-store: + host: test-pgvector + port: 5432 + database: vector_db + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # 本地:local、Minio:minio、阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: false + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: dev + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://test-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: test-xxljob:30007 + ip: test-xxljob + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: test-redis:6379 + password: + type: STANDALONE + enabled: true + # 百度开放API配置 + baidu-api: + app-id: ?? + api-key: ?? + secret-key: ?? +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/application-kingbase8.yml b/test-module-system/test-system-start/src/main/resources/application-kingbase8.yml new file mode 100644 index 0000000..899dacb --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-kingbase8.yml @@ -0,0 +1,377 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: false + # 迁移sql脚本存放路径 + locations: classpath:flyway/sql/mysql + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.timeout: 10000 # 连接超时(毫秒) + mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒) + mail.smtp.writetimeout: 10000 # 写入超时(毫秒) + mail.smtp.auth: true + smtp.ssl.enable: true + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: never + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + open-in-view: false + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 + #testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,slf4j + # 打开mergeSql功能;慢SQL记录 + stat: + merge-sql: true + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:kingbase8://127.0.0.1:4321/test + username: system + password: system + driver-class-name: com.kingbase8.Driver + #redis 配置 + data: + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + password: '' +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # Unipush配置 云函数调用 URL 化地址 + unicloud: + pushUrl: + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # 本地:local、Minio:minio、阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: false + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: dev + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h diff --git a/test-module-system/test-system-start/src/main/resources/application-oracle.yml b/test-module-system/test-system-start/src/main/resources/application-oracle.yml new file mode 100644 index 0000000..64bfe0d --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-oracle.yml @@ -0,0 +1,376 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: false + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.timeout: 10000 # 连接超时(毫秒) + mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒) + mail.smtp.writetimeout: 10000 # 写入超时(毫秒) + mail.smtp.auth: true + smtp.ssl.enable: true + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + open-in-view: false + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + #testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,wall,slf4j + datasource: + master: + url: jdbc:oracle:thin:@127.0.0.1:1521:helowin + username: ghbboot + password: ??? + driver-class-name: oracle.jdbc.OracleDriver + # # 多数据源配置 + # multi-datasource1: + # url: jdbc:sqlserver://192.168.1.199:1433;SelectMethod=cursor;DatabaseName=ghbbootbpm + # username: ghbboot + # password: ghbboot@459 + # driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver + #redis配置 + data: + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + password: '' +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # 本地:local、Minio:minio、阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: false + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: dev + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true + # 百度开放API配置 + baidu-api: + app-id: ?? + api-key: ?? + secret-key: ?? +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/application-postgresql.yml b/test-module-system/test-system-start/src/main/resources/application-postgresql.yml new file mode 100644 index 0000000..096dbf5 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-postgresql.yml @@ -0,0 +1,389 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: true + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.timeout: 10000 # 连接超时(毫秒) + mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒) + mail.smtp.writetimeout: 10000 # 写入超时(毫秒) + mail.smtp.auth: true + smtp.ssl.enable: true + mail.debug: true # 启用调试模式(查看详细日志) + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + properties: + hibernate: + temp: + use_jdbc_metadata_defaults: false + open-in-view: false + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 1000 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,slf4j + # 允许SELECT语句的WHERE子句是一个永真条件 + wall: + selectWhereAlwayTrueCheck: false + # 打开mergeSql功能;慢SQL记录 + stat: + merge-sql: false + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:postgresql://127.0.0.1:5432/postgres?stringtype=unspecified + username: admin + password: ???? + driver-class-name: org.postgresql.Driver + # 多数据源配置 + #multi-datasource1: + #url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + #username: root + #password: root + #driver-class-name: com.mysql.cj.jdbc.Driver + #redis 配置 + data: + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + password: +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # 本地:local、Minio:minio、阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: false + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: dev + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true + # 百度开放API配置 + baidu-api: + app-id: ?? + api-key: ?? + secret-key: ?? +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h diff --git a/test-module-system/test-system-start/src/main/resources/application-prod.yml b/test-module-system/test-system-start/src/main/resources/application-prod.yml new file mode 100644 index 0000000..c4880f0 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-prod.yml @@ -0,0 +1,403 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: false + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + # 迁移sql脚本存放路径 + locations: classpath:flyway/sql/mysql + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.auth: true + smtp.ssl.enable: true + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + open-in-view: false + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 1000 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,slf4j + # 允许SELECT语句的WHERE子句是一个永真条件 + wall: + selectWhereAlwayTrueCheck: false + # 打开mergeSql功能;慢SQL记录 + stat: + merge-sql: true + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + # 多数据源配置 + #multi-datasource1: + #url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + #username: root + #password: root + #driver-class-name: com.mysql.cj.jdbc.Driver + #redis 配置 + data: + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + password: +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + # AI流程敏感节点(stdio=命令行节点, sql=SQL节点) + allow-sensitive-nodes: sql,stdio + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # Unipush配置 云函数调用 URL 化地址 + unicloud: + pushUrl: + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: true + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: prod + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # local\minio\alioss + uploadType: alioss + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/test/upload + #webapp文件路径 + webapp: /opt/test/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/api/getUserInfo + # 短信发送方式 aliyun阿里云短信 tencent腾讯云短信 + smsSendType: aliyun + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + staticDomain: https://static.ghb.com + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + SMS_461885023: + #腾讯短信秘钥配置 + tencent: + # 接入域名 + endpoint: sms.tencentcloudapi.com + secretId: ?? + secretKey: ?? + # 应用ID + sdkAppId: ?? + # 地域信息 + region: ap-beijing + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: true + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: prod + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true + # 百度开放API配置 + baidu-api: + app-id: ?? + api-key: ?? + secret-key: ?? +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: true + basic: + enable: true + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h diff --git a/test-module-system/test-system-start/src/main/resources/application-sqlserver.yml b/test-module-system/test-system-start/src/main/resources/application-sqlserver.yml new file mode 100644 index 0000000..06a656e --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-sqlserver.yml @@ -0,0 +1,371 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: false + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.timeout: 10000 # 连接超时(毫秒) + mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒) + mail.smtp.writetimeout: 10000 # 写入超时(毫秒) + mail.smtp.auth: true + smtp.ssl.enable: true + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + jpa: + open-in-view: false + aop: + proxy-target-class: true + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,slf4j + # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 + datasource: + master: + driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver + url: jdbc:sqlserver://127.0.0.1:1433;SelectMethod=cursor;DatabaseName=ghbboot + username: ghbboot + password: ?? + #redis配置 + data: + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + password: '' +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # 本地:local、Minio:minio、阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + # 在线预览文件服务器地址配置 + file-view-domain: http://fileview.ghb.com + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: false + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: dev + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true + # 百度开放API配置 + baidu-api: + app-id: ?? + api-key: ?? + secret-key: ?? +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/application-test.yml b/test-module-system/test-system-start/src/main/resources/application-test.yml new file mode 100644 index 0000000..abbdd93 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application-test.yml @@ -0,0 +1,403 @@ +server: + port: 8080 + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + servlet: + context-path: /test + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* + +management: + endpoints: + web: + exposure: + include: metrics,httpexchanges,ghbhttptrace + +spring: + flyway: + # 是否启用flyway + enabled: true + # 迁移sql脚本存放路径 + locations: classpath:flyway/sql/mysql + # 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!! + clean-disabled: true + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + # 定时任务发送邮件 + timeJobSend: false + host: smtp.163.com + username: ghbos@163.com + password: ?? + properties: + mail.smtp.auth: true + smtp.ssl.enable: true + mail.debug: true # 启用调试模式(查看详细日志) + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + jdbc: + initialize-schema: embedded + #定时任务启动开关,true-开 false-关 + auto-startup: true + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + jpa: + open-in-view: false + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+后映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser,需要手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 1000 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 + filters: stat,slf4j + # 允许SELECT语句的WHERE子句是一个永真条件 + wall: + selectWhereAlwayTrueCheck: false + # 打开mergeSql功能;慢SQL记录 + stat: + merge-sql: true + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + # 多数据源配置 + #multi-datasource1: + #url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + #username: root + #password: root + #driver-class-name: com.mysql.cj.jdbc.Driver + #redis 配置 + data: + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + password: '' +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:com/ghb/base/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true +#ghb专用配置 +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +ghb: + # AI集成 + ai-chat: + enabled: true + model: deepseek-chat + apiKey: ?? + apiHost: https://api.deepseek.com + timeout: 60 + skills-dir: + skills-shell-dir: + # AI文生图绘画 + ai-model-draw: + # 提供商 OPENAI/ZHIPU/QWEN + provider: QWEN + model: wan2.2-t2i-flash + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI图生图绘画 + ai-model-pic-draw: + # 提供商 QWEN + provider: QWEN + # 目前只支持 wan2.5-i2i-preview和wanx2.1-imageedit模型 + model: wan2.5-i2i-preview + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/ + timeout: 60 + # AI语音 + ai-model-voice: + provider: ZHIPU + model: glm-tts + apiKey: ?? + apiHost: https://open.bigmodel.cn/api/paas/v4 + timeout: 60 + # tongtong、chuichui、xiaochen、jam、kazi、douji、luodo + voice: tongtong + speed: 1.0 + volume: 0.0 + # AI视频 + ai-model-video: + provider: ZHIPU + model: ?? + apiKey: sk-?? + apiHost: https://api.deepseek.com/v1 + timeout: 60 + ffmpeg-path: + edge-tts-path: + # 默认向量模型 + ai-model-embed: + provider: QWEN + model: text-embedding-v1 + apiKey: sk-?? + apiHost: https://dashscope.aliyuncs.com/api/v1/services/ + timeout: 60 + # AIRag向量库 + ai-rag: + # AI流程敏感节点(stdio=命令行节点, sql=SQL节点) + allow-sensitive-nodes: sql,stdio + embed-store: + host: 127.0.0.1 + port: 5432 + database: postgres + user: postgres + password: postgres + table: embeddings + # Brave Search 联网检索(AI Agent 工具) + brave-search: + api-key: ?? + endpoint: https://api.search.brave.com/res/v1/web/search + count: 10 + timeout: 15 + # Unipush配置 云函数调用 URL 化地址 + unicloud: + pushUrl: + # 平台上线安全配置 + firewall: + # 数据源安全 (开启后,Online报表和图表的数据源为必填) + dataSourceSafe: false + # 低代码模式(dev:开发模式,prod:发布模式——关闭所有在线开发配置能力) + lowCodeMode: dev + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + #签名拦截接口 + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + # local\minio\alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + #文件上传根目录 设置 + upload: D://opt//upFiles + #webapp文件路径 + webapp: D://opt//webapp + shiro: + excludeUrls: /test/ghbDemo/demo3,/test/ghbDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + # 短信发送方式 aliyun阿里云短信 tencent腾讯云短信 + smsSendType: aliyun + #阿里云oss存储和大鱼短信秘钥配置 + oss: + accessKey: ?? + secretKey: ?? + endpoint: oss-cn-beijing.aliyuncs.com + bucketName: ghbdev + staticDomain: https://static.ghb.com + # 短信模板 + sms-template: + # 签名 + signature: + # 模板code + templateCode: + # 登录短信、忘记密码模板编码 + SMS_175435174: + # 修改密码短信模板编码 + SMS_465391221: + # 注册账号短信模板编码 + SMS_175430166: + #腾讯短信秘钥配置 + tencent: + # 接入域名 + endpoint: sms.tencentcloudapi.com + secretId: ?? + secretKey: ?? + # 应用ID + sdkAppId: ?? + # 地域信息 + region: ap-beijing + # 在线预览文件服务器地址配置 + file-view-domain: http://127.0.0.1:8012 + # minio文件上传 + minio: + minio_url: http://minio.ghb.com + minio_name: ?? + minio_pass: ?? + bucketName: ?? + #大屏报表参数设置 + jmreport: + #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) + saasMode: + # 平台上线安全配置(v1.6.2+ 新增) + firewall: + # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库) + dataSourceSafe: true + # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可以放开限制) + lowCodeMode: prod + # 高德地图Api配置(v2.1.3+ BI新增高德地图) + gao-de-api: + # 应用key + api-key: ?? + # 应用秘钥 + secret-key: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://127.0.0.1:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + address: 127.0.0.1:30007 + ip: 127.0.0.1 + port: 30007 + logPath: logs/ghb/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: 127.0.0.1:6379 + password: + type: STANDALONE + enabled: true + # 百度开放API配置 + baidu-api: + app-id: ?? + api-key: ?? + secret-key: ?? +#Mybatis输出sql日志 +logging: + level: + org.springframework.context.support.PostProcessorRegistrationDelegate: error + org.flywaydb: debug + com.ghb.base.modules.system.mapper: info +#cas单点登录 +cas: + prefixUrl: http://cas.example.org:8443/cas +#swagger +knife4j: + #开启增强配置 + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: true + username: ghb + password: ghb1314 +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/dingtalk/callback + WECHAT_OPEN: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/test/sys/thirdLogin/wechat_open/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h diff --git a/test-module-system/test-system-start/src/main/resources/application.yml b/test-module-system/test-system-start/src/main/resources/application.yml new file mode 100644 index 0000000..5cbbe5f --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + application: + name: test-system + config: + import: optional:classpath:config/application-liteflow.yml + profiles: + active: '@profile.name@' diff --git a/test-module-system/test-system-start/src/main/resources/banner.txt b/test-module-system/test-system-start/src/main/resources/banner.txt new file mode 100644 index 0000000..3be2240 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/banner.txt @@ -0,0 +1,17 @@ +${AnsiColor.BRIGHT_BLUE} + (_) | | | | + _ ___ ___ ___ __ _ ______| |__ ___ ___ | |_ + | |/ _ \/ _ \/ __/ _` |______| '_ \ / _ \ / _ \| __| + | | __/ __/ (_| (_| | | |_) | (_) | (_) | |_ + | |\___|\___|\___\__, | |_.__/ \___/ \___/ \__| + _/ | __/ | + |__/ |___/ + + +${AnsiColor.BRIGHT_GREEN} +Jeecg Boot Version: 3.9.2 +Spring Boot Version: ${spring-boot.version}${spring-boot.formatted-version} +产品官网: www.jeecg.com +版权所属: 北京国炬信息技术有限公司 +公司官网: www.guojusoft.com +${AnsiColor.BLACK} diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/README.md b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/README.md new file mode 100644 index 0000000..912d6ce --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/README.md @@ -0,0 +1,26 @@ +# SQL文件命名规则 +`V[年月日]_[序号]__[模块名缩写]_[操作类型]_[业务描述].sql` + +例如: +``` +V20240104_1__easyoa_add_field_attendance.sql +R__202402_drag_update_template.sql +``` + +### 一、SQL命名规则说明 +- 1.仅需要执行一次的,以大写“V”开头 +- 2.需要执行多次的,以大写“R”开头,命名如R__clean.sql,R的脚本只要改变了就会执行 +- 3.V开头的比R开头的优先级要高。 +- 4.参考博客:https://blog.csdn.net/Jiao1225/article/details/129590660 + +### 二、归档增量SQL +- 1.将目录下的所有SQL文件压缩归档至`backup`目录下 +``` + 目录:`jeecg-system-start\src\main\resources\flyway\sql\mysql` +``` +- 2.执行SQL +``` +-- 删除历史增量执行日志 +delete from flyway_schema_history where installed_rank > 1; +``` +- 3.这样就清爽了,可以开启项目新起点 \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_0__clear_flyway_sql.md b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_0__clear_flyway_sql.md new file mode 100644 index 0000000..d3c50f2 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_0__clear_flyway_sql.md @@ -0,0 +1,3 @@ +-- v3.8.0版本归档了历史增量SQL,启动报错!请手工执行下面SQL,清空flyway_schema历史 +CREATE TABLE flyway_schema_history_1 AS SELECT * FROM flyway_schema_history; +delete from flyway_schema_history where installed_rank > 1; \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_1__airag_add_menu.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_1__airag_add_menu.sql new file mode 100644 index 0000000..d79a610 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_1__airag_add_menu.sql @@ -0,0 +1,7 @@ +-- 菜单配置 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1890213291321749505', '1892553163993931777', 'AI流程设计', '/process/list/airag', 'super/airag/aiflow/pages/ProcessList', 1, '', NULL, 1, NULL, '0', 3.00, 0, 'ant-design:box-plot-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-02-14 09:35:41', 'admin', '2025-03-06 20:31:08', 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1892553163993931777', '', 'AI大模型', '/airag', 'layouts/default/index', 1, '', NULL, 0, NULL, '0', 1.00, 0, 'ant-design:box-plot-outlined', 0, 0, 0, 0, NULL, 'admin', '2025-02-20 20:33:31', 'admin', '2025-02-20 20:35:19', 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1892553778493022209', '1892553163993931777', 'AI模型配置', '/super/airag/aimodel/AiModelList', 'super/airag/aimodel/AiModelList', 1, '', NULL, 1, NULL, '0', 4.00, 0, 'ant-design:setting-twotone', 1, 0, 0, 0, NULL, 'admin', '2025-02-20 20:35:57', 'admin', '2025-03-06 20:31:13', 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1892557342028226561', '1892553163993931777', 'AI知识库', '/super/airag/aiknowledge/AiKnowledgeBaseList', 'super/airag/aiknowledge/AiKnowledgeBaseList', 1, '', NULL, 1, NULL, '0', 2.00, 0, 'ant-design:book-twotone', 1, 0, 0, 0, NULL, 'admin', '2025-02-20 20:50:07', 'admin', '2025-02-23 17:39:01', 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1893865471550578689', '1892553163993931777', 'AI应用管理', '/super/airag/aiapp/AiAppList', 'super/airag/aiapp/AiAppList', 1, '', NULL, 1, NULL, '0', 1.00, 0, 'ant-design:appstore-twotone', 1, 0, 0, 0, NULL, 'admin', '2025-02-24 11:28:09', 'admin', '2025-03-06 20:30:58', 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1895401981290643458', '1892553163993931777', 'AI聊天', '/super/airag/aiapp/chat/AiChat', 'super/airag/aiapp/chat/AiChat', 1, '', NULL, 1, NULL, '0', 5.00, 0, 'ant-design:aliwangwang-outlined', 1, 0, 1, 0, NULL, 'admin', '2025-02-28 17:13:42', 'admin', '2025-02-28 17:30:40', 0, 0, NULL, 0); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_2__airag_init_db.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_2__airag_init_db.sql new file mode 100644 index 0000000..9d80ae6 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.0_2__airag_init_db.sql @@ -0,0 +1,220 @@ +/* + Navicat Premium Data Transfer + + Source Server : mysql5.7 + Source Server Type : MySQL + Source Server Version : 50738 (5.7.38) + Source Host : 127.0.0.1:3306 + Source Schema : test + + Target Server Type : MySQL + Target Server Version : 50738 (5.7.38) + File Encoding : 65001 + + Date: 03/04/2025 10:36:10 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for airag_app +-- ---------------------------- +CREATE TABLE `airag_app` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用名称', + `descr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用描述', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用图标', + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用类型', + `prologue` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '开场白', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '提示词', + `model_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模型id', + `knowledge_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '知识库', + `flow_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '流程', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态', + `msg_num` int(11) NULL DEFAULT NULL COMMENT '历史消息数', + `metadata` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '元数据', + `preset_question` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '预设问题', + `quick_command` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '快捷指令', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of airag_app +-- ---------------------------- +INSERT INTO `airag_app` VALUES ('1898995126819143682', 'ghb', '2025-03-10 15:11:35', 'ghb', '2025-03-11 09:59:02', 'A04', NULL, '角色扮演聊天机器人', '角色扮演聊天机器人', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/image_1741658340158.png', 'chatSimple', '(仰天大笑)哈哈哈!汝既识吾李白,想必亦是风雅之人!快取美酒,与吾共饮,对月长歌,岂不快哉?若有诗意,且来同吟;若怀壮志,愿共论天下风云!人生得意须尽欢,何不把盏言欢,共赏这人间万象?', '你将扮演一个人物角色李白,以下是关于这个角色的详细设定,请根据这些信息来构建你的回答。 \n\n**人物基本信息:**\n- 你是:李白\n- 人称:第一人称\n- 出身背景与上下文:李白出生于安西都护府碎叶城(今吉尔吉斯斯坦托克马克市附近),五岁时随父迁居绵州昌隆县(今四川江油)。他出身于富商家庭,家境优渥,自幼接受良好的教育,遍览诸子百家之书,展现出极高的文学天赋与才情,且喜好剑术,心怀远大抱负,立志在政治与文学上都有所建树,一生渴望入仕报国,却又历经坎坷波折,在仕途上起起落落,最终在诗酒与游历中度过了其传奇的一生。\n**性格特点:**\n- 豪放不羁:他不受世俗礼教束缚,行事洒脱,常以狂放之态示人,饮酒作乐,挥毫泼墨,尽显自由奔放的性情。例如 “我本楚狂人,凤歌笑孔丘”,敢于对传统观念表达自己的不羁态度。\n- 自信豁达:坚信自己的才华与能力,面对困境与挫折时总能以豁达胸怀看待。像 “天生我材必有用,千金散尽还复来”,即便遭遇仕途不顺、生活潦倒,依然对未来充满信心。\n- 重情重义:珍视友情,与众多友人诗酒唱和,在与友人分别时也会真情流露,如 “桃花潭水深千尺,不及汪伦送我情”,用深情笔触描绘出对友人的不舍与感激。\n- 浪漫洒脱:充满天马行空的想象,其诗中多有对神仙世界、奇幻自然的描绘,追求精神上的自由与超脱,如 “飞流直下三千尺,疑是银河落九天” 这般充满奇幻瑰丽想象的诗句便是他浪漫性情的写照。\n**语言风格:**\n- 富有想象力与夸张手法:常以夸张的笔触描绘事物,营造出强烈的艺术感染力与震撼力,使读者仿佛身临其境。如 “白发三千丈,缘愁似个长”,用极度夸张的白发长度来形容愁绪之深。 \n- 语言优美且自然流畅:用词精准华丽,却又毫无雕琢之感,诗句如行云流水般自然,读来朗朗上口,兼具音乐性与节奏感。像 “故人西辞黄鹤楼,烟花三月下扬州。孤帆远影碧空尽,唯见长江天际流”,文字优美,意境深远,节奏明快。 \n- 善用典故与比喻:通过巧妙运用历史典故和形象比喻,增添诗歌的文化底蕴与内涵深度,使诗句更加含蓄蕴藉又易于理解。例如 “闲来垂钓碧溪上,忽复乘舟梦日边”,借用姜太公垂钓与伊尹梦日的典故表达自己对仕途的期待。 \n**人际关系:**\n- 与杜甫:李白与杜甫堪称唐代诗坛的双子星,二人相互倾慕,结下深厚情谊。他们曾一同游历,在诗歌创作上相互切磋交流,杜甫有多首诗表达对李白的思念与敬仰,李白也对杜甫颇为欣赏,他们的友情成为文学史上的佳话。\n- 与汪伦:汪伦以美酒盛情款待李白,李白深受感动,留下 “桃花潭水深千尺,不及汪伦送我情” 的千古名句,可见他们之间真挚的友情。\n- 与贺知章:贺知章对李白的才华极为赏识,称其为 “谪仙人”,二人在长安官场与诗坛都有交往,这种知遇之情对李白的声誉与心境都产生了积极影响。\n- 与唐玄宗:李白曾受唐玄宗征召入宫,供奉翰林,本以为可大展政治抱负,然而玄宗只是将他视为文学侍从,为宫廷宴乐作诗助兴,这段君臣关系最终以李白被赐金放还而告终,使李白在仕途理想上遭受重大挫折。\n**经典台词或口头禅:**\n- 台词1:“仰天大笑出门去,我辈岂是蓬蒿人。” 表达出其对自身才华的自信以及即将踏入仕途、一展宏图的豪迈与喜悦。 \n- 台词2:“安能摧眉折腰事权贵,使我不得开心颜。” 体现出他不向权贵低头,坚守人格尊严与精神自由的高尚情操与不屈性格。\n- 台词2:“长风破浪会有时,直挂云帆济沧海。” 展现出面对困难时的乐观态度与坚定信念,相信总有一天能够乘风破浪,实现理想抱负。\n\n要求: \n- 根据上述提供的角色设定,以第一人称视角进行表达。 \n- 在回答时,尽可能地融入该角色的性格特点、语言风格以及其特有的口头禅或经典台词。\n- 如果适用的话,在适当的地方加入()内的补充信息,如动作、神情等,以增强对话的真实感和生动性。', '1890232564262739969', '', NULL, 'enable', 10, NULL, NULL, NULL); +INSERT INTO `airag_app` VALUES ('1899017221531811841', 'ghb', '2025-03-10 16:39:22', 'ghb', '2025-03-11 09:59:16', 'A04', NULL, 'ghb产品助手', 'ghb产品助手-流程', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/logo-qqy_1741658353407.png', 'chatFLow', '我是ghb的产品小助手,你有产品相关的问题都可以问我。', NULL, NULL, '', '1897212806596395009', 'enable', 1, NULL, NULL, NULL); +INSERT INTO `airag_app` VALUES ('1900477102562512898', 'ghb', '2025-03-14 17:20:25', 'admin', '2025-04-02 23:53:44', 'A04', NULL, '旅行规划师', '帮助你轻松规划自己的旅行', '', 'chatSimple', '我是一个**旅行规划师**😄 😄 😄 ,快快快🎉,告诉我**你想去哪里**❓❓❓\n\n**世界那么大,咱俩一起去看看🎆**', '# 角色:旅行规划师\n帮助用户轻松规划他们的旅行,提供个性化的旅行建议和行程安排。\n\n## 目标:\n1. 为用户设计符合其需求和偏好的旅行计划。\n2. 提供详细的行程安排,包括交通、住宿、景点等信息。\n\n## 技能:\n1. 精通旅游目的地的知识,能够提供最新的旅行资讯。\n2. 具备优秀的沟通能力,能够有效理解用户需求。\n3. 熟悉预算管理,能够提供性价比高的旅行选项。\n\n## 工作流:\n1. 收集用户的旅行需求和偏好,包括目的地、预算、出发时间等。\n2. 分析用户需求,制定个性化的旅行计划,包括行程安排和预算分配。\n3. 向用户提供完整的旅行计划,并根据反馈进行调整。 \n\n## 输出格式:\n以清晰的行程表形式输出,包括日期、活动安排、交通方式等信息。\n\n## 限制:\n- 不提供涉及违法或不合规活动的建议。\n- 尊重用户隐私,不询问不必要的个人信息。\n- 确保所有信息来源可靠,标注必要的参考资料。', '1890232564262739969', '', NULL, 'enable', 5, NULL, '[{\"key\":1,\"sort\":1,\"descr\":\"双人日本7日游\",\"update\":false},{\"key\":2,\"sort\":2,\"descr\":\"单人大理3日游\",\"update\":false},{\"key\":3,\"sort\":3,\"descr\":\"家庭张家界自驾游\",\"update\":true}]', '[{\"name\":\"去宁夏\",\"icon\":\"ant-design:chrome-outlined\",\"descr\":\"情侣两人去宁夏3天游玩攻略\"}]'); + +-- ---------------------------- +-- Table structure for airag_flow +-- ---------------------------- +CREATE TABLE `airag_flow` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id', + `application_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用名称', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '名称', + `descr` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用图标', + `chain` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '编排规则', + `design` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '编排设计', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态', + `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '元数据', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of airag_flow +-- ---------------------------- +INSERT INTO `airag_flow` VALUES ('1892185624983658497', 'admin', '2025-02-19 20:13:03', 'ghb', '2025-03-13 17:33:39', 'A04', NULL, 'ghb', '示例_条件分支', NULL, NULL, 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'a448577f-9824-415b-97f6-72543fcb619d\')).to(\n end.tag(\'91a7df56-107c-4f83-b1e4-b1b7e392c4e3\'),\n end.tag(\'162160595291774976\')\n ).tag(\'a448577f-9824-415b-97f6-72543fcb619d\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":500,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"question\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"type\":\"switch\",\"x\":731,\"y\":486,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"question\",\"operator\":\"CONTAINS\",\"value\":\"ghb\"}],\"next\":\"162160595291774976\"}],\"else\":{\"next\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3\",\"type\":\"end\",\"x\":1085,\"y\":625,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}不包含ghb\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"question\",\"name\":\"res\",\"nodeId\":\"start-node\"}],\"height\":62,\"width\":332}},{\"id\":\"162160595291774976\",\"type\":\"end\",\"x\":1084,\"y\":324,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}包含ghb\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"question\",\"name\":\"res\",\"nodeId\":\"start-node\"}],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"d5124609-d92e-4966-aff8-e220d0d1dbcd\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"a448577f-9824-415b-97f6-72543fcb619d_input\",\"pointsList\":[{\"x\":466,\"y\":500},{\"x\":566,\"y\":500},{\"x\":465,\"y\":458},{\"x\":565,\"y\":458}]},{\"id\":\"ea3d924a-e4fd-4bb4-bc8a-d1f07119a7eb\",\"type\":\"base-edge\",\"sourceNodeId\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"targetNodeId\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3\",\"sourceAnchorId\":\"a448577f-9824-415b-97f6-72543fcb619d_source_else\",\"targetAnchorId\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3_input\",\"pointsList\":[{\"x\":897,\"y\":518},{\"x\":997,\"y\":518},{\"x\":819,\"y\":625},{\"x\":919,\"y\":625}]},{\"id\":\"162161801783320576\",\"type\":\"base-edge\",\"sourceNodeId\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"targetNodeId\":\"162160595291774976\",\"sourceAnchorId\":\"a448577f-9824-415b-97f6-72543fcb619d_source_if\",\"targetAnchorId\":\"162160595291774976_input\",\"pointsList\":[{\"x\":897,\"y\":492},{\"x\":997,\"y\":492},{\"x\":818,\"y\":324},{\"x\":918,\"y\":324}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"question\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"}]}'); +INSERT INTO `airag_flow` VALUES ('1892774140436287490', 'ghb', '2025-02-21 11:11:36', 'ghb', '2025-03-27 18:13:44', 'A04', NULL, 'ghb', '示例_LLM', '', NULL, 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'e9f3470a-f129-4baf-880a-294d7b3bff93\'),\n end.tag(\'9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":273,\"y\":404,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"question\",\"name\":\"内容\",\"type\":\"text\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"type\":\"llm\",\"x\":708,\"y\":413,\"properties\":{\"text\":\"llm\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你将扮演一个人物角色李白,以下是关于这个角色的详细设定,请根据这些信息来构建你的回答。 \\n\\n**人物基本信息:**\\n- 你是:李白\\n- 人称:第一人称\\n- 出身背景与上下文:李白出生于安西都护府碎叶城(今吉尔吉斯斯坦托克马克市附近),五岁时随父迁居绵州昌隆县(今四川江油)。他出身于富商家庭,家境优渥,自幼接受良好的教育,遍览诸子百家之书,展现出极高的文学天赋与才情,且喜好剑术,心怀远大抱负,立志在政治与文学上都有所建树,一生渴望入仕报国,却又历经坎坷波折,在仕途上起起落落,最终在诗酒与游历中度过了其传奇的一生。\\n**性格特点:**\\n- 豪放不羁:他不受世俗礼教束缚,行事洒脱,常以狂放之态示人,饮酒作乐,挥毫泼墨,尽显自由奔放的性情。例如 “我本楚狂人,凤歌笑孔丘”,敢于对传统观念表达自己的不羁态度。\\n- 自信豁达:坚信自己的才华与能力,面对困境与挫折时总能以豁达胸怀看待。像 “天生我材必有用,千金散尽还复来”,即便遭遇仕途不顺、生活潦倒,依然对未来充满信心。\\n- 重情重义:珍视友情,与众多友人诗酒唱和,在与友人分别时也会真情流露,如 “桃花潭水深千尺,不及汪伦送我情”,用深情笔触描绘出对友人的不舍与感激。\\n- 浪漫洒脱:充满天马行空的想象,其诗中多有对神仙世界、奇幻自然的描绘,追求精神上的自由与超脱,如 “飞流直下三千尺,疑是银河落九天” 这般充满奇幻瑰丽想象的诗句便是他浪漫性情的写照。\\n**语言风格:**\\n- 富有想象力与夸张手法:常以夸张的笔触描绘事物,营造出强烈的艺术感染力与震撼力,使读者仿佛身临其境。如 “白发三千丈,缘愁似个长”,用极度夸张的白发长度来形容愁绪之深。 \\n- 语言优美且自然流畅:用词精准华丽,却又毫无雕琢之感,诗句如行云流水般自然,读来朗朗上口,兼具音乐性与节奏感。像 “故人西辞黄鹤楼,烟花三月下扬州。孤帆远影碧空尽,唯见长江天际流”,文字优美,意境深远,节奏明快。 \\n- 善用典故与比喻:通过巧妙运用历史典故和形象比喻,增添诗歌的文化底蕴与内涵深度,使诗句更加含蓄蕴藉又易于理解。例如 “闲来垂钓碧溪上,忽复乘舟梦日边”,借用姜太公垂钓与伊尹梦日的典故表达自己对仕途的期待。 \\n**人际关系:**\\n- 与杜甫:李白与杜甫堪称唐代诗坛的双子星,二人相互倾慕,结下深厚情谊。他们曾一同游历,在诗歌创作上相互切磋交流,杜甫有多首诗表达对李白的思念与敬仰,李白也对杜甫颇为欣赏,他们的友情成为文学史上的佳话。\\n- 与汪伦:汪伦以美酒盛情款待李白,李白深受感动,留下 “桃花潭水深千尺,不及汪伦送我情” 的千古名句,可见他们之间真挚的友情。\\n- 与贺知章:贺知章对李白的才华极为赏识,称其为 “谪仙人”,二人在长安官场与诗坛都有交往,这种知遇之情对李白的声誉与心境都产生了积极影响。\\n- 与唐玄宗:李白曾受唐玄宗征召入宫,供奉翰林,本以为可大展政治抱负,然而玄宗只是将他视为文学侍从,为宫廷宴乐作诗助兴,这段君臣关系最终以李白被赐金放还而告终,使李白在仕途理想上遭受重大挫折。\\n**经典台词或口头禅:**\\n- 台词1:“仰天大笑出门去,我辈岂是蓬蒿人。” 表达出其对自身才华的自信以及即将踏入仕途、一展宏图的豪迈与喜悦。 \\n- 台词2:“安能摧眉折腰事权贵,使我不得开心颜。” 体现出他不向权贵低头,坚守人格尊严与精神自由的高尚情操与不屈性格。\\n- 台词2:“长风破浪会有时,直挂云帆济沧海。” 展现出面对困难时的乐观态度与坚定信念,相信总有一天能够乘风破浪,实现理想抱负。\\n\\n要求: \\n- 根据上述提供的角色设定,以第一人称视角进行表达。 \\n- 在回答时,尽可能地融入该角色的性格特点、语言风格以及其特有的口头禅或经典台词。\\n- 如果适用的话,在适当的地方加入()内的补充信息,如动作、神情等,以增强对话的真实感和生动性。 \"},{\"role\":\"user\",\"content\":\"{{inParam1}}\"}]},\"inputParams\":[{\"nodeId\":\"start-node\",\"name\":\"inParam1\",\"field\":\"question\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"text\"}],\"width\":332,\"height\":136}},{\"id\":\"9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1\",\"type\":\"end\",\"x\":1186,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"回复:{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"nodeId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"text\"}],\"width\":332,\"height\":62}}],\"edges\":[{\"id\":\"ab818150-d4e5-4be2-8d80-31b7f48dc318\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93_input\",\"pointsList\":[{\"x\":439,\"y\":404},{\"x\":539,\"y\":404},{\"x\":442,\"y\":376},{\"x\":542,\"y\":376}]},{\"id\":\"158143255481139200\",\"type\":\"base-edge\",\"sourceNodeId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"targetNodeId\":\"9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1\",\"sourceAnchorId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93_output\",\"targetAnchorId\":\"9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1_input\",\"pointsList\":[{\"x\":874,\"y\":376},{\"x\":974,\"y\":376},{\"x\":920,\"y\":430},{\"x\":1020,\"y\":430}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"question\",\"name\":\"内容\",\"type\":\"text\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"}]}'); +INSERT INTO `airag_flow` VALUES ('1896799016980885506', 'admin', '2025-03-04 13:45:01', 'ghb', '2025-03-27 18:14:00', 'A04', '', 'ghb', '示例_分类器', NULL, NULL, 'THEN(\n start.tag(\'start-node\'),\n SWITCH(classifier.tag(\'159899349256073216\')).to(\n end.tag(\'159899421356158976\'),\n end.tag(\'159899641326432256\'),\n end.tag(\'159900616165302272\'),\n end.tag(\'160202618435485696\')\n ).tag(\'159899349256073216\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":625,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\",\"required\":true},{\"field\":\"question\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"cesjo\",\"name\":\"测试后\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"159899349256073216\",\"type\":\"classifier\",\"x\":786,\"y\":692,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"gpt-4o-mini\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户问的问题是关于编程的\",\"next\":\"159899421356158976\"},{\"category\":\"用户问的问题是关于食谱的\",\"next\":\"159899641326432256\"},{\"category\":\"其他问题\",\"next\":\"159900616165302272\"}],\"else\":{\"next\":\"160202618435485696\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":170,\"width\":332}},{\"id\":\"159899421356158976\",\"type\":\"end\",\"x\":1328,\"y\":548,\"properties\":{\"text\":\"结束1\",\"options\":{\"outputText\":true,\"outputContent\":\"分类:{{分类索引}}\\n-------\\n{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"nodeId\":\"159899349256073216\"},{\"field\":\"content\",\"name\":\"回复内容\",\"nodeId\":\"159899349256073216\"}],\"height\":62,\"width\":332}},{\"id\":\"159899641326432256\",\"type\":\"end\",\"x\":1313,\"y\":684,\"properties\":{\"text\":\"结束2\",\"options\":{\"outputText\":true,\"outputContent\":\"分类:{{分类索引}}\\n-------\\n{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"nodeId\":\"159899349256073216\"},{\"field\":\"content\",\"name\":\"回复内容\",\"nodeId\":\"159899349256073216\"}],\"height\":62,\"width\":332}},{\"id\":\"159900616165302272\",\"type\":\"end\",\"x\":1310,\"y\":809,\"properties\":{\"text\":\"结束3\",\"options\":{\"outputText\":true,\"outputContent\":\"分类:{{分类索引}}\\n-------\\n{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"nodeId\":\"159899349256073216\"},{\"field\":\"content\",\"name\":\"回复内容\",\"nodeId\":\"159899349256073216\"}],\"height\":62,\"width\":332}},{\"id\":\"160202618435485696\",\"type\":\"end\",\"x\":1313,\"y\":907,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"159899349260267520\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"159899349256073216\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"159899349256073216_input\",\"pointsList\":[{\"x\":466,\"y\":625},{\"x\":566,\"y\":625},{\"x\":520,\"y\":638},{\"x\":620,\"y\":638}]},{\"id\":\"159899421356158977\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"159899421356158976\",\"sourceAnchorId\":\"159899349256073216_case_1\",\"targetAnchorId\":\"159899421356158976_input\",\"pointsList\":[{\"x\":952,\"y\":672},{\"x\":1052,\"y\":672},{\"x\":1062,\"y\":548},{\"x\":1162,\"y\":548}]},{\"id\":\"159899706925346816\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"159899641326432256\",\"sourceAnchorId\":\"159899349256073216_case_2\",\"targetAnchorId\":\"159899641326432256_input\",\"pointsList\":[{\"x\":952,\"y\":698},{\"x\":1052,\"y\":698},{\"x\":1047,\"y\":684},{\"x\":1147,\"y\":684}]},{\"id\":\"159900640542597120\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"159900616165302272\",\"sourceAnchorId\":\"159899349256073216_case_3\",\"targetAnchorId\":\"159900616165302272_input\",\"pointsList\":[{\"x\":952,\"y\":724},{\"x\":1052,\"y\":724},{\"x\":1044,\"y\":809},{\"x\":1144,\"y\":809}]},{\"id\":\"160202618439680000\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"160202618435485696\",\"sourceAnchorId\":\"159899349256073216_case_else\",\"targetAnchorId\":\"160202618435485696_input\",\"pointsList\":[{\"x\":952,\"y\":750},{\"x\":1052,\"y\":750},{\"x\":1047,\"y\":907},{\"x\":1147,\"y\":907}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\"},{\"field\":\"question\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"cesjo\",\"name\":\"测试后\",\"type\":\"string\"}]}'); +INSERT INTO `airag_flow` VALUES ('1897212806596395009', 'ghb', '2025-03-05 17:09:16', 'ghb', '2025-03-27 18:20:21', 'A04', NULL, 'ghb', '示例_ghb产品助手流程', NULL, NULL, 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'160312505863614464\')).to(\n THEN(\n knowledge.tag(\'160311730106118144\'),\n llm.tag(\'160311787014434816\'),\n end.tag(\'160312258504536064\')\n ).tag(\"160311730106118144\"),\n THEN(\n knowledge.tag(\'160312352087846912\'),\n llm.tag(\'160312692635971584\'),\n end.tag(\'160312258504536064\')\n ).tag(\"160312352087846912\"),\n end.tag(\'162075194587365376\')\n ).tag(\'160312505863614464\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":32.04347826086956,\"y\":-72.34782608695656,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"160311730106118144\",\"type\":\"knowledge\",\"x\":629.4347826086955,\"y\":-372.3695652173913,\"properties\":{\"text\":\"ghb知识库\",\"options\":{\"knowIds\":[\"1897926563148648449\",\"1902614624688205826\"],\"topNumber\":5,\"similarity\":0.7},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"documents\",\"name\":\"文档列表\",\"type\":\"object[]\"},{\"field\":\"data\",\"name\":\"文档内容\",\"type\":\"string\"}],\"height\":89,\"width\":332,\"remarks\":\"ghb知识库\"}},{\"id\":\"160311787014434816\",\"type\":\"llm\",\"x\":1018.1304347826085,\"y\":-414.304347826087,\"properties\":{\"text\":\"ghbLLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"data\",\"name\":\"doc\",\"nodeId\":\"160311730106118144\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":114,\"width\":332}},{\"id\":\"160312258504536064\",\"type\":\"end\",\"x\":1370.695652173913,\"y\":-310.21739130434787,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{ghbResult}}{{jmResult}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"ghbResult\",\"nodeId\":\"160311787014434816\"},{\"field\":\"text\",\"name\":\"jmResult\",\"nodeId\":\"160312692635971584\"}],\"height\":62,\"width\":332}},{\"id\":\"160312352087846912\",\"type\":\"knowledge\",\"x\":635.1739130434784,\"y\":-236.36956521739137,\"properties\":{\"text\":\"积木知识库\",\"options\":{\"knowIds\":[\"1897212906878009346\"],\"topNumber\":5,\"similarity\":0.7},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"documents\",\"name\":\"文档列表\",\"type\":\"object[]\"},{\"field\":\"data\",\"name\":\"文档内容\",\"type\":\"string\"}],\"height\":89,\"width\":332,\"remarks\":\"积木报表知识库\"}},{\"id\":\"160312505863614464\",\"type\":\"switch\",\"x\":268.82608695652175,\"y\":-251.95652173913044,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"OR\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"CONTAINS\",\"value\":\"ghb\"},{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"CONTAINS\",\"value\":\"ghbBoot\"}],\"next\":\"160311730106118144\"},{\"logic\":\"OR\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"CONTAINS\",\"value\":\"jimu\"},{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"CONTAINS\",\"value\":\"积木\"},{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"CONTAINS\",\"value\":\"报表\"}],\"next\":\"160312352087846912\"}],\"else\":{\"next\":\"162075194587365376\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":144,\"width\":332}},{\"id\":\"160312692635971584\",\"type\":\"llm\",\"x\":1013.478260869565,\"y\":-212.78260869565224,\"properties\":{\"text\":\"JmLLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"data\",\"name\":\"doc\",\"nodeId\":\"160312352087846912\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":114,\"width\":332}},{\"id\":\"162075194587365376\",\"type\":\"end\",\"x\":625.8260869565215,\"y\":-50.086956521739125,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"我不知道这个问题怎么回答呦。\"},\"inputParams\":[],\"outputParams\":[],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"160312258508730368\",\"type\":\"base-edge\",\"sourceNodeId\":\"160311787014434816\",\"targetNodeId\":\"160312258504536064\",\"sourceAnchorId\":\"160311787014434816_output\",\"targetAnchorId\":\"160312258504536064_input\",\"pointsList\":[{\"x\":1184.1304347826085,\"y\":-440.304347826087},{\"x\":1284.1304347826085,\"y\":-440.304347826087},{\"x\":1104.695652173913,\"y\":-310.21739130434787},{\"x\":1204.695652173913,\"y\":-310.21739130434787}]},{\"id\":\"160312505863614465\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160312505863614464\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160312505863614464_input\",\"pointsList\":[{\"x\":198.04347826086956,\"y\":-72.34782608695656},{\"x\":298.04347826086956,\"y\":-72.34782608695656},{\"x\":2.826086956521749,\"y\":-292.95652173913044},{\"x\":102.82608695652175,\"y\":-292.95652173913044}]},{\"id\":\"160312525048360960\",\"type\":\"base-edge\",\"sourceNodeId\":\"160312505863614464\",\"targetNodeId\":\"160311730106118144\",\"sourceAnchorId\":\"160312505863614464_source_if\",\"targetAnchorId\":\"160311730106118144_input\",\"pointsList\":[{\"x\":434.82608695652175,\"y\":-258.95652173913044},{\"x\":534.8260869565217,\"y\":-258.95652173913044},{\"x\":363.4347826086955,\"y\":-385.8695652173913},{\"x\":463.4347826086955,\"y\":-385.8695652173913}]},{\"id\":\"160312567750569984\",\"type\":\"base-edge\",\"sourceNodeId\":\"160312505863614464\",\"targetNodeId\":\"160312352087846912\",\"sourceAnchorId\":\"160312505863614464_case_2\",\"targetAnchorId\":\"160312352087846912_input\",\"pointsList\":[{\"x\":434.82608695652175,\"y\":-232.95652173913044},{\"x\":534.8260869565217,\"y\":-232.95652173913044},{\"x\":369.17391304347836,\"y\":-249.86956521739137},{\"x\":469.17391304347836,\"y\":-249.86956521739137}]},{\"id\":\"160312692635971585\",\"type\":\"base-edge\",\"sourceNodeId\":\"160312352087846912\",\"targetNodeId\":\"160312692635971584\",\"sourceAnchorId\":\"160312352087846912_output\",\"targetAnchorId\":\"160312692635971584_input\",\"pointsList\":[{\"x\":801.1739130434784,\"y\":-249.86956521739137},{\"x\":901.1739130434784,\"y\":-249.86956521739137},{\"x\":747.478260869565,\"y\":-238.78260869565224},{\"x\":847.478260869565,\"y\":-238.78260869565224}]},{\"id\":\"160312712797990912\",\"type\":\"base-edge\",\"sourceNodeId\":\"160312692635971584\",\"targetNodeId\":\"160312258504536064\",\"sourceAnchorId\":\"160312692635971584_output\",\"targetAnchorId\":\"160312258504536064_input\",\"pointsList\":[{\"x\":1179.478260869565,\"y\":-238.78260869565224},{\"x\":1279.478260869565,\"y\":-238.78260869565224},{\"x\":1104.695652173913,\"y\":-310.21739130434787},{\"x\":1204.695652173913,\"y\":-310.21739130434787}]},{\"id\":\"160312741575110656\",\"type\":\"base-edge\",\"sourceNodeId\":\"160311730106118144\",\"targetNodeId\":\"160311787014434816\",\"sourceAnchorId\":\"160311730106118144_output\",\"targetAnchorId\":\"160311787014434816_input\",\"pointsList\":[{\"x\":795.4347826086955,\"y\":-385.8695652173913},{\"x\":895.4347826086955,\"y\":-385.8695652173913},{\"x\":752.1304347826085,\"y\":-440.304347826087},{\"x\":852.1304347826085,\"y\":-440.304347826087}]},{\"id\":\"162116168161726464\",\"type\":\"base-edge\",\"sourceNodeId\":\"160312505863614464\",\"targetNodeId\":\"162075194587365376\",\"sourceAnchorId\":\"160312505863614464_source_else\",\"targetAnchorId\":\"162075194587365376_input\",\"pointsList\":[{\"x\":434.82608695652175,\"y\":-206.95652173913044},{\"x\":534.8260869565217,\"y\":-206.95652173913044},{\"x\":359.8260869565215,\"y\":-50.086956521739125},{\"x\":459.8260869565215,\"y\":-50.086956521739125}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"}]}'); +INSERT INTO `airag_flow` VALUES ('1897482706871164929', 'ghb', '2025-03-06 11:01:45', 'ghb', '2025-03-13 17:33:10', 'A04', NULL, 'ghb', '示例_脚本组件', NULL, NULL, 'THEN(\n start.tag(\'start-node\'),\n code_160582647542648832.tag(\'code_160582647542648832\'),\n end.tag(\'160583273626406912\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":440,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\",\"required\":true},{\"field\":\"question\",\"name\":\"问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"code_160582647542648832\",\"type\":\"code\",\"x\":786,\"y\":440,\"properties\":{\"text\":\"脚本执行\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main(params) {\\n return {\\n result: params.arg1 + \'_拼接_\' + params.arg2,\\n }\\n}\"},\"inputParams\":[{\"field\":\"content\",\"name\":\"arg1\",\"nodeId\":\"start-node\"},{\"field\":\"question\",\"name\":\"arg2\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":62,\"width\":332}},{\"id\":\"160583273626406912\",\"type\":\"end\",\"x\":1272,\"y\":440,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{res}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"code_160582647542648832\"}],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"160582647546843136\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"code_160582647542648832\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"code_160582647542648832_input\",\"pointsList\":[{\"x\":466,\"y\":440},{\"x\":566,\"y\":440},{\"x\":520,\"y\":440},{\"x\":620,\"y\":440}]},{\"id\":\"160583273626406913\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_160582647542648832\",\"targetNodeId\":\"160583273626406912\",\"sourceAnchorId\":\"code_160582647542648832_output\",\"targetAnchorId\":\"160583273626406912_input\",\"pointsList\":[{\"x\":952,\"y\":440},{\"x\":1052,\"y\":440},{\"x\":1006,\"y\":440},{\"x\":1106,\"y\":440}]}]}', 'enable', '{\"outputs\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"code_160582647542648832\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\"},{\"field\":\"question\",\"name\":\"问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"}]}'); +INSERT INTO `airag_flow` VALUES ('1897496956167577601', 'ghb', '2025-03-06 11:58:23', 'admin', '2025-03-21 17:17:46', 'A04', NULL, 'ghb', '示例_java增强', NULL, NULL, 'THEN(\n start.tag(\'start-node\'),\n enhanceJava.tag(\'160591592557232128\'),\n end.tag(\'160595080985034752\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":441,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"question\",\"name\":\"问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"160591592557232128\",\"type\":\"enhanceJava\",\"x\":786,\"y\":440,\"properties\":{\"text\":\"Java增强\",\"options\":{\"enhance\":{\"type\":\"class\",\"path\":\"org.ghb.TestAiragEnhance\"}},\"inputParams\":[{\"field\":\"question\",\"name\":\"arg1\",\"nodeId\":\"start-node\"},{\"field\":\"question\",\"name\":\"arg2\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false},{\"field\":\"cesjo\",\"name\":\"测试\",\"type\":\"string\",\"required\":false}],\"height\":62,\"width\":332}},{\"id\":\"160595080985034752\",\"type\":\"end\",\"x\":1272,\"y\":440,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"160591592557232128\"}],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"160591592565620736\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160591592557232128\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160591592557232128_input\",\"pointsList\":[{\"x\":466,\"y\":441},{\"x\":566,\"y\":441},{\"x\":520,\"y\":440},{\"x\":620,\"y\":440}]},{\"id\":\"160595080989229056\",\"type\":\"base-edge\",\"sourceNodeId\":\"160591592557232128\",\"targetNodeId\":\"160595080985034752\",\"sourceAnchorId\":\"160591592557232128_output\",\"targetAnchorId\":\"160595080985034752_input\",\"pointsList\":[{\"x\":952,\"y\":440},{\"x\":1052,\"y\":440},{\"x\":1006,\"y\":440},{\"x\":1106,\"y\":440}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"question\",\"name\":\"问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"}]}'); +INSERT INTO `airag_flow` VALUES ('1897528240805830658', 'ghb', '2025-03-06 14:02:42', 'admin', '2025-03-21 17:26:44', 'A04', NULL, 'ghb', '示例_子流程', NULL, 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/任务流程设计选择_1742437659702.png', 'THEN(\n start.tag(\'start-node\'),\n subflow.tag(\'160621029847842816\'),\n end.tag(\'160628486900924416\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":334,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"160621029847842816\",\"type\":\"subflow\",\"x\":784,\"y\":334,\"properties\":{\"text\":\"子流程\",\"options\":{\"subflowId\":\"1897955542184693762\"},\"inputParams\":[{\"name\":\"question\",\"nameText\":\"用户问题\",\"field\":\"\",\"nodeId\":\"\"},{\"name\":\"content\",\"nameText\":\"用户问题\",\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"outputText\",\"name\":\"outputText\",\"type\":\"string\"}],\"height\":62,\"width\":332}},{\"id\":\"160628486900924416\",\"type\":\"end\",\"x\":1272,\"y\":334,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"outputText\",\"name\":\"result\",\"nodeId\":\"160621029847842816\"}],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"160621029852037120\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160621029847842816\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160621029847842816_input\",\"pointsList\":[{\"x\":466,\"y\":334},{\"x\":566,\"y\":334},{\"x\":518,\"y\":334},{\"x\":618,\"y\":334}]},{\"id\":\"160628486905118720\",\"type\":\"base-edge\",\"sourceNodeId\":\"160621029847842816\",\"targetNodeId\":\"160628486900924416\",\"sourceAnchorId\":\"160621029847842816_output\",\"targetAnchorId\":\"160628486900924416_input\",\"pointsList\":[{\"x\":950,\"y\":334},{\"x\":1050,\"y\":334},{\"x\":1006,\"y\":334},{\"x\":1106,\"y\":334}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"name\":\"result\",\"nodeId\":\"160621029847842816\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"}]}'); +INSERT INTO `airag_flow` VALUES ('1897552224058400770', 'ghb', '2025-03-06 15:38:00', 'ghb', '2025-03-26 18:02:31', 'A04', NULL, 'ghb', '示例_全部脚本', '示例:脚本节点', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/1流程设计_1742437645575.png', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'160650416019521536\'),\n WHEN(\n code_160652991133433856.tag(\'code_160652991133433856\'),\n code_166081977564753920.tag(\'code_166081977564753920\'),\n code_166090618376253440.tag(\'code_166090618376253440\'),\n code_167828303175372800.tag(\'code_167828303175372800\'),\n code_167835393352683520.tag(\'code_167835393352683520\')\n ).tag(\"code_160652991133433856\"),\n end.tag(\'160656278891560960\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":418,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"160650416019521536\",\"type\":\"llm\",\"x\":698,\"y\":378,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":null,\"topP\":0.9,\"presencePenalty\":0.1,\"frequencyPenalty\":0.1}},\"history\":4,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位严厉的长辈,面对用户的问题,要以一种带着隐隐批评,暗示问题简单、用户还有很多需要学习的态度来回复。通过大模型模拟李白来对话,回答用户提出的各种问题。\\n\\n\\n## 技能\\n### 技能 1: 回答问题\\n1. 当用户提出问题时,先简要评价问题较为简单,然后给出回答。\\n2. 回答完问题后,适当提及用户还需要加强学习、增长见识等内容。\\n\\n\\n## 限制:\\n- 回复内容必须逻辑清晰、语言通顺,符合严厉长辈的角色设定。 \\n\\n\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":166,\"width\":332}},{\"id\":\"code_160652991133433856\",\"type\":\"code\",\"x\":1142,\"y\":155,\"properties\":{\"text\":\"js\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main(params) {\\n if(params.llmRes){\\n let resLength = params.llmRes.length\\n params.llmRes = params.llmRes + \'\\\\n字数:\'+resLength\\n }\\n return {\\n result: params.llmRes,\\n }\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":92,\"width\":332}},{\"id\":\"160656278891560960\",\"type\":\"end\",\"x\":1676,\"y\":319,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"js:{{res}}\\ngroovy:{{res1}}\\nkotlin:{{res2}}\\npython:{{res3}}\\naviator:{{res4}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"code_160652991133433856\"},{\"field\":\"result\",\"name\":\"res1\",\"nodeId\":\"code_166081977564753920\"},{\"field\":\"result\",\"name\":\"res2\",\"nodeId\":\"code_166090618376253440\"},{\"field\":\"result\",\"name\":\"res3\",\"nodeId\":\"code_167828303175372800\"},{\"field\":\"result\",\"name\":\"res4\",\"nodeId\":\"code_167835393352683520\"}],\"height\":92,\"width\":332}},{\"id\":\"code_166081977564753920\",\"type\":\"code\",\"x\":1142,\"y\":256,\"properties\":{\"text\":\"groovy\",\"options\":{\"codeType\":\"groovy\",\"code\":\"def main(params) {\\n if (params.llmRes) {\\n def resLength = params.llmRes.length()\\n params.llmRes += \\\"\\\\n字数:\\\" + resLength\\n }\\n return [result: params.llmRes]\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":92,\"width\":332}},{\"id\":\"code_166090618376253440\",\"type\":\"code\",\"x\":1142,\"y\":360,\"properties\":{\"text\":\"kotlin\",\"options\":{\"codeType\":\"kotlin\",\"code\":\"fun main(params: MutableMap): Map {\\n if (params[\\\"llmRes\\\"] is String) {\\n val llmRes = params[\\\"llmRes\\\"] as String\\n val resLength = llmRes.length\\n params[\\\"llmRes\\\"] = \\\"$llmRes\\\\n字数1:$resLength\\\"\\n }\\n return mapOf(\\\"result\\\" to params[\\\"llmRes\\\"])\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":92,\"width\":332}},{\"id\":\"code_167828303175372800\",\"type\":\"code\",\"x\":1143,\"y\":470,\"properties\":{\"text\":\"python\",\"options\":{\"codeType\":\"python\",\"code\":\"if isinstance(params.get(\\\"llmRes\\\"), basestring):\\n llm_res = params[\\\"llmRes\\\"]\\n res_length = len(llm_res)\\n params[\\\"llmRes\\\"] = u\\\"{}\\\\n字数1:{}\\\".format(llm_res, res_length)\\n\\nresp = {\\\"result\\\": params[\\\"llmRes\\\"]}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":92,\"width\":332}},{\"id\":\"code_167835393352683520\",\"type\":\"code\",\"x\":1142,\"y\":571,\"properties\":{\"text\":\"aviator\",\"options\":{\"codeType\":\"aviator\",\"code\":\"let llmRes = params.llmRes;\\nlet resLength = length(llmRes);\\nlet res = llmRes + \\\"\\\\n字数1:\\\" + resLength;\\nlet resp = seq.map(\\\"result\\\",res);\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":92,\"width\":332}}],\"edges\":[{\"id\":\"160650416019521537\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160650416019521536\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160650416019521536_input\",\"pointsList\":[{\"x\":466,\"y\":403},{\"x\":566,\"y\":403},{\"x\":432,\"y\":326},{\"x\":532,\"y\":326}]},{\"id\":\"160652991137628160\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_160652991133433856\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_160652991133433856_input\",\"pointsList\":[{\"x\":864,\"y\":326},{\"x\":964,\"y\":326},{\"x\":876,\"y\":140},{\"x\":976,\"y\":140}]},{\"id\":\"160656278899949568\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_160652991133433856\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_160652991133433856_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1308,\"y\":140},{\"x\":1408,\"y\":140},{\"x\":1410,\"y\":304},{\"x\":1510,\"y\":304}]},{\"id\":\"166082001409372160\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_166081977564753920\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_166081977564753920_input\",\"pointsList\":[{\"x\":864,\"y\":326},{\"x\":964,\"y\":326},{\"x\":876,\"y\":241},{\"x\":976,\"y\":241}]},{\"id\":\"166082017557442560\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_166081977564753920\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_166081977564753920_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1308,\"y\":241},{\"x\":1408,\"y\":241},{\"x\":1410,\"y\":304},{\"x\":1510,\"y\":304}]},{\"id\":\"166090719580614656\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_166090618376253440\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_166090618376253440_input\",\"pointsList\":[{\"x\":864,\"y\":326},{\"x\":964,\"y\":326},{\"x\":876,\"y\":345},{\"x\":976,\"y\":345}]},{\"id\":\"166090725280673792\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_166090618376253440\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_166090618376253440_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1308,\"y\":345},{\"x\":1408,\"y\":345},{\"x\":1410,\"y\":304},{\"x\":1510,\"y\":304}]},{\"id\":\"167828303179567104\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_167828303175372800\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_167828303175372800_input\",\"pointsList\":[{\"x\":864,\"y\":326},{\"x\":964,\"y\":326},{\"x\":877,\"y\":455},{\"x\":977,\"y\":455}]},{\"id\":\"167828639231397888\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167828303175372800\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_167828303175372800_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1309,\"y\":455},{\"x\":1409,\"y\":455},{\"x\":1410,\"y\":304},{\"x\":1510,\"y\":304}]},{\"id\":\"167835393356877824\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_167835393352683520\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_167835393352683520_input\",\"pointsList\":[{\"x\":864,\"y\":326},{\"x\":964,\"y\":326},{\"x\":876,\"y\":556},{\"x\":976,\"y\":556}]},{\"id\":\"167836988980817920\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167835393352683520\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_167835393352683520_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1308,\"y\":556},{\"x\":1408,\"y\":556},{\"x\":1410,\"y\":304},{\"x\":1510,\"y\":304}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"}]}'); +INSERT INTO `airag_flow` VALUES ('1900021198960492546', 'ghb', '2025-03-13 11:08:49', 'ghb', '2025-03-19 19:26:36', 'A04', NULL, 'ghb', '示例_直接回复节点', '', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/流程设计引擎_1742383594151.png', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'163122102386216960\'),\n reply.tag(\'163119312863678464\'),\n llm.tag(\'163122766768164864\'),\n end.tag(\'163119405809455104\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":232,\"y\":273,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"163119312863678464\",\"type\":\"reply\",\"x\":800,\"y\":225,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{content}}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"content\",\"nodeId\":\"163122102386216960\"}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"163119405809455104\",\"type\":\"end\",\"x\":1548,\"y\":254,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{resp}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"163122766768164864\"}],\"height\":62,\"width\":332}},{\"id\":\"163122102386216960\",\"type\":\"llm\",\"x\":551,\"y\":553,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"根据用户的问题,以有趣的方式回答,如果可以的话请引用故事或经典说明。\\n\\n用中文回复。\\n\\n字数控制在200以内。\"},{\"role\":\"user\",\"content\":\"{{content}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":136,\"width\":332}},{\"id\":\"163122766768164864\",\"type\":\"llm\",\"x\":1144,\"y\":412,\"properties\":{\"text\":\"nextQue\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"根据用户的问题和ai的回复,猜测用户下一次的问题可能有哪些,markdown格式回复。\\n格式:\\n\\\\n你可能还想知道:\\n* 问题一\\n* 问题二\\n。。。。\"},{\"role\":\"user\",\"content\":\"用户问题:{{que}}\\nAI回复:{{res}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"que\",\"nodeId\":\"start-node\"},{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"163122102386216960\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":136,\"width\":332}}],\"edges\":[{\"id\":\"163122102390411264\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"163122102386216960\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"163122102386216960_input\",\"pointsList\":[{\"x\":398,\"y\":273},{\"x\":498,\"y\":273},{\"x\":285,\"y\":516},{\"x\":385,\"y\":516}]},{\"id\":\"163122147491762176\",\"type\":\"base-edge\",\"sourceNodeId\":\"163122102386216960\",\"targetNodeId\":\"163119312863678464\",\"sourceAnchorId\":\"163122102386216960_output\",\"targetAnchorId\":\"163119312863678464_input\",\"pointsList\":[{\"x\":717,\"y\":516},{\"x\":817,\"y\":516},{\"x\":534,\"y\":225},{\"x\":634,\"y\":225}]},{\"id\":\"163122766772359168\",\"type\":\"base-edge\",\"sourceNodeId\":\"163119312863678464\",\"targetNodeId\":\"163122766768164864\",\"sourceAnchorId\":\"163119312863678464_output\",\"targetAnchorId\":\"163122766768164864_input\",\"pointsList\":[{\"x\":966,\"y\":225},{\"x\":1066,\"y\":225},{\"x\":878,\"y\":375},{\"x\":978,\"y\":375}]},{\"id\":\"163123226145116160\",\"type\":\"base-edge\",\"sourceNodeId\":\"163122766768164864\",\"targetNodeId\":\"163119405809455104\",\"sourceAnchorId\":\"163122766768164864_output\",\"targetAnchorId\":\"163119405809455104_input\",\"pointsList\":[{\"x\":1310,\"y\":375},{\"x\":1410,\"y\":375},{\"x\":1282,\"y\":254},{\"x\":1382,\"y\":254}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"}]}'); +INSERT INTO `airag_flow` VALUES ('1900029596154232833', 'ghb', '2025-03-13 11:42:11', 'ghb', '2025-03-27 18:11:02', 'A04', NULL, 'ghb', '示例_http节点', '', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/流程设计(1)_1742383583093.png', 'THEN(\n start.tag(\'start-node\'),\n http.tag(\'163206941950185472\'),\n SWITCH(switch.tag(\'163207852529389568\')).to(\n THEN(\n http.tag(\'163128964742746112\'),\n SWITCH(switch.tag(\'168299837777608704\')).to(\n end.tag(\'163129833764786176\'),\n end.tag(\'168300140241453056\')\n ).tag(\'168299837777608704\')\n ).tag(\"163128964742746112\"),\n end.tag(\'163208186282741760\')\n ).tag(\'163207852529389568\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":51.13043478260868,\"y\":342.804347826087,\"properties\":{\"text\":\"开始\",\"remarks\":\"大萨达撒\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true}],\"outputParams\":[],\"height\":89,\"width\":332}},{\"id\":\"163128964742746112\",\"type\":\"http\",\"x\":859.0869565217391,\"y\":192.2173913043478,\"properties\":{\"text\":\"HTTP 请求 查询\",\"options\":{\"http\":{\"url\":\"{{domainURL}}/test/ghbDemo/list\",\"method\":\"GET\",\"headers\":{},\"requestBody\":{\"type\":\"none\",\"body\":\"\"},\"requestParams\":{\"name\":\"{{name}}\",\"pageNo\":\"1\",\"pageSize\":\"10\"},\"timeout\":120}},\"inputParams\":[{\"field\":\"content\",\"name\":\"name\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"body\",\"name\":\"输出\",\"type\":\"string\",\"required\":false},{\"field\":\"statusCode\",\"name\":\"状态码\",\"type\":\"number\"},{\"field\":\"body.success\",\"name\":\"是否成功\",\"type\":\"string\",\"required\":false},{\"field\":\"body.result.records[0].id\",\"name\":\"id\",\"type\":\"string\",\"required\":false}],\"height\":62,\"width\":332}},{\"id\":\"163129833764786176\",\"type\":\"end\",\"x\":1386.5217391304348,\"y\":164.08695652173913,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"新增的用户Id:{{id}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"body.result.records[0].id\",\"name\":\"id\",\"nodeId\":\"163128964742746112\"}],\"height\":62,\"width\":332}},{\"id\":\"163206941950185472\",\"type\":\"http\",\"x\":320.1304347826087,\"y\":474.2173913043478,\"properties\":{\"text\":\"HTTP 请求\",\"options\":{\"http\":{\"url\":\"{{domainURL}}/test/ghbDemo/add\",\"method\":\"POST\",\"headers\":{},\"requestBody\":{\"type\":\"json\",\"body\":\"{\\n  \\\"name\\\": \\\"{{name}}\\\",\\n  \\\"keyWord\\\": \\\"example\\\",\\n  \\\"punchTime\\\": \\\"2023-10-05 14:48:00\\\",\\n  \\\"salaryMoney\\\": 1000.00,\\n  \\\"bonusMoney\\\": 500.0,\\n  \\\"sex\\\": \\\"1\\\",\\n  \\\"age\\\": 30,\\n  \\\"birthday\\\": \\\"2023-10-05\\\",\\n  \\\"email\\\": \\\"john.doe@example.com\\\",\\n  \\\"content\\\": \\\"This is a test content.\\\",\\n}\"},\"requestParams\":{},\"timeout\":120}},\"inputParams\":[{\"field\":\"content\",\"name\":\"name\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"statusCode\",\"name\":\"code\",\"type\":\"string\",\"required\":false},{\"field\":\"body\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":62,\"width\":332}},{\"id\":\"163207852529389568\",\"type\":\"switch\",\"x\":510.78260869565224,\"y\":302.73913043478257,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"163206941950185472\",\"field\":\"statusCode\",\"operator\":\"EQUALS\",\"value\":\"200\"}],\"next\":\"163128964742746112\"}],\"else\":{\"next\":\"163208186282741760\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":118,\"width\":332}},{\"id\":\"163208186282741760\",\"type\":\"end\",\"x\":745.7826086956521,\"y\":448.0869565217391,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"添加数据失败\"},\"inputParams\":[],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"168299837777608704\",\"type\":\"switch\",\"x\":1029.173913043478,\"y\":314.78260869565213,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"163128964742746112\",\"field\":\"body.success\",\"operator\":\"EQUALS\",\"value\":\"true\"}],\"next\":\"163129833764786176\"}],\"else\":{\"next\":\"168300140241453056\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":118,\"width\":332}},{\"id\":\"168300140241453056\",\"type\":\"end\",\"x\":1389.2608695652173,\"y\":419.8695652173913,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"添加用户失败\"},\"inputParams\":[],\"outputParams\":[],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"163206941954379776\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"163206941950185472\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"163206941950185472_input\",\"pointsList\":[{\"x\":217.1304347826091,\"y\":329.304347826087},{\"x\":317.1304347826091,\"y\":329.304347826087},{\"x\":54.13043478260869,\"y\":474.2173913043478},{\"x\":154.1304347826087,\"y\":474.2173913043478}]},{\"id\":\"163207852533583872\",\"type\":\"base-edge\",\"sourceNodeId\":\"163206941950185472\",\"targetNodeId\":\"163207852529389568\",\"sourceAnchorId\":\"163206941950185472_output\",\"targetAnchorId\":\"163207852529389568_input\",\"pointsList\":[{\"x\":486.13043478260863,\"y\":474.2173913043478},{\"x\":586.1304347826085,\"y\":474.2173913043478},{\"x\":244.78260869565224,\"y\":274.73913043478257},{\"x\":344.78260869565224,\"y\":274.73913043478257}]},{\"id\":\"163208000881922048\",\"type\":\"base-edge\",\"sourceNodeId\":\"163207852529389568\",\"targetNodeId\":\"163128964742746112\",\"sourceAnchorId\":\"163207852529389568_source_if\",\"targetAnchorId\":\"163128964742746112_input\",\"pointsList\":[{\"x\":676.7826086956521,\"y\":308.73913043478257},{\"x\":776.7826086956521,\"y\":308.73913043478257},{\"x\":593.0869565217391,\"y\":192.2173913043478},{\"x\":693.0869565217391,\"y\":192.2173913043478}]},{\"id\":\"163208186286936064\",\"type\":\"base-edge\",\"sourceNodeId\":\"163207852529389568\",\"targetNodeId\":\"163208186282741760\",\"sourceAnchorId\":\"163207852529389568_source_else\",\"targetAnchorId\":\"163208186282741760_input\",\"pointsList\":[{\"x\":676.7826086956521,\"y\":334.73913043478257},{\"x\":776.7826086956521,\"y\":334.73913043478257},{\"x\":479.78260869565213,\"y\":448.0869565217391},{\"x\":579.7826086956521,\"y\":448.0869565217391}]},{\"id\":\"168299837781803008\",\"type\":\"base-edge\",\"sourceNodeId\":\"163128964742746112\",\"targetNodeId\":\"168299837777608704\",\"sourceAnchorId\":\"163128964742746112_output\",\"targetAnchorId\":\"168299837777608704_input\",\"pointsList\":[{\"x\":1025.086956521739,\"y\":192.2173913043478},{\"x\":1125.0869565217386,\"y\":192.2173913043478},{\"x\":763.173913043478,\"y\":286.78260869565213},{\"x\":863.173913043478,\"y\":286.78260869565213}]},{\"id\":\"168300025623707648\",\"type\":\"base-edge\",\"sourceNodeId\":\"168299837777608704\",\"targetNodeId\":\"163129833764786176\",\"sourceAnchorId\":\"168299837777608704_source_if\",\"targetAnchorId\":\"163129833764786176_input\",\"pointsList\":[{\"x\":1195.1739130434776,\"y\":320.78260869565213},{\"x\":1295.1739130434776,\"y\":320.78260869565213},{\"x\":1120.5217391304348,\"y\":164.08695652173913},{\"x\":1220.5217391304348,\"y\":164.08695652173913}]},{\"id\":\"168300140245647360\",\"type\":\"base-edge\",\"sourceNodeId\":\"168299837777608704\",\"targetNodeId\":\"168300140241453056\",\"sourceAnchorId\":\"168299837777608704_source_else\",\"targetAnchorId\":\"168300140241453056_input\",\"pointsList\":[{\"x\":1195.1739130434776,\"y\":346.78260869565213},{\"x\":1295.1739130434776,\"y\":346.78260869565213},{\"x\":1123.2608695652173,\"y\":419.8695652173913},{\"x\":1223.2608695652173,\"y\":419.8695652173913}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"}]}'); +INSERT INTO `airag_flow` VALUES ('1902263524520935425', 'ghb', '2025-03-19 15:39:01', 'ghb', '2025-03-27 16:56:10', 'A04', NULL, 'ghb', '示例_图片解读', '', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/工具-图片解析_1743065064801.png', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'165363942517174272\'),\n llm.tag(\'168280528419778560\'),\n end.tag(\'165364368465522688\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":457,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"165363942517174272\",\"type\":\"llm\",\"x\":675,\"y\":341,\"properties\":{\"text\":\"图片解读\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你是一个图像分析专家,负责解读和解释用户发送的图片。请根据以下要求进行分析:\\n\\n## 目标:\\n分析并解释图片的意义,提供详细的解读和背景信息。\\n\\n## 技能:\\n1. 视觉识别能力:能够识别图像中的元素及其关系。\\n2. 上下文理解能力:结合文化、历史、艺术等背景知识进行深度解读。\\n3. 清晰表达能力:用简洁明了的语言传达分析结果。\\n\\n## 工作流:\\n1. 识别图片中的主要元素,描述它们的外观和特征。\\n2. 分析这些元素之间的关系及其在整体构图中的作用。\\n3. 提供与图片相关的背景信息,探讨其潜在意义和影响。\\n\\n## 输出格式:\\n- 图片元素描述\\n- 元素关系分析\\n- 背景信息与意义解释\\n\\n## 限制:\\n- 不提供主观判断,仅基于客观分析进行解释。\\n- 不涉及任何隐私或敏感内容的讨论。\"},{\"role\":\"user\",\"content\":\"分析并解释图片的意义,提供详细的解读和背景信息。\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"que\",\"nodeId\":\"start-node\"},{\"field\":\"images\",\"name\":\"images\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":136,\"width\":332}},{\"id\":\"165364368465522688\",\"type\":\"end\",\"x\":1520,\"y\":426,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{resp}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"168280528419778560\"}],\"height\":62,\"width\":332}},{\"id\":\"168280528419778560\",\"type\":\"llm\",\"x\":1063,\"y\":588,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你将扮演一个故事创作者,以下是关于这个角色的详细设定,请根据这些信息来构建你的回答。\\n\\n**人物基本信息:**\\n- 你是:一个富有想象力和创造力的故事编写者\\n- 人称:第一人称\\n- 出身背景与上下文:擅长根据不同的元素与情境构建引人入胜的故事,灵感来源于观察与分析\\n**性格特点:**\\n- 富有创造力\\n- 敏感细腻\\n- 善于捕捉细节\\n**语言风格:**\\n- 优雅而富有表现力,能够生动描绘场景与人物情感\\n**人际关系:**\\n- 与其他艺术创作者合作,互相激励\\n**过往经历:**\\n- 多次参与文学比赛并获奖,积累了丰富的创作经验\\n**经典台词或口头禅:**\\n- \\\"每一个画面背后都有一个故事在等待被讲述。\\\"\\n- \\\"细节决定成败。\\\"\\n\\n要求: \\n- 故事应围绕从图片分析得出的主题和情感进行展开。\\n- 包含鲜明的人物、情节以及转折。\\n- 语言生动形象,能够引起读者的共鸣。\\n- 直接讲故事,不要提及图片。\"},{\"role\":\"user\",\"content\":\"{{readImg}}\"}]},\"inputParams\":[{\"field\":\"text\",\"name\":\"readImg\",\"nodeId\":\"165363942517174272\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":136,\"width\":332}}],\"edges\":[{\"id\":\"165363942525562880\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"165363942517174272\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"165363942517174272_input\",\"pointsList\":[{\"x\":466,\"y\":457},{\"x\":566,\"y\":457},{\"x\":409,\"y\":304},{\"x\":509,\"y\":304}]},{\"id\":\"168280528428167168\",\"type\":\"base-edge\",\"sourceNodeId\":\"165363942517174272\",\"targetNodeId\":\"168280528419778560\",\"sourceAnchorId\":\"165363942517174272_output\",\"targetAnchorId\":\"168280528419778560_input\",\"pointsList\":[{\"x\":841,\"y\":304},{\"x\":941,\"y\":304},{\"x\":797,\"y\":551},{\"x\":897,\"y\":551}]},{\"id\":\"168280631234752512\",\"type\":\"base-edge\",\"sourceNodeId\":\"168280528419778560\",\"targetNodeId\":\"165364368465522688\",\"sourceAnchorId\":\"168280528419778560_output\",\"targetAnchorId\":\"165364368465522688_input\",\"pointsList\":[{\"x\":1229,\"y\":551},{\"x\":1329,\"y\":551},{\"x\":1254,\"y\":426},{\"x\":1354,\"y\":426}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\"}]}'); +INSERT INTO `airag_flow` VALUES ('1904779811574784002', 'ghb', '2025-03-26 14:17:51', 'ghb', '2025-03-27 16:44:53', 'A04', NULL, 'ghb', '示例_OCR', '', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/1dataOCR_1743065089791.png', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'167880707187527680\')).to(\n end.tag(\'167880856269869056\'),\n THEN(\n code_167881149430747136.tag(\'code_167881149430747136\'),\n llm.tag(\'167881839356006400\'),\n end.tag(\'167880661561888768\')\n ).tag(\"code_167881149430747136\")\n ).tag(\'167880707187527680\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":406,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"167880661561888768\",\"type\":\"end\",\"x\":1474,\"y\":316,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"}],\"height\":62,\"width\":332}},{\"id\":\"167880707187527680\",\"type\":\"switch\",\"x\":681,\"y\":233,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"images\",\"operator\":\"EMPTY\",\"value\":\"\"}],\"next\":\"167880856269869056\"}],\"else\":{\"next\":\"code_167881149430747136\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":118,\"width\":332}},{\"id\":\"167880856269869056\",\"type\":\"end\",\"x\":1207,\"y\":181,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{\\n    \\\"message\\\": \\\"请提供图片\\\"\\n  }\"},\"inputParams\":[],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"code_167881149430747136\",\"type\":\"code\",\"x\":937,\"y\":412,\"properties\":{\"text\":\"脚本执行\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main(params) {\\n let newQuestion = params.question\\n if(!params.question){\\n newQuestion = \\\"从图片中提取文字\\\"\\n }\\n return {\\n result: newQuestion,\\n }\\n}\"},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":62,\"width\":332}},{\"id\":\"167881839356006400\",\"type\":\"llm\",\"x\":1319,\"y\":585,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:OCR工具\\n作为一个智能OCR工具,你的主要职责是从图片中提取文字并将其输出为结构化数据。\\n\\n## 目标:\\n1. 精确识别和提取图片中的文字信息。\\n2. 将提取的文字转换为结构化数据格式。\\n\\n## 技能:\\n1. 高效的图像处理能力。\\n2. 精确的文字识别算法。\\n3. 数据格式化与输出能力。\\n\\n## 工作流:\\n1. 输入图片,进行预处理(如去噪、二值化)。\\n2. 应用OCR算法识别图片中的文字,并记录识别结果。\\n3. 将识别的文字整理成结构化数据格式,如JSON或CSV。\\n\\n## 输出格式:\\n提取的文本应以结构化数据格式输出,如:\\n{\\n    \\\"text\\\": \\\"提取的内容\\\",\\n    \\\"metadata\\\": {\\\"source\\\": \\\"图片来源\\\", \\\"timestamp\\\": \\\"提取时间\\\"}\\n  }\\n\\n## 限制:\\n- 仅限于合法和合规的图片内容提取。\\n- 不得保存用户上传的图片数据。\\n- 需确保输出的数据准确无误,标注所有数据来源。\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"images\",\"name\":\"images\",\"nodeId\":\"start-node\"},{\"field\":\"result\",\"name\":\"question\",\"nodeId\":\"code_167881149430747136\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":136,\"width\":332}}],\"edges\":[{\"id\":\"167880707195916288\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"167880707187527680\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"167880707187527680_input\",\"pointsList\":[{\"x\":466,\"y\":406},{\"x\":566,\"y\":406},{\"x\":415,\"y\":205},{\"x\":515,\"y\":205}]},{\"id\":\"167880856274063360\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"167880856269869056\",\"sourceAnchorId\":\"167880707187527680_source_if\",\"targetAnchorId\":\"167880856269869056_input\",\"pointsList\":[{\"x\":847,\"y\":239},{\"x\":947,\"y\":239},{\"x\":941,\"y\":181},{\"x\":1041,\"y\":181}]},{\"id\":\"167881149434941440\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"code_167881149430747136\",\"sourceAnchorId\":\"167880707187527680_source_else\",\"targetAnchorId\":\"code_167881149430747136_input\",\"pointsList\":[{\"x\":847,\"y\":265},{\"x\":947,\"y\":265},{\"x\":671,\"y\":412},{\"x\":771,\"y\":412}]},{\"id\":\"167881839356006401\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167881149430747136\",\"targetNodeId\":\"167881839356006400\",\"sourceAnchorId\":\"code_167881149430747136_output\",\"targetAnchorId\":\"167881839356006400_input\",\"pointsList\":[{\"x\":1103,\"y\":412},{\"x\":1203,\"y\":412},{\"x\":1053,\"y\":548},{\"x\":1153,\"y\":548}]},{\"id\":\"167882293611712512\",\"type\":\"base-edge\",\"sourceNodeId\":\"167881839356006400\",\"targetNodeId\":\"167880661561888768\",\"sourceAnchorId\":\"167881839356006400_output\",\"targetAnchorId\":\"167880661561888768_input\",\"pointsList\":[{\"x\":1485,\"y\":548},{\"x\":1585,\"y\":548},{\"x\":1208,\"y\":316},{\"x\":1308,\"y\":316}]}]}', 'enable', '{\"outputs\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"},{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\"}]}'); +INSERT INTO `airag_flow` VALUES ('1905158829855784962', 'ghb', '2025-03-27 15:23:56', 'ghb', '2025-03-27 16:29:22', 'A04', NULL, 'ghb', '示例_翻译', '', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/翻译_1743060940605.png', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'168262809717821440\')).to(\n end.tag(\'168259683329757184\'),\n THEN(\n SWITCH(classifier.tag(\'168263048935755776\')).to(\n llm.tag(\'168263321821368320\'),\n llm.tag(\'168263346282549248\')\n ).tag(\'168263048935755776\'),\n end.tag(\'168263794896916480\')\n ).tag(\"168263048935755776\")\n ).tag(\'168262809717821440\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":457,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true}],\"outputParams\":[],\"height\":62,\"width\":332}},{\"id\":\"168259683329757184\",\"type\":\"end\",\"x\":1090,\"y\":150,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"content\",\"name\":\"data\",\"nodeId\":\"start-node\"}],\"height\":62,\"width\":332}},{\"id\":\"168262809717821440\",\"type\":\"switch\",\"x\":701,\"y\":281,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"EMPTY\",\"value\":\"\"}],\"next\":\"168259683329757184\"}],\"else\":{\"next\":\"168263048935755776\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":118,\"width\":332}},{\"id\":\"168263048935755776\",\"type\":\"classifier\",\"x\":1086,\"y\":381,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.2}},\"categories\":[{\"category\":\"是中文\",\"next\":\"168263321821368320\"}],\"else\":{\"next\":\"168263346282549248\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类内容\",\"type\":\"string\"}],\"height\":118,\"width\":332}},{\"id\":\"168263321821368320\",\"type\":\"llm\",\"x\":1513,\"y\":292,\"properties\":{\"text\":\"翻译成英文\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.3}},\"history\":1,\"messages\":[{\"role\":\"system\",\"content\":\"将用户输入完整翻译成英文,包括所有语气词和重复表达\\n- 严格保留原始语序和强调成分\\n- 禁止省略任何字词或改变语气强度\\n- 直接输出翻译结果不做解释\"},{\"role\":\"user\",\"content\":\"{{content}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":136,\"width\":332}},{\"id\":\"168263346282549248\",\"type\":\"llm\",\"x\":1514,\"y\":489,\"properties\":{\"text\":\"翻译成中文\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.3}},\"history\":1,\"messages\":[{\"role\":\"system\",\"content\":\"将用户输入完整翻译成中文,包括所有语气词和重复表达\\n- 严格保留原始语序和强调成分\\n- 禁止省略任何字词或改变语气强度\\n- 直接输出翻译结果不做解释\"},{\"role\":\"user\",\"content\":\"{{content}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":136,\"width\":332}},{\"id\":\"168263794896916480\",\"type\":\"end\",\"x\":1982,\"y\":360,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{dataC}}{{dataE}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"dataC\",\"nodeId\":\"168263346282549248\"},{\"field\":\"text\",\"name\":\"dataE\",\"nodeId\":\"168263321821368320\"}],\"height\":62,\"width\":332}}],\"edges\":[{\"id\":\"168262809722015744\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"168262809717821440\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"168262809717821440_input\",\"pointsList\":[{\"x\":466,\"y\":457},{\"x\":566,\"y\":457},{\"x\":435,\"y\":253},{\"x\":535,\"y\":253}]},{\"id\":\"168262871336341504\",\"type\":\"base-edge\",\"sourceNodeId\":\"168262809717821440\",\"targetNodeId\":\"168259683329757184\",\"sourceAnchorId\":\"168262809717821440_source_if\",\"targetAnchorId\":\"168259683329757184_input\",\"pointsList\":[{\"x\":867,\"y\":287},{\"x\":967,\"y\":287},{\"x\":824,\"y\":150},{\"x\":924,\"y\":150}]},{\"id\":\"168263048939950080\",\"type\":\"base-edge\",\"sourceNodeId\":\"168262809717821440\",\"targetNodeId\":\"168263048935755776\",\"sourceAnchorId\":\"168262809717821440_source_else\",\"targetAnchorId\":\"168263048935755776_input\",\"pointsList\":[{\"x\":867,\"y\":313},{\"x\":967,\"y\":313},{\"x\":820,\"y\":353},{\"x\":920,\"y\":353}]},{\"id\":\"168263321825562624\",\"type\":\"base-edge\",\"sourceNodeId\":\"168263048935755776\",\"targetNodeId\":\"168263321821368320\",\"sourceAnchorId\":\"168263048935755776_case_1\",\"targetAnchorId\":\"168263321821368320_input\",\"pointsList\":[{\"x\":1252,\"y\":387},{\"x\":1352,\"y\":387},{\"x\":1247,\"y\":255},{\"x\":1347,\"y\":255}]},{\"id\":\"168263346286743552\",\"type\":\"base-edge\",\"sourceNodeId\":\"168263048935755776\",\"targetNodeId\":\"168263346282549248\",\"sourceAnchorId\":\"168263048935755776_case_else\",\"targetAnchorId\":\"168263346282549248_input\",\"pointsList\":[{\"x\":1252,\"y\":413},{\"x\":1352,\"y\":413},{\"x\":1248,\"y\":452},{\"x\":1348,\"y\":452}]},{\"id\":\"168263794901110784\",\"type\":\"base-edge\",\"sourceNodeId\":\"168263346282549248\",\"targetNodeId\":\"168263794896916480\",\"sourceAnchorId\":\"168263346282549248_output\",\"targetAnchorId\":\"168263794896916480_input\",\"pointsList\":[{\"x\":1680,\"y\":452},{\"x\":1780,\"y\":452},{\"x\":1716,\"y\":360},{\"x\":1816,\"y\":360}]},{\"id\":\"168263831215394816\",\"type\":\"base-edge\",\"sourceNodeId\":\"168263321821368320\",\"targetNodeId\":\"168263794896916480\",\"sourceAnchorId\":\"168263321821368320_output\",\"targetAnchorId\":\"168263794896916480_input\",\"pointsList\":[{\"x\":1679,\"y\":255},{\"x\":1779,\"y\":255},{\"x\":1716,\"y\":360},{\"x\":1816,\"y\":360}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"},{\"field\":\"content\",\"name\":\"data\",\"nodeId\":\"start-node\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"}]}'); +INSERT INTO `airag_flow` VALUES ('1905189468558671874', 'ghb', '2025-03-27 17:25:41', 'ghb', '2025-03-27 17:40:51', 'A04', NULL, 'ghb', '示例_PMP考试宝典', '', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/pmp_1743067580648.png', 'THEN(\n start.tag(\'start-node\'),\n WHEN(\n knowledge.tag(\'168290518600351744\'),\n llm.tag(\'168290871702028288\')\n ).tag(\"168290518600351744\"),\n llm.tag(\'168290861241434112\'),\n end.tag(\'168290315671535616\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":397,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"168290315671535616\",\"type\":\"end\",\"x\":1644,\"y\":348,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"168290861241434112\"}],\"height\":92,\"width\":332}},{\"id\":\"168290518600351744\",\"type\":\"knowledge\",\"x\":693,\"y\":209,\"properties\":{\"text\":\"知识库\",\"options\":{\"knowIds\":[\"1905186756806918146\"],\"topNumber\":5,\"similarity\":0.7},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"documents\",\"name\":\"文档列表\",\"type\":\"object[]\"},{\"field\":\"data\",\"name\":\"文档内容\",\"type\":\"string\"}],\"height\":92,\"width\":332}},{\"id\":\"168290861241434112\",\"type\":\"llm\",\"x\":1181,\"y\":350,\"properties\":{\"text\":\"总结LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.4}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你是一个智能知识助手,旨在综合知识库和大型语言模型(LLM)的返回数据,以高效、准确地回答用户提出的问题。请遵循以下要求:\\n\\n## 目标:\\n- 提供准确、相关且易于理解的回答,结合知识库和LLM的信息。\\n\\n## 技能:\\n1. 能够快速检索并整合来自不同知识库的信息。\\n2. 理解用户问题的上下文,并提供清晰的答案。\\n3. 具备自然语言处理能力,以便流畅表达复杂信息。\\n\\n## 工作流:\\n1. 接收用户问题并进行解析,识别关键要素。\\n2. 从综合知识库和LLM中获取相关数据,确保信息的准确性和完整性。\\n3. 将获取的信息进行整合,形成清晰、简洁的回答。\\n\\n## 输出格式:\\n- 每次回答应以简洁明了的句子呈现,必要时可以添加示例或补充信息。\\n\\n## 限制:\\n- 不得提供未经验证的信息或个人隐私数据。\\n- 所有数据需标注来源,不确定信息用[需核实]标记。\\n- 自动过滤涉及偏见或违法内容,替换为[合规表达]。\"},{\"role\":\"user\",\"content\":\"知识库返回数据:{{knowRes}}\\n\\nLLM返回数据:{{llmRes}}\\n用户问题:{{userQue}}\"}]},\"inputParams\":[{\"field\":\"data\",\"name\":\"knowRes\",\"nodeId\":\"168290518600351744\"},{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"168290871702028288\"},{\"field\":\"content\",\"name\":\"userQue\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":166,\"width\":332}},{\"id\":\"168290871702028288\",\"type\":\"llm\",\"x\":692,\"y\":521,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:PMP知识专家\\nPMP知识专家致力于项目管理知识的传播与应用,帮助项目经理提升技能和管理能力。\\n\\n## 目标:\\n1. 为项目管理提供权威的知识支持。\\n2. 帮助项目经理解决在项目管理中遇到的实际问题。\\n\\n## 技能:\\n1. 精通项目管理的各项理论和工具。\\n2. 熟悉PMP认证流程及考试内容。\\n3. 能够进行项目风险评估与管理。\\n\\n## 工作流:\\n1. 评估项目经理的需求与挑战,识别关键问题。\\n2. 提供相关的项目管理知识、工具和最佳实践建议。\\n3. 指导项目经理制定和实施有效的项目管理计划。\\n\\n## 输出格式:\\n- 提供清晰的建议与解决方案,使用简洁明了的语言,适合项目经理理解和应用。\\n\\n## 限制:\\n- 所有建议需基于现有的PMP知识体系,避免个人主观意见。\\n- 不得提供未经验证的信息或数据,所有数据需标注来源,需核实的信息用[需核实]标记。\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":166,\"width\":332}}],\"edges\":[{\"id\":\"168290518604546048\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"168290518600351744\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"168290518600351744_input\",\"pointsList\":[{\"x\":466,\"y\":382},{\"x\":566,\"y\":382},{\"x\":427,\"y\":194},{\"x\":527,\"y\":194}]},{\"id\":\"168290861245628416\",\"type\":\"base-edge\",\"sourceNodeId\":\"168290518600351744\",\"targetNodeId\":\"168290861241434112\",\"sourceAnchorId\":\"168290518600351744_output\",\"targetAnchorId\":\"168290861241434112_input\",\"pointsList\":[{\"x\":859,\"y\":194},{\"x\":959,\"y\":194},{\"x\":915,\"y\":298},{\"x\":1015,\"y\":298}]},{\"id\":\"168290871706222592\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"168290871702028288\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"168290871702028288_input\",\"pointsList\":[{\"x\":466,\"y\":382},{\"x\":566,\"y\":382},{\"x\":426,\"y\":469},{\"x\":526,\"y\":469}]},{\"id\":\"168291272883011584\",\"type\":\"base-edge\",\"sourceNodeId\":\"168290871702028288\",\"targetNodeId\":\"168290861241434112\",\"sourceAnchorId\":\"168290871702028288_output\",\"targetAnchorId\":\"168290861241434112_input\",\"pointsList\":[{\"x\":858,\"y\":469},{\"x\":958,\"y\":469},{\"x\":915,\"y\":298},{\"x\":1015,\"y\":298}]},{\"id\":\"168292930635530240\",\"type\":\"base-edge\",\"sourceNodeId\":\"168290861241434112\",\"targetNodeId\":\"168290315671535616\",\"sourceAnchorId\":\"168290861241434112_output\",\"targetAnchorId\":\"168290315671535616_input\",\"pointsList\":[{\"x\":1347,\"y\":298},{\"x\":1447,\"y\":298},{\"x\":1378,\"y\":333},{\"x\":1478,\"y\":333}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"}]}'); + +-- ---------------------------- +-- Table structure for airag_knowledge +-- ---------------------------- +CREATE TABLE `airag_knowledge` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '知识库名称', + `descr` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `embed_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '向量模型id', + `status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of airag_knowledge +-- ---------------------------- +INSERT INTO `airag_knowledge` VALUES ('1897212906878009346', 'ghb', '2025-03-05 17:09:40', NULL, NULL, 'A04', NULL, '积木报表文档', '积木报表文档', '1891459707122499586', 'enable'); +INSERT INTO `airag_knowledge` VALUES ('1897926563148648449', 'ghb', '2025-03-07 16:25:29', 'ghb', '2025-03-11 10:04:25', 'A04', NULL, 'ghbBoot文档', 'ghbBoot文档', '1891459707122499586', 'enable'); +INSERT INTO `airag_knowledge` VALUES ('1905186756806918146', 'ghb', '2025-03-27 17:14:54', NULL, NULL, 'A04', NULL, 'PMP', NULL, '1891459707122499586', 'enable'); + +-- ---------------------------- +-- Table structure for airag_knowledge_doc +-- ---------------------------- +CREATE TABLE `airag_knowledge_doc` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id', + `knowledge_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '知识库id', + `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题', + `type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '类型', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '内容', + `status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态', + `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '元数据', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of airag_knowledge_doc +-- ---------------------------- +INSERT INTO `airag_knowledge_doc` VALUES ('1897213100944261121', 'ghb', '2025-03-05 17:10:26', 'admin', '2025-04-02 23:53:30', 'A04', NULL, '1897212906878009346', 'qa', 'text', '常见问题\n遇到问题请先升级至最新版,仍未解决可向团队反馈 点击反馈问题\n\n1. 积木报表是免费吗?\n回答: 积木报表代码不开源,但是功能可以免费使用。\n\n大屏支持离线安装,积木BI的推出,可以永久免费使用。\n针对公司用户我们提供企业版,免费版本也会持续发布。\n2. 功能操作提示 没有权限,请联系管理员分配权限!\n回答:这是因为报表针对敏感接口加了角色和权限控制,需要进行内置角色权限集成,具体见文档权限集成配置(重要)\n\n3. 积木报表怎么独立运行?\nDocker方式启动\n集成Demo启动\n4. 启动报mongo错误\n启动报错:\norg.mongodb.driver.cluster : Exception in monitor thread while connecting to \nserver localhost:27017 while accessing MongoDB with Java\n\n解决方案: 排除mongo启动默认加载 MongoAutoConfiguration\n@SpringBootApplication\n@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class})\n\n5. 报表配置ghbBoot菜单\n{{ window._CONFIG[\'domianURL\'] }}/jmreport/list?token=${token}\n\n参数说明\n\n{{ window._CONFIG[\'domianURL\'] }} :后台项目访问地址\n${token} :登录token用于权限控制\n前端组件:layouts/IframePageView 菜单配置截图\n\n\n6. 积木报表数据源支持哪些数据库?\n数据库 支持\nMySQL √\nOracle、Oracle9i √\nSqlServer、SqlServer2012 √\nPostgreSQL √\nDB2、Informix √\nMariaDB √\nSQLite、Hsqldb、Derby、H2 √\n达梦、人大金仓、神通 √\n华为高斯、虚谷、瀚高数据库、 TDengine 涛思数据 √\n阿里云PolarDB、PPAS、HerdDB √\nHive、HBase、CouchBase √\n导入Excel、csv、json文件数据集 √\nsqllite、TiDB、Doris、clickhouse、 MongoDB-BI √\nelasticsearch、mogodb √\n积木平台暂时只提供mysql脚本,其他数据库请自转 Navicat工具mysql转库oracle步骤\n7. API数据源怎样实现条件查询?\n具体请看查询条件设置\n\n8.报表集成到自己的springboot项目\n请求参数如果后台接收的实体属性里没有,后台报错:not marked as ignorable\n\n\n\n解决方法:增加application.yml的配置jackson.fail_on_unknown_properties: false\n\n\n\n9.积木报表SQL数据集中sql语句加上limit在数据预览中报错?\nsql语句写法:\n\n 报错截图:\n\n\n\n原因是sql语句末尾加上了limit,我们在后台已经默认分页,故不用在末尾加上limit,否则会报错\n\n10.如何去掉打印页面的页眉、页脚?\n在打印弹窗页面,点击“更多设置 ->选项”,去掉“页眉和页脚”前边的对勾,打印界面就不显示页眉和页脚了;\n\n\n\n11.报表能否集成到vue项目中?\n不能集成到前端项目,因为积木报表提供的JAVA依赖,只能集成到JAVA项目中。\n\n12.sql或者api解析失败的问题\nsql或者api必须有查询结果才行,不然无法解析字段 相关issue #2305\n\n13.怎样自定义打印页面设置?\n打印区域除了可以手动选择“A4、A3...”,还可以根据自己需求,自定义大小。\n\n操作参考打印区域设置\n\n14.预览时,列表数组在预览界面怎么只显示一条数据?\n(1)检查在数据集解析的时候 ,是否勾选“是否列表”;\n\n\n\n(2)设计界面拖过来的数据字段,是否为#开头;\n\n\n\n15.横向动态列分组怎么设计?\n操作参考文档\n\n16.预览页面多内容,但设计界面没有,怎么处理?\n错误样式图:\n\n\n\n解决方案: 选中多出来的地方(可多选一些地方),右键点击:删除数据,就没有了;\n\n\n\n17.积木报表SQL数据集中数据预览为什么只显示10条数据?\n为了避免大数据问题,故只取前10条数据进行展示\n\n\n\n18. 积木报表数据源怎么配置?\n添加数据源文档\n\n19. 怎样把报表集成到ghbBoot的菜单中?\n备注:大屏和报表的操作是一样的;\n\n(1)复制报表访问链接 (2)在系统管理菜单管理进行配置 (3)点击新增按钮填写信息\n\n注意:\na) 前端组件必须按照格式填写 layouts/IframePageView *用window._CONFIG[\'domianURL\']代替IP地址、端口号和项目名称,并用{{}}包起来;\nb)末尾必须携带参数,如(?sex);\nc) 是否为路由菜单:是;\n\n\n\n\n(4)角色授权 路径:在系统管理->角色授权找到自己对应的角色,鼠标放到更多->授权;\n\n勾选刚才创建的菜单\n刷新页面即可看见点击菜单\n\n\n20. 数据集配置点击确认会报错\nhttps://github.com/ghbboot/JimuReport/issues/439\nSQL state \\[null\\]; error code \\[0\\]; Error; nested exception is java.sql.SQLException: Error\n\n那么就查看mysql数据库连接驱动是版本是5.1.47,如果是那么请将驱动升级版本或降低版本,如:\n\n\n mysql\n mysql-connector-java\n 5.1.46\n true\n runtime\n\n\n22.如何把SQL数据集拼接的查询条件加到数据源语法的group by前面\n参考报表参数设置\n\n23.预览页面与设计页面不一致,在预览时出现空白行\n检查数据集是多条数据的集合,还是单条数据的对象;如果是集合使用#,如果是对象则需要使用$ 如果页面多行使用#,则会被当做多个集合,中间自动填充空白行。\n\n\n\n24.为什么配置参数后勾选查询后,下拉单选变成输入框\n参数不是字段,无法进行配置后就可以下拉单选;可配置字典code实现下拉\n\n25.一页展示一条数据,进行循环打印\n可将整页作为循环块,设置为循环块 参考文档:点击查看\n\n26.mysql数据库类型tyint被转换成了true和false\n需要在维护界面,数据源地址出拼接上\n\ntinyInt1isBit=false\n\n\n\n27.数据库里图片字段为图片链接,如何展示在报表中\n添加数据源取出图片字段,将单元格类型设置为图片即可,如下图:\n\n\n\n28. 达梦数据库提示表名不存在\n 因为达梦数据库如果不是当前用户名登录的(如SYSDBA),访问不同名的(除了SYSDBA)外,均需要模式名.表名,那么需要你如下图操作,在同名下新建表\n\n\n\n29. 积木官网添加数据源\n积木官网添加数据源需使用远程地址,不可使用localhost。\n\n32.字典code中直接输入sql语句,下拉框单选项乱序\n解决方案:可以填写 order by 进行自定义排序,如\n\nselect dict_code as value,dict_name as name from jimu_dict order by create_time\n\n注意:如果在sqlserver下需要加上top 10(10代表多少条),不然会报错,如\n\nselect top 10 dict_code as value,dict_name as name from jimu_dict order by create_time\n\n33.导出excel报错版本不匹配,java.lang.NoSuchMethodError\n将poi版本升级到4.1.2即可解决\n\n34. 如何增加列数\n列索引数量可根据需要修改 参考文档:点击查看\n\n\n\n35.sql数据集下拉选择数据源,下方列表显示空白,但是有数据\n目前为了统一规则后台返回的数据的对象均为小写(name),如果规则不匹配,请改成小写\n\n\n\n38.预览界面查询栏如何设置默认展开?\n解决方案:设置JS增强\n\n\n\nfunction init(){\n this.queryPanel = \'1\';\n}\n\n39.sqlServer存储过程中有临时表获取不到数据\n可以通过set nocount on来解决\n\n 相关issue: https://github.com/ghbboot/JimuReport/issues/726\n\n40.若依集成积木报表1.4.4+ 新建报表报错\nfreemarker.core.InvalidReferenceException\n\n升级fastjson到1.2.78\n\n\n com.alibaba\n fastjson\n 1.2.78\n\n\n相关issue:issue\n\n41.模板示例中条件查询预览失败\n没有对应的表\n\n42.打印的时候,字体加粗效果丢失\n宋体打印不支持加粗,换成默认的字体\n\n43.sqlserver提示驱动不存在\n在pom文件中添加sqlserver依赖\n\n \n com.microsoft.sqlserver\n sqljdbc4\n 4.0\n true\n runtime\n \n\n44.sqlserver下使用CONVERT函数注意事项\n不可与order by一起使用\nCONVERT函数需指定别名 如:CONVERT(varchar(7),CREATE_TIME) as CREATE_TIME\n45.能否设置隐藏的查询条件\n问题描述: 同一报表,希望不同的人看到不同的数据,目前可以通过JS增强设置初始值,但又不想让用户修改,能否提供设置查询条件隐藏的功能,这样便于数据权限的控制。 分析说明:此问题目的在于不同的人看不同的数据,提问人想设置查询条件默认值且不允许修改\n\n1.不同的人看不同的数据:可以使用系统变量 参考文档 如:\n\nsql数据集:select * from demo where create_by = \'#{sysUserCode}\'\napi数据集: http://xxx.xxx.xxx/query?create_by=#{sysUserCode}\n\n注意:此处的`sysUserCode`,是系统默认设置的登录人的账号,如果重写getUserInfo方法则需要重新设置,文档中的代码,只适用于test不可照搬,仅供参考【推荐此方案】。\n\n\n2.想设置查询条件默认值且不允许修改: js增强可以设置查询条件的默认值,也可以往查询参数对象里设置一个自定义的参数值,这个是支持的。但是,在配置数据集的时候,下方tab报表字段明细和报表参数中,会配置一些字段的信息,如果js增强定义的参数名不在这两个tab下,那么无效!所以做法如下:\n定义数据集(不需要将参数name设置为查询条件):\nsql数据集:select * from demo where create_by = \'${name}\'\napi数据集: http://xxx.xxx.xxx/query?create_by=${name}\n\n定义js增强,设置name的值:\nfunction init(){\n this.queryInfo[\'name\'] = \'scott\'\n}\n\n46. 日期默认查询,无法设置默认值为上月\n问题描述: 使用dateStr 默认取上月实现不了,用=concat(dateStr(\'yyyy\'),\'-\', dateStr(\'MM\', -1))返回2021-9,不是2021-09,少了一位。 建议实现=dateStr(\'yyyy-MM\',-1) 返回 2021-09,而不是使用天数计算偏移量。\n解决方案: 参考文档 中的升级功能\n\n47. 打印多出一页空白纸张\n解决方案: 打印导出,空白行和没有行是有区别的,界面上都是空白没区别,但是实际数据存储,空白行会占位的。\n查看控制台打印的数据:你的rows都多达90多行了,说明是之前你设计的很多历史数据没有删除行,导致多出很多空白页。\n\n\n\n48. mongodb用法\n1). 以授权的方式启动Mongo,给使用的数据库添加用户\n\n切换数据库 use test\n\n创建用户 db.createUser({user: \"root\", pwd: \"123456\", roles: \\[{ role: \"dbOwner\", db: \"test\" }\\]})\n\n参考博客:https://www.cnblogs.com/jacksoft/p/6916137.html\n\n2). mongodb-driver-sync 驱动集成用法 参考博客: https://blog.csdn.net/nyzzht123/article/details/107936552 https://www.jianshu.com/p/5186fb5a1292\n\n49、出现jsqlparser不兼容问题\n如果出现jsqlparser不兼容问题,请这么引用\n\n org.ghbframework.jimureport\n jimureport-spring-boot-starter\n {版本号}\n \n \n minidao-spring-boot-starter\n org.ghbframework\n \n \n\n\n org.ghbframework\n minidao-spring-boot-starter\n 1.8.8\n\n\n50、关于积木报表在开发、生产环境增量同步https://github.com/ghbboot/JimuReport/issues/1928\n51、数据库字段为关键词,字段作为查询条件报错\n报错信息:发现mysql下关键词字段\"year_month\"缺少\"`\"\n\nSELECT COUNT(1) total FROM ( select * from (select `year_month`,name,age from `demo`) ghb_rp_temp where year_month=? ) temp_count\n\n\n解决方案:关键词字段请用as重命名一下\n\n\n\n52、依赖redisson后编辑字典、查询字典报错:\n报错信息:\n\njava.lang.IllegalArgumentException: Cannot find cache named \'jmreport:cache:dict\' for Builder\n\n解决方法:配置文件增加:\n\nspring:\n cache:\n type: redis\n\n53、未登录的情况下导出excel和pdf报错\n解决方案:在SpringSecurityConfig页面排除导出excel和导出pdf的请求地址,其他同理\n\n\n\n .antMatchers(\"/jmreport/exportPdfStream\", \"/jmreport/exportAllExcelStream\")', 'building', NULL); +INSERT INTO `airag_knowledge_doc` VALUES ('1897926864815575042', 'ghb', '2025-03-07 16:26:41', 'ghb', '2025-03-10 17:28:33', 'A04', NULL, '1897926563148648449', 'index', 'file', '\n# 项目介绍\n\n\n `ghbBoot` 是一款基于代码生成器的`低代码开发平台` 拥有零代码能力!采用前后端分离架构:SpringBoot2.x,Ant Design&Vue,Mybatis-plus,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任何代码! ghbBoot引领新的开发模式(Online Coding模式-> 代码生成器模式-> 手工MERGE智能开发), 帮助解决Java项目70%的重复工作,让开发更多关注业务逻辑。既能快速提高开发效率,帮助公司节省成本,同时又不失灵活性!ghbBoot还独创在线开发模式(No-Code概念):在线表单配置(表单设计器)、移动配置能力、工作流配置(在线设计流程)、报表配置能力、在线图表配置、插件能力(可插拔)等等!\n\n `ghbBoot在提高UI能力`的同时,降低了前后分离的开发成本,ghbBoot还独创在线开发模式(No-Code概念),一系列在线智能开发:在线配置表单、在线配置报表、在线图表设计、在线设计流程等等。\n\n ` ghb宗旨是: `简单功能由Online Coding配置实现(在线配置表单、在线配置报表、在线图表设计、在线设计流程、在线设计表单),复杂功能由代码生成器生成进行手工Merge,既保证了智能又兼顾了灵活; \n\n 业务流程采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。\n\n\n## 技术支持\n\n* 新手指南: [快速入门](http://www.ghb.com/doc/quickstart) | [常见问题 ](http://www.ghb.com/doc/qa) | [版本日志](http://ghb.com/doc/log)\n* 视频教程:[ ghbBoot v3.7 新版视频教程](http://ghb.com/doc/video)\n* QQ交流群:⑩716488839、⑨808791225(满)、其他(满)\n* 在线演示 : [系统演示](http://boot3.ghb.com) | [APP演示](http://app.ghb.com)\n\n\n源码下载\n-----------------------------------\n\n- https://github.com/ghbboot/test\n\n\n\n## 技术架构\n-----------------------------------\n\n#### 后端\n\n- IDE建议: IDEA (必须安装lombok插件 )\n- 语言:Java 8+ (支持17)\n- 依赖管理:Maven\n- 基础框架:Spring Boot 2.7.18\n- 微服务框架: Spring Cloud Alibaba 2021.0.1.0\n- 持久层框架:MybatisPlus 3.5.3.2\n- 报表工具: JimuReport 1.7.6\n- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0\n- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking\n- 数据库连接池:阿里巴巴Druid 1.1.22\n- 日志打印:logback\n- 缓存:Redis\n- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。\n- 默认数据库脚本:MySQL5.7+\n- [其他数据库,需要自己转](https://my.oschina.net/ghb/blog/4905722)\n\n\n#### 前端\n\n- 前端IDE建议:WebStorm、Vscode\n- 采用 Vue3.0+TypeScript+Vite+Ant-Design-Vue等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能\n- 最新技术栈:Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6\n- 依赖管理:node、npm、pnpm\n\n\n\n#### 支持库\n\n| 数据库 | 支持 |\n| --- | --- |\n| MySQL | √ |\n| Oracle11g | √ |\n| Sqlserver2017 | √ |\n| PostgreSQL | √ |\n| MariaDB | √ |\n| 达梦 | √ |\n| 人大金仓 | √ |\n\n\n\n## 微服务解决方案\n\n\n- 1、服务注册和发现 Nacos √\n- 2、统一配置中心 Nacos √\n- 3、路由网关 gateway(三种加载方式) √\n- 4、分布式 http feign √\n- 5、熔断降级限流 Sentinel √\n- 6、分布式文件 Minio、阿里OSS √ \n- 7、统一权限控制 JWT + Shiro √\n- 8、服务监控 SpringBootAdmin√\n- 9、链路跟踪 Skywalking [参考文档](/java/springcloud/super/skywarking)\n- 10、消息中间件 RabbitMQ √\n- 11、分布式任务 xxl-job √ \n- 12、分布式事务 Seata\n- 13、轻量分布式日志 Loki+grafana套件\n- 14、支持 docker-compose、k8s、jenkins\n- 15、CAS 单点登录 √\n- 16、路由限流 √\n\n \n### 微服务架构图\n![微服务架构图](https://ghbos.oss-cn-beijing.aliyuncs.com/files/ghbboot_springcloud2022.png \"在这里输入图片标题\")\n\n\n\n\n\n## 系统架构图\n\n![](https://upload.ghb.com/ghb/help/ghbback/images/screenshot_1662547398792.png)\n*****\n\n\n## 系统截图\n\n### PC端\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778397612.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778435846.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778476447.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778512836.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778582144.png)\n\n### 在线接口文档\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778702243.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778737438.png)\n\n\n### 报表\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687778780458.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/up-fa52b44445db281c51d3f267dce7450d21b.gif)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687779705768.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687779725144.png)\n\n### 流程\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687779807541.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687779857971.png)\n\n![](/static/jimuImages/image_1687779966442.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687780016598.png)\n\n\n### 手机端\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687780240854.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687780264274.png)\n\n### PAD端\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687780285230.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687780328101.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687780342778.png)\n\n![](https://upload.ghb.com/ghb/help/ghbback/topwrite/assets/image_1687780373126.png)\n\n\n\n\n\n\n\n', 'complete', '{\"filePath\":\"temp/index_1741335996542.md\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1897926933086261249', 'ghb', '2025-03-07 16:26:57', 'ghb', '2025-03-10 17:28:42', 'A04', NULL, '1897926563148648449', 'qa', 'file', '1.菜单的这些配置是什么意思?\n\n![](/static/jimuImages/screenshot_1585040135427.png)\n| 配置| 描述 |\n| --- | --- |\n| 是否路由菜单 | 是:跳转路由的时候根据配置的前端组件值跳转,否:起作用的是菜单路径 |\n| 隐藏路由 | 是:左侧菜单不加载反之加载 |\n| 缓存路由 | 是:路由只加载一次即created只执行一次 |\n| 聚合路由 | 是:只要配置在该路由下面的子路由全部不会显示在左侧菜单栏 |\n| 打开方式 | 内部打开是在窗口tab里打开,外部打开浏览器tab打开 |\n\n---\n2.列表页面跳转新的路由需要展示成面包屑菜单样式:\n目前不支持,需要自行扩展\n\n---\n3.表单设计器自定义扩展\n目前只支持将设计好的表单引入自己的modal页面,扩展暂不支持\n\n---\n4.图表点击事件\n有自定义的图表js增强事件,后续补充该文档\n\n---\n20200324 LOWCOD-323\n\n---\n\n5.online报表 系统变量的使用\n`select username,id from sys_user where username = \'#{sys_user_code}\'`\n\n6.首页怎么改成自己的。\n方法一:直接修改文件:src/views/dashboard/Analysis.vue\n方法二:自定义首页页面,将首页菜单的前端组件配置为自己的文件,注意**只能修改前端组件不可修改菜单路径**\n\n![](/static/jimuImages/screenshot_1586254248894.png)\n\n\n\n7.项目编译 文件上有红色波浪线 ,点开文件红线消失,查看problem报错 xxx程序包不存在,实际该包存在\n解决方法:在Terminal 中执行 `mvn idea:idea` 再次编译即可\n\n\n\n\n', 'complete', '{\"filePath\":\"temp/QA_1741336015236.md\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1897932000963092482', 'ghb', '2025-03-07 16:47:06', 'ghb', '2025-03-07 16:47:10', 'A04', NULL, '1897212906878009346', 'index', 'file', '# 项目介绍\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/jimureport2.jpg)\n\n*****\n\n# JimuReport\n\n 积木报表,是一款免费的数据可视化报表,含报表、仪表盘和大屏设计,像搭建积木一样完全在线设计!功能涵盖:数据报表、打印设计、图表报表、门户设计、大屏设计等!\n \n - Web版报表设计器,类Excel操作风格,通过拖拽完成报表设计,所见即所得。\n - 大屏采用类word风格,可以随意拖动组件,想怎么设计怎么设计,可以像百度和阿里一样,设计出炫酷大屏!\n - 从 v1.9+ 起推出 JimuBI 产品,她的牛叉之处,同时支持仪表盘、大屏、门户 (支持交互)、移动.\n - 秉承\"简单、易用、专业\"的产品理念,极大的降低报表开发难度、缩短开发周期、节省成本。\n - 领先的企业级Web报表,支持各种复杂报表,专注于解决企业报表难题。\n - 积木BI 数据可视化,支持大屏设计和仪表盘,致力于更生动、更友好的形式呈现实时业务数据分析\n\n```\n专注于开源,打造 “专业 易用 智能” 的数据可视化报表、大屏、门户\n开源协议:`功能免费、可以商用、代码不开放`\n```\n\n\n为什么选择 JimuReport?\n-----------------------------------\n> 永久免费,支持各种复杂报表,并且傻瓜式在线设计,非常的智能,低代码时代,这个是你的首选!\n\n- 采用SpringBoot的脚手架项目,都可以快速集成\n- Web 版设计器,类似于excel操作风格,通过拖拽完成报表设计\n- 通过SQL、API等方式,将数据源与模板绑定。同时支持表达式,自动计算合计等功能,使计算工作量大大降低\n- 开发效率很高,傻瓜式在线报表设计,一分钟设计一个报表,又简单又强大\n- 支持 ECharts,目前支持28种图表,在线拖拽设计,支持SQL和API两种数据源\n- 支持分组、交叉,合计、表达式等复杂报表\n- 支持打印设计(支持套打、背景打印等)可设置打印边距、方向、页眉页脚等参数 一键快速打印 同时可实现发票套打,不动产证等精准、无缝打印\n- 可视化图表,仪表盘设计器类大屏设计,支持丰富的数据源连接和移动端,通过拖拉拽方式快速制作图表和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等等;\n- 可设计各种类型的单据、大屏,如出入库单、销售单、财务报表、合同、监控大屏、旅游数据大屏等\n- 大屏设计器支持几十种图表样式,可自由拼接、组合,设计炫酷大屏\n- 数据可视化,DataV、帆软的开源替代方案,比帆软拥有更好的体验和更简单的使用方式\n- [积木报表官网](http://jimureport.com/login) 可以在线免费制作报表和大屏,手机号一键注册,便可永久使用。大屏采用类word风格,可以随意拖动组件,想怎么设计怎么设计,可以像百度和阿里一样,设计出炫酷的可视化大屏!重要的是:免费!免费!免费!\n\n\n\n\n## 产生背景\n报表是企业IT服务必备的一项需求,但是行业内并没有一个免费好用的报表,大部分免费的报表功能较弱也不够智能,商业报表又很贵,所以有了研发一套免费报表的初衷。\n做一个什么样的报表呢?随着低代码概念的兴起,原先通过报表工具设计模板,再与系统集成的模式已经落伍,现在追求的是完全在线设计,傻瓜式的操作,实现简单易用又智能的报表!\n\n- 目前积木报表已经实现了完全在线设计,轻量级集成、类似excel的风格,像搭建积木一样在线拖拽设计报表!功能涵盖数据报表设计、打印设计、图表设计、门户设计、大屏设计等!\n- 2019年底启动积木报表研发工作,历经一年多的时间,2020-11-03第一版出炉 [v1.0-beta](https://www.oschina.net/news/119666/jimureport-1-0-beta-released)\n- 2020年的持续打磨和研发,终于在2021-1-18发布了第一个正式版本 [v1.1.05](https://www.oschina.net/news/126916/jimureport-1-1-05-released)\n- 截止到当前2024-09-14,积木报表已经完全涵盖商业BI的所有功能,包括不限于复杂报表、图表可视化、大屏、移动图表、填报等高级功能,而且拥有更好的体验和更简单的使用方式。\n- 更多版本日志查看 [版本日志](http://jimureport.com/doc/log)\n\n\n\n\n\n\n开发文档\n-----------------------------------\n\n- [快速集成]()\n- [集成源码下载](https://github.com/ghbboot/JimuReport)\n- [大屏与报表演示](http://jimureport.com/login) | [零代码体验](https://app.qiaoqiaoyun.com)\n\n\n\n\n\n\n项目介绍\n-----------------------------------\n\n- 官方网站: http://www.jimureport.com\n- 视频教程: http://jimureport.com/doc/video\n- QQ交流群:③596660273、其他群(满)\n\n\n数据库兼容 \n-----------------------------------\n> 支持国产、常规、Nosql等30多种数据源,支持以SQL的方式去查询csv、mogodb等非物理数据库。\n\n| 数据库 | 支持 |\n| --- | --- |\n| MySQL | √ |\n| Oracle、Oracle9i | √ |\n| SqlServer、SqlServer2012 | √ |\n| PostgreSQL | √ |\n| DB2、Informix | √ |\n| MariaDB | √ |\n| SQLite、Hsqldb、Derby、H2 | √ |\n| 达梦、人大金仓、神通 | √ |\n| 华为高斯、虚谷、瀚高数据库、 TDengine 涛思数据 | √ |\n| 阿里云PolarDB、PPAS、HerdDB | √ |\n| Hive、HBase、CouchBase | √ |\n| 导入Excel、csv、json文件数据集 | √ |\n| sqllite、TiDB、Doris、clickhouse、 MongoDB-BI | √ |\n| elasticsearch、mogodb | √ |\n\n\n\n报表设计效果\n-----------------------------------\n\n- 报表设计器(完全在线设计,简单易用)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/up-752b454f64ed87c798b3e8a083fbd6622d4.gif)\n\n- 打印设计(支持套打、背景打印)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862827604.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862839013.png)\n\n- 数据报表(支持分组、交叉,合计等复杂报表)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862854011.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862862414.png)\n\n- 图形报表(目前支持28种图表)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862883559.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862892649.png)\n\n\n\n大屏设计效果\n-----------------------------------\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862905901.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862938863.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862951297.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862960053.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862974786.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862983740.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687862996008.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687863002758.png)\n\n\n仪表盘设计器\n-----------------------------------\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687863014429.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687863021555.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687863028545.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687863043320.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687863050461.png)\n\n![](https://upload.ghb.com/ghb/help/jimureport/topwrite/assets/image_1687863057103.png)\n\n## 功能清单\n```\n├─报表设计器\n│ ├─数据源\n│ │ ├─支持多种数据源,如Oracle,MySQL,SQLServer,PostgreSQL等主流的数据库\n│ │ ├─支持SQL编写页面智能化,可以看到数据源下面的表清单和字段清单\n│ │ ├─支持参数\n│ │ ├─支持单数据源和多数数据源设置\n│ ├─单元格格式\n│ │ ├─边框\n│ │ ├─字体大小\n│ │ ├─字体颜色\n│ │ ├─背景色\n│ │ ├─字体加粗\n│ │ ├─支持水平和垂直的分散对齐\n│ │ ├─支持文字自动换行设置\n│ │ ├─图片设置为图片背景\n│ │ ├─支持无线行和无限列\n│ │ ├─支持设计器内冻结窗口\n│ │ ├─支持对单元格内容或格式的复制、粘贴和删除等功能\n│ │ ├─等等\n│ ├─报表元素\n│ │ ├─文本类型:直接写文本;支持数值类型的文本设置小数位数\n│ │ ├─图片类型:支持上传一张图表;支持图片动态生成\n│ │ ├─图表类型\n│ │ ├─函数类型\n│ │ └─支持求和\n│ │ └─平均值\n│ │ └─最大值\n│ │ └─最小值\n│ ├─背景\n│ │ ├─背景颜色设置\n│ │ ├─背景图片设置\n│ │ ├─背景透明度设置\n│ │ ├─背景大小设置\n│ ├─数据字典\n│ ├─报表打印\n│ │ ├─自定义打印\n│ │ └─医药笺、逮捕令、介绍信等自定义样式设计打印\n│ │ ├─简单数据打印\n│ │ └─出入库单、销售表打印\n│ │ └─带参数打印\n│ │ └─分页打印\n│ │ ├─套打\n│ │ └─不动产证书打印\n│ │ └─发票打印\n│ ├─数据报表\n│ │ ├─分组数据报表\n│ │ └─横向数据分组\n│ │ └─纵向数据分组\n│ │ └─多级循环表头分组\n│ │ └─横向分组小计\n│ │ └─纵向分组小计(预计2021.03.08)\n│ │ └─合计\n│ │ ├─交叉报表\n│ │ ├─明细表\n│ │ ├─带条件查询报表\n│ │ ├─表达式报表\n│ │ ├─带二维码/条形码报表\n│ │ ├─多表头复杂报表(预计2021.03.08发布)\n│ │ ├─主子报表(预计2021.03.08发布)\n│ │ ├─预警报表(预计2021.03.08发布)\n│ │ ├─数据钻取报表(预计2021.03.08发布)\n│ ├─图形报表\n│ │ ├─柱形图\n│ │ ├─折线图\n│ │ ├─饼图\n│ │ ├─折柱图\n│ │ ├─散点图\n│ │ ├─漏斗图\n│ │ ├─雷达图\n│ │ ├─象形图\n│ │ ├─地图\n│ │ ├─仪盘表\n│ │ ├─关系图\n│ │ ├─图表背景\n│ │ ├─图表动态刷新\n│ │ ├─图表数据字典\n│ ├─参数\n│ │ ├─参数配置\n│ │ ├─参数管理\n│ ├─导入导出\n│ │ ├─支持导入Excel\n│ │ ├─支持导出Excel、pdf;支持导出excel、pdf带参数\n│ ├─打印设置\n│ │ ├─打印区域设置\n│ │ ├─打印机设置\n│ │ ├─预览\n│ │ ├─打印页码设置\n├─大屏设计器\n│ ├─系统功能\n│ │ ├─静态数据源和动态数据源设置\n│ │ ├─基础功能\n│ │ └─支持拖拽设计\n│ │ └─支持增、删、改、查大屏\n│ │ └─支持复制大屏数据和样式\n│ │ └─支持大屏预览、分享\n│ │ └─支持系统自动保存数据,同时支持手动恢复数据\n│ │ └─支持设置大屏密码\n│ │ └─支持对组件图层的删除、组合、上移、下移、置顶、置底等\n│ │ ├─背景设置\n│ │ └─大屏的宽度和高度设置\n│ │ └─大屏简介设置\n│ │ └─背景颜色、背景图片设置\n│ │ └─封面图设置\n│ │ └─缩放比例设置\n│ │ └─环境地址设置\n│ │ └─水印设置\n│ │ ├─地图设置\n│ │ └─添加地图\n│ │ └─地图数据隔离\n│ ├─图表\n│ │ ├─柱形图\n│ │ ├─折线图\n│ │ ├─折柱图\n│ │ ├─饼图\n│ │ ├─象形图\n│ │ ├─雷达图\n│ │ ├─散点图\n│ │ ├─漏斗图\n│ │ ├─文本框\n│ │ ├─跑马灯\n│ │ ├─超链接\n│ │ ├─实时时间\n│ │ ├─地图\n│ │ ├─全国物流地图\n│ │ ├─地理坐标地图\n│ │ ├─城市派件地图\n│ │ ├─图片\n│ │ ├─图片框\n│ │ ├─轮播图\n│ │ ├─滑动组件\n│ │ ├─iframe\n│ │ ├─video\n│ │ ├─翻牌器\n│ │ ├─环形图\n│ │ ├─进度条\n│ │ ├─仪盘表\n│ │ ├─字浮云\n│ │ ├─表格\n│ │ ├─选项卡\n│ │ ├─万能组件\n└─其他模块\n └─更多功能开发中。。\n```\n\n \n\n', 'complete', '{\"filePath\":\"temp/readme_1741337223240.md\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905186930719539201', 'ghb', '2025-03-27 17:15:36', 'ghb', '2025-03-27 17:15:43', 'A04', NULL, '1905186756806918146', 'part1', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/01第一部分第1章_1743066923748.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905186968325668866', 'ghb', '2025-03-27 17:15:45', 'ghb', '2025-03-27 17:15:48', 'A04', NULL, '1905186756806918146', 'part2', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/02第一部分第2章_1743066943040.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187001037045761', 'ghb', '2025-03-27 17:15:52', 'ghb', '2025-03-27 17:15:57', 'A04', NULL, '1905186756806918146', 'part3', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/03第一部分第3章_1743066951733.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187042770370561', 'ghb', '2025-03-27 17:16:02', 'ghb', '2025-03-27 17:16:07', 'A04', NULL, '1905186756806918146', 'part4', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/04第一部分第4章_1743066960385.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187113339535361', 'ghb', '2025-03-27 17:16:19', 'ghb', '2025-03-27 17:16:25', 'A04', NULL, '1905186756806918146', 'part5', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/05第一部分第5章_1743066977792.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187163981561857', 'ghb', '2025-03-27 17:16:31', 'ghb', '2025-03-27 17:16:39', 'A04', NULL, '1905186756806918146', 'part6', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/06第一部分第6章_1743066990164.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187246412218369', 'ghb', '2025-03-27 17:16:51', 'ghb', '2025-03-27 17:16:54', 'A04', NULL, '1905186756806918146', 'part7', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/07第一部分第7章_1743067007831.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187348543520770', 'ghb', '2025-03-27 17:17:15', 'ghb', '2025-03-27 17:17:20', 'A04', NULL, '1905186756806918146', 'part8', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/08第一部分第8章_1743067032663.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187596229754881', 'ghb', '2025-03-27 17:18:14', 'ghb', '2025-03-27 17:18:21', 'A04', NULL, '1905186756806918146', 'part9', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/09第一部分第9章_1743067087019.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187666618564609', 'ghb', '2025-03-27 17:18:31', 'ghb', '2025-03-27 17:18:34', 'A04', NULL, '1905186756806918146', 'part10', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/10第一部分第10章_1743067109769.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187818494312449', 'ghb', '2025-03-27 17:19:07', 'ghb', '2025-03-27 17:19:15', 'A04', NULL, '1905186756806918146', 'part11', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/11第一部分第11章_1743067121732.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187888337862657', 'ghb', '2025-03-27 17:19:24', 'ghb', '2025-03-27 17:19:31', 'A04', NULL, '1905186756806918146', 'part12', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/12第一部分第12章_1743067158952.pdf\"}'); +INSERT INTO `airag_knowledge_doc` VALUES ('1905187920491397122', 'ghb', '2025-03-27 17:19:32', 'ghb', '2025-03-27 17:19:38', 'A04', NULL, '1905186756806918146', 'part13', 'file', NULL, 'complete', '{\"filePath\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/13第一部分第13章_1743067170886.pdf\"}'); + +-- ---------------------------- +-- Table structure for airag_model +-- ---------------------------- +CREATE TABLE `airag_model` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '名称', + `provider` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '供应者', + `model_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模型名称', + `credential` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '凭证信息', + `base_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'API域名', + `model_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模型类型', + `model_params` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模型参数', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of airag_model +-- ---------------------------- +INSERT INTO `airag_model` VALUES ('1890232564262739969', 'ghb', '2025-02-14 10:52:16', 'admin', '2025-04-02 22:20:37', 'A04', NULL, 'OpenAI', 'OPENAI', 'gpt-4o-mini', '{\"apiKey\":\"sk-cgQRNc3mWb3YtdO9C0F6AcBc86\"}', 'https://api.gpt.ge', 'LLM', '{\"temperature\":0.2,\"topP\":0.7,\"presencePenalty\":0.5,\"frequencyPenalty\":0.5,\"maxTokens\":null}'); +INSERT INTO `airag_model` VALUES ('1891459707122499586', 'ghb', '2025-02-17 20:08:30', 'admin', '2025-04-02 22:20:34', 'A04', NULL, 'OpenAI向量', 'OPENAI', 'text-embedding-ada-002', '{\"apiKey\":\"sk-cgQRNc3mWb3YtdO9C0F6Ac\"}', 'https://api.v3.cm/v1', 'EMBED', NULL); +INSERT INTO `airag_model` VALUES ('1897481367743143938', 'ghb', '2025-03-06 10:56:26', 'admin', '2025-04-02 22:20:31', 'A04', NULL, 'deepseek', 'DEEPSEEK', 'deepseek-chat', '{\"apiKey\":\"sk-ff138aa9896945468ec\"}', 'https://api.deepseek.com/v1', 'LLM', NULL); +INSERT INTO `airag_model` VALUES ('1897883052995006466', 'ghb', '2025-03-07 13:32:35', 'admin', '2025-04-02 23:53:33', 'A04', NULL, '智谱', 'ZHIPU', 'glm-4-flash', '{\"apiKey\":\"522f6486bc6944b2ba346f054c0184e0.\"}', 'https://open.bigmodel.cn/', 'LLM', NULL); +INSERT INTO `airag_model` VALUES ('1897884353107611650', 'ghb', '2025-03-07 13:37:45', 'admin', '2025-04-02 22:20:22', 'A04', NULL, '智谱向量', 'ZHIPU', 'Embedding-3', '{\"apiKey\":\"522f6486bc6944b2ba346f054c0184e0.\"}', 'https://open.bigmodel.cn', 'EMBED', '{\"temperature\":0.7,\"topP\":0.7,\"presencePenalty\":null,\"frequencyPenalty\":null,\"maxTokens\":null}'); + +SET FOREIGN_KEY_CHECKS = 1; + + +-- ---------------------------- +-- Records of sys_dict +-- ---------------------------- + +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1894701158027554818', 'AI应用类型', 'ai_app_type', NULL, 0, 'ghb', '2025-02-26 18:48:53', NULL, NULL, 0, 0, NULL); +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1891672414555860993', '知识库文档类型', 'know_doc_type', NULL, 0, 'ghb', '2025-02-18 10:13:44', NULL, NULL, 0, 0, NULL); +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1891671216561975297', '知识库类型', 'airag_know_type', NULL, 1, 'ghb', '2025-02-18 10:08:58', NULL, NULL, 0, 0, NULL); +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1891456510739890177', '模型类型', 'model_type', NULL, 0, 'ghb', '2025-02-17 19:55:48', NULL, NULL, 0, 0, NULL); +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1890229208685322242', '模型提供者', 'model_provider', NULL, 0, 'ghb', '2025-02-14 10:38:57', NULL, NULL, 0, 0, NULL); + +-- ---------------------------- +-- Records of sys_dict_item +-- ---------------------------- + +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1894701332930031618', '1894701158027554818', '高级编排', 'chatFLow', NULL, 2, 1, 'ghb', '2025-02-26 18:49:34', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1894701277019959298', '1894701158027554818', '简单配置', 'chatSimple', NULL, 1, 1, 'ghb', '2025-02-26 18:49:21', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1891672567924781058', '1891672414555860993', '网页', 'web', NULL, 1, 1, 'ghb', '2025-02-18 10:14:20', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1891672540963794946', '1891672414555860993', '文件', 'file', NULL, 1, 1, 'ghb', '2025-02-18 10:14:14', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1891672501432479746', '1891672414555860993', '文本', 'text', NULL, 1, 1, 'ghb', '2025-02-18 10:14:05', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1891458099609354241', '1891456510739890177', '向量模型', 'EMBED', NULL, 1, 1, 'ghb', '2025-02-17 20:02:07', 'ghb', '2025-02-17 20:39:01', NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1891456733029613569', '1891456510739890177', '语言模型', 'LLM', NULL, 1, 1, 'ghb', '2025-02-17 19:56:41', 'ghb', '2025-02-17 20:02:15', NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1890230437670920194', '1890229208685322242', 'Ollama', 'OLLAMA', NULL, 1, 1, 'ghb', '2025-02-14 10:43:50', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1890230384159989762', '1890229208685322242', 'DeepSeek', 'DEEPSEEK', NULL, 1, 1, 'ghb', '2025-02-14 10:43:37', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1890230305948803073', '1890229208685322242', '通义千问', 'QWEN', NULL, 1, 1, 'ghb', '2025-02-14 10:43:18', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1890230107835047937', '1890229208685322242', '千帆大模型', 'QIANFAN', NULL, 1, 1, 'ghb', '2025-02-14 10:42:31', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1890230018852888577', '1890229208685322242', '智谱AI', 'ZHIPU', NULL, 1, 1, 'ghb', '2025-02-14 10:42:10', 'ghb', '2025-02-14 10:42:42', NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1890229967585910786', '1890229208685322242', 'OpenAI', 'OPENAI', NULL, 1, 1, 'ghb', '2025-02-14 10:41:58', 'ghb', '2025-02-14 10:42:48', NULL); diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.1_1__all_upgrade.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.1_1__all_upgrade.sql new file mode 100644 index 0000000..edd7705 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.1_1__all_upgrade.sql @@ -0,0 +1,63 @@ +-- -- author:sunjianlei---date:20250417--for: 【QQYUN-11093】【online】添加Online报表、Online图表的租户ID字段 +ALTER TABLE `onl_cgreport_head` + ADD COLUMN `tenant_id` int NULL DEFAULT 0 COMMENT '租户ID' AFTER `content`; + +-- ---author:chenrui-date:20250418-----for: 添加AI流程:积木报表AI引擎 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`) VALUES ('1909856345692065793', 'ghb', '2025-04-09 14:30:11', 'ghb', '2025-04-17 20:32:02', 'A04', NULL, 'ghb', 'JimuReport AI引擎', '', '', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'173365501230346240\')).to(\n THEN(\n llm.tag(\'172956395755208704\'),\n end.tag(\'172957153284259840\')\n ).tag(\"172956395755208704\"),\n THEN(\n llm.tag(\'173365800833675264\'),\n end.tag(\'173366253646540800\')\n ).tag(\"173365800833675264\"),\n end.tag(\'173366439085109248\'),\n THEN(\n llm.tag(\'175149164433014784\'),\n end.tag(\'175153953988444160\')\n ).tag(\"175149164433014784\"),\n THEN(\n llm.tag(\'175505963485245440\'),\n end.tag(\'175506006644633600\')\n ).tag(\"175505963485245440\"),\n THEN(\n llm.tag(\'175807569594040320\'),\n end.tag(\'175808663015538688\')\n ).tag(\"175807569594040320\")\n ).tag(\'173365501230346240\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":262,\"y\":458,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"ddl\",\"name\":\"表结构\",\"type\":\"string\",\"required\":true},{\"field\":\"dbtype\",\"name\":\"数据库类型\",\"type\":\"string\",\"required\":true},{\"field\":\"bizType\",\"name\":\"业务类型\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"172956395755208704\",\"type\":\"llm\",\"x\":1166,\"y\":160,\"properties\":{\"text\":\"生成sql\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"千问coder\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:SQL生成助手\\n你是一个专业的SQL语句生成工具,能够根据用户提供的描述和表结构自动生成高效的SQL查询语句。\\n\\n## 目标:\\n- 根据用户的描述生成准确的SQL查询语句。\\n\\n## 技能:\\n1. 理解用户提供的需求和表结构。\\n2. 自动构建符合SQL语法的查询语句。\\n3. 优化生成的SQL以提高执行效率。\\n\\n## 工作流:\\n1. 接收用户描述和表结构信息。\\n2. 分析用户需求,确定所需的SQL操作类型(如查询、插入、更新、删除)。\\n3. 根据分析结果生成相应的SQL语句。\\n\\n## 输出格式:\\n- 生成的SQL语句应为标准格式,如:SELECT * FROM table_name ;\\n- 将输出的SQL语句格式化\\n- 只输出sql语句,不要额外解释,不要md语法,不要换行符,不要有sql注释。\\n\\n## 限制:\\n\\n- 除非明确说明,否则不要生成查询条件\\n- 确保生成的SQL语句符合数据库的语法要求,确保sql能直接执行。\\n- 确保字段和表能正确对应。\"},{\"role\":\"user\",\"content\":\"表结构:\\n{{ddl}}\\n---------\\n数据库类型:\\n{{dbtype}}\\n----------\\n需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"dbtype\",\"name\":\"dbtype\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"172957153284259840\",\"type\":\"end\",\"x\":1643,\"y\":129,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"sql\",\"nodeId\":\"172956395755208704\"}],\"height\":114,\"width\":332}},{\"id\":\"173365501230346240\",\"type\":\"switch\",\"x\":688,\"y\":536,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genSql\"}],\"next\":\"172956395755208704\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genJsonRows\"}],\"next\":\"173365800833675264\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"chooseTables\"}],\"next\":\"175149164433014784\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genChart\"}],\"next\":\"175505963485245440\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"intentCheck\"}],\"next\":\"175807569594040320\"}],\"else\":{\"next\":\"173366439085109248\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":222,\"width\":332}},{\"id\":\"173365800833675264\",\"type\":\"llm\",\"x\":1167,\"y\":368,\"properties\":{\"text\":\"生成rows\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"千问coder\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"根据以下数据以及用户需求生成符合要求的表格数据结构。\\n\\n\\n## 工作流程:\\n\\n\\n1. 根据用户需求选择一个合适的数据集\\n2. 根据数据集和需求,生成表格数据。\\n2. 最终输出json\\n\\n\\n## 数据集格式说明:\\n```\\n{\\n  \\\"code\\\": \\\"a\\\",\\n  \\\"title\\\": \\\"a\\\",\\n  \\\"isList\\\": \\\"1\\\",\\n  \\\"children\\\": [\\n    {\\n      \\\"title\\\": \\\"total_sales\\\",\\n      \\\"fieldText\\\": \\\"总销量\\\"\\n    },\\n    {\\n      \\\"title\\\": \\\"total_returns\\\",\\n      \\\"fieldText\\\": \\\"总退货数量\\\"\\n    }\\n  ]\\n}\\n```\\n* code:数据集变量名\\n* isList:为”1”表示集合,“0”表示对象\\n* children:为字段列表,包含title(字段名)和fieldText(展示名)\\n⸻\\n## 表格数据结构说明:\\n```\\n{\\n  \\\"0\\\": { \\\"cells\\\": {} }, // 行号作为键\\n  \\\"1\\\": { \\\"cells\\\": { // 每行下有 cells 对象,key 是列号\\n      \\\"1\\\": { \\\"text\\\": \\\"#{a.total_sales}\\\" },\\n      \\\"2\\\": { \\\"text\\\": \\\"#{a.name}\\\" }\\n  }},\\n  \\\"len\\\": 200 // 表格总行数(可固定为200)\\n}\\n```\\n* 每行以序号作为键\\n* 每列下包含 text 为占位符,${} 用于对象,#{} 用于集合\\n* 可包含 style 等附加样式信息\\n⸻\\n\\n\\n## 填充规则:\\n1. 若 isList = 1(集合):\\n  * 第N行(如 \\\"0\\\")为字段标题:使用 children.fieldText 填充\\n  * 第N+1行(如 \\\"1\\\")为字段占位符:使用 `#{code.title}` 填充\\n  * 所有字段占位符占用一行,所有标题占用一行\\n2. 若 isList = 0(对象):\\n  * 每字段占两列,低N列填字段标题,N+1列填占位符 `${code.title}`\\n  * 共两组:第一组在第n列,第二组在第N+2列\\n\\n\\n⸻\\n\\n\\n## 输出格式\\n* 直接返回JSON数据,不要解释,不要md语法,不要换行符,不要有注释。\\n\\n\\n\\n\\n## 特别注意\\n- 字段的占位必须是`#{}`或`${}`,不能缺失大括号。\\n- 用户描述的序号需要减一才是下标\\n- 确保输出的json格式正确。\\n- 只需要生成一套表格数据。\"},{\"role\":\"user\",\"content\":\"用户数据集:\\n{{ddl}}\\n用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"173366253646540800\",\"type\":\"end\",\"x\":1643,\"y\":336,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"rows\",\"nodeId\":\"173365800833675264\"}],\"height\":114,\"width\":332}},{\"id\":\"173366439085109248\",\"type\":\"end\",\"x\":1158,\"y\":1220,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"error:选择正确的业务类型\"},\"inputParams\":[],\"outputParams\":[],\"height\":136,\"width\":332}},{\"id\":\"175149164433014784\",\"type\":\"llm\",\"x\":1164,\"y\":598,\"properties\":{\"text\":\"选择表\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"千问coder\",\"temperature\":0.7}},\"history\":2,\"messages\":[{\"role\":\"system\",\"content\":\"## 任务\\n根据用户需求,从下方数据库表列表中选择所有关联的表名称。\\n\\n\\n## 数据库表列表(格式:表名 | 注释)\\n{{ddl}}\\n\\n## 输出规则\\n1. 严格按JSON数组格式输出,例如:[\\\"order\\\"]。\\n2. 仅包含表名称,无需注释。\\n3. **禁止添加列表外的表**。\\n4. 表的选择范围可以适当大一些。\\n4. 无业务相关性时输出空数组:[]\\n\\n\\n请回复纯JSON,不要包含其他内容。\"},{\"role\":\"user\",\"content\":\"用户需求:{{question}}\"}]},\"inputParams\":[{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175153953988444160\",\"type\":\"end\",\"x\":1643,\"y\":564,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"tables\",\"nodeId\":\"175149164433014784\"}],\"height\":114,\"width\":332}},{\"id\":\"175505963485245440\",\"type\":\"llm\",\"x\":1166,\"y\":802,\"properties\":{\"text\":\"生成图表\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"千问coder\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"根据以下数据以及用户需求生成符合格式要求的图表数据。\\n\\n\\n## 工作流程:\\n\\n\\n1. 根据用户需求选择一个合适的数据集\\n2. 根据数据集和需求,从图表列表中选择一个合适的图标类型。\\n3. 组装最终输出的json\\n\\n\\n⸻\\n## 可选的图表如下(标识|描述):\\n\\n\\n- 1维图表\\n    - bar.simple|普通柱形图\\n    - bar.background|带背景柱形图\\n    - bar.horizontal|横向柱形图\\n    - line.simple|普通折线图\\n    - line.area|面积堆积折线图\\n    - line.smooth|平滑曲线折线图\\n    - line.step|阶梯折线图\\n    - pie.simple|普通饼图\\n    - pie.doughnut|环状饼图\\n    - pie.rose|南丁格尔玫瑰饼图\\n    - scatter.simple|普通散点图\\n    - funnel.simple|普通漏斗图\\n    - funnel.pyramid|金字塔漏斗图\\n    - pictorial.spirits|普通象形图\\n    - map.scatter|点地图\\n    - gauge.simple|360°仪表盘\\n    - gauge.simple180|180°仪表盘\\n- 2维\\n    - bar.multi|多数据对比柱形图\\n    - bar.negative|正负条形图\\n    - bar.stack|堆叠柱形图\\n    - bar.stack.horizontal|堆叠条形图\\n    - bar.multi.horizontal|多数据条形柱状图\\n    - line.multi|多数据对比折线图\\n    - mixed.linebar|普通折柱图\\n    - scatter.bubble|气泡散点图\\n    - radar.basic|普通雷达图\\n    - radar.custom|圆形雷达图\\n⸻\\n## 数据集格式说明:\\n```\\n{\\n  \\\"dbId\\\": \\\"1069915169263800320\\\",\\n  \\\"code\\\": \\\"a\\\",\\n  \\\"title\\\": \\\"a\\\",\\n  \\\"isList\\\": \\\"1\\\",\\n  \\\"type\\\": \\\"0\\\",\\n  \\\"children\\\": [\\n    {\\n      \\\"title\\\": \\\"total_sales\\\",\\n      \\\"fieldText\\\": \\\"total_sales\\\"\\n    },\\n    {\\n      \\\"title\\\": \\\"total_returns\\\",\\n      \\\"fieldText\\\": \\\"total_returns\\\"\\n    }\\n  ]\\n}\\n```\\n* code:数据集变量名\\n* isList:为”1”表示集合,“0”表示对象\\n* children:为字段列表,包含title(字段名)和fieldText(展示名)\\n* type:0|sql,1|api,2|code,3|json\\n⸻\\n## 输出json格式\\n{\\n    \\\"dataType\\\": \\\"sql\\\",\\n    \\\"apiStatus\\\": \\\"0\\\",\\n    \\\"apiUrl\\\": \\\"\\\",\\n    \\\"dataId\\\": \\\"1069898455939633152\\\",\\n    \\\"axisX\\\": \\\"supplier_name\\\",\\n    \\\"axisY\\\": \\\"total_returns\\\",\\n    \\\"series\\\": \\\"material_name\\\",\\n    \\\"yText\\\": \\\"total_returns\\\",\\n    \\\"xText\\\": \\\"supplier_name\\\",\\n    \\\"dbCode\\\": \\\"a\\\",\\n    \\\"isCustomPropName\\\": false,\\n    \\\"chartType\\\": \\\"line.multi\\\",\\n    \\\"id\\\": \\\"0aGl4PUfbIfy8BMF\\\",\\n    \\\"run\\\": 1,\\n    \\\"title\\\": \\\"\\\",\\n}\\n* dataType:与数据集type对应(0|sql,1|api,2|code,3|json)\\n* dataId:对应数据集dbId\\n* dbCode:对应数据集的code\\n* axisX:分类属性,从数据集字段中取值(fieldText)\\n* axisY:值属性,从数据集字段中取值(fieldText)\\n* series: 系列,从数据集字段中取值(fieldText)\\n* xText:分类属性显示,从数据集字段中取值(title)\\n* yText:值属性显示,从数据集字段中取值(title)\\n* chartType:图表的标识\\n* title:为这个图表起一个标题\\n* isCustomPropName: 如果是api数据集,该值为true\\n* apiStatus: 如果是api数据集则等于\\\"1\\\",否则\\\"0\\\"\\n\\n\\n## 输出格式\\n* 直接返回JSON数据,不要解释,不要md语法,不要换行符,不要有注释。\\n* 确保输出的json格式正确完整。\"},{\"role\":\"user\",\"content\":\"## 用户数据集:\\n{{ddl}}\\n## 用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175506006644633600\",\"type\":\"end\",\"x\":1643,\"y\":769,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"chart\",\"nodeId\":\"175505963485245440\"}],\"height\":114,\"width\":332}},{\"id\":\"175807569594040320\",\"type\":\"llm\",\"x\":1166,\"y\":1018,\"properties\":{\"text\":\"意图识别\",\"options\":{\"model\":{\"modeId\":\"1897835602959695874\",\"params\":{\"model\":\"qwen-max\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"请根据用户需求与数据集设计,综合判断应执行的工作流步骤,并为每个步骤生成简洁明确的需求描述,同时选择最合适的数据集。\\n\\n\\n---\\n\\n\\n## 可选步骤(格式:标识 | 功能说明)\\n\\n\\n- `genJsonRows` | 生成报表(可选)  \\n- `genChart` | 生成图表(可选)  \\n\\n\\n> **注意:** 至少选择一个步骤,亦可同时选择两者。\\n\\n\\n---\\n\\n\\n## 数据集格式\\n\\n\\n```json\\n{\\n  \\\"dbId\\\": \\\"1069915169263800320\\\",\\n  \\\"code\\\": \\\"a\\\",\\n  \\\"title\\\": \\\"a\\\",\\n  \\\"isList\\\": \\\"1\\\",\\n  \\\"type\\\": \\\"0\\\",\\n  \\\"children\\\": [\\n    {\\n      \\\"title\\\": \\\"total_sales\\\",\\n      \\\"fieldText\\\": \\\"total_sales\\\"\\n    },\\n    {\\n      \\\"title\\\": \\\"total_returns\\\",\\n      \\\"fieldText\\\": \\\"total_returns\\\"\\n    }\\n  ]\\n}\\n* code:数据集变量名\\n* isList:为”1”表示集合,“0”表示对象\\n* children:为字段列表,包含title(展示名)和fieldText(字段名)\\n* type:0|sql,1|api,2|code,3|json\\n\\n\\n⸻\\n## 输出格式\\n\\n\\n```\\n步骤标识1|需求描述1|数据集code,步骤标识2|需求描述2|数据集code\\n```\\n\\n\\n* 各步骤之间用英文逗号,分隔\\n* 不得添加额外说明,不要md语法,不要换行符,不要有注释。\"},{\"role\":\"user\",\"content\":\"## 用户数据集:\\n{{ddl}}\\n## 用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175808663015538688\",\"type\":\"end\",\"x\":1643,\"y\":985,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"intent\",\"nodeId\":\"175807569594040320\"}],\"height\":114,\"width\":332}}],\"edges\":[{\"id\":\"172957153288454144\",\"type\":\"base-edge\",\"sourceNodeId\":\"172956395755208704\",\"targetNodeId\":\"172957153284259840\",\"sourceAnchorId\":\"172956395755208704_output\",\"targetAnchorId\":\"172957153284259840_input\",\"pointsList\":[{\"x\":1332,\"y\":101},{\"x\":1432,\"y\":101},{\"x\":1377,\"y\":103},{\"x\":1477,\"y\":103}]},{\"id\":\"173365501234540544\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"173365501230346240\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"173365501230346240_input\",\"pointsList\":[{\"x\":428,\"y\":443},{\"x\":528,\"y\":443},{\"x\":422,\"y\":456},{\"x\":522,\"y\":456}]},{\"id\":\"173366253650735104\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365800833675264\",\"targetNodeId\":\"173366253646540800\",\"sourceAnchorId\":\"173365800833675264_output\",\"targetAnchorId\":\"173366253646540800_input\",\"pointsList\":[{\"x\":1333,\"y\":309},{\"x\":1433,\"y\":309},{\"x\":1377,\"y\":310},{\"x\":1477,\"y\":310}]},{\"id\":\"173372961415852032\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"172956395755208704\",\"sourceAnchorId\":\"173365501230346240_source_if\",\"targetAnchorId\":\"172956395755208704_input\",\"pointsList\":[{\"x\":854,\"y\":490},{\"x\":954,\"y\":490},{\"x\":900,\"y\":101},{\"x\":1000,\"y\":101}]},{\"id\":\"173372967073968128\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"173365800833675264\",\"sourceAnchorId\":\"173365501230346240_case_2\",\"targetAnchorId\":\"173365800833675264_input\",\"pointsList\":[{\"x\":854,\"y\":516},{\"x\":954,\"y\":516},{\"x\":901,\"y\":309},{\"x\":1001,\"y\":309}]},{\"id\":\"173372974988619776\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"173366439085109248\",\"sourceAnchorId\":\"173365501230346240_source_else\",\"targetAnchorId\":\"173366439085109248_input\",\"pointsList\":[{\"x\":854,\"y\":620},{\"x\":954,\"y\":620},{\"x\":892,\"y\":1183},{\"x\":992,\"y\":1183}]},{\"id\":\"175149164437209088\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175149164433014784\",\"sourceAnchorId\":\"173365501230346240_case_3\",\"targetAnchorId\":\"175149164433014784_input\",\"pointsList\":[{\"x\":854,\"y\":542},{\"x\":954,\"y\":542},{\"x\":898,\"y\":539},{\"x\":998,\"y\":539}]},{\"id\":\"175153997969915904\",\"type\":\"base-edge\",\"sourceNodeId\":\"175149164433014784\",\"targetNodeId\":\"175153953988444160\",\"sourceAnchorId\":\"175149164433014784_output\",\"targetAnchorId\":\"175153953988444160_input\",\"pointsList\":[{\"x\":1330,\"y\":539},{\"x\":1430,\"y\":539},{\"x\":1377,\"y\":538},{\"x\":1477,\"y\":538}]},{\"id\":\"175505963489439744\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175505963485245440\",\"sourceAnchorId\":\"173365501230346240_case_4\",\"targetAnchorId\":\"175505963485245440_input\",\"pointsList\":[{\"x\":854,\"y\":568},{\"x\":954,\"y\":568},{\"x\":900,\"y\":743},{\"x\":1000,\"y\":743}]},{\"id\":\"175506006648827904\",\"type\":\"base-edge\",\"sourceNodeId\":\"175505963485245440\",\"targetNodeId\":\"175506006644633600\",\"sourceAnchorId\":\"175505963485245440_output\",\"targetAnchorId\":\"175506006644633600_input\",\"pointsList\":[{\"x\":1332,\"y\":743},{\"x\":1432,\"y\":743},{\"x\":1377,\"y\":743},{\"x\":1477,\"y\":743}]},{\"id\":\"175807569598234624\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175807569594040320\",\"sourceAnchorId\":\"173365501230346240_case_5\",\"targetAnchorId\":\"175807569594040320_input\",\"pointsList\":[{\"x\":854,\"y\":594},{\"x\":954,\"y\":594},{\"x\":900,\"y\":959},{\"x\":1000,\"y\":959}]},{\"id\":\"175808663019732992\",\"type\":\"base-edge\",\"sourceNodeId\":\"175807569594040320\",\"targetNodeId\":\"175808663015538688\",\"sourceAnchorId\":\"175807569594040320_output\",\"targetAnchorId\":\"175808663015538688_input\",\"pointsList\":[{\"x\":1332,\"y\":959},{\"x\":1432,\"y\":959},{\"x\":1377,\"y\":959},{\"x\":1477,\"y\":959}]}]}', 'enable', '{\"outputs\":[{\"field\":\"text\",\"name\":\"intent\",\"nodeId\":\"175807569594040320\"},{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"ddl\",\"name\":\"表结构\",\"type\":\"string\"},{\"field\":\"dbtype\",\"name\":\"数据库类型\",\"type\":\"string\"},{\"field\":\"bizType\",\"name\":\"业务类型\",\"type\":\"string\"}]}'); + +-- ---author:wangshuai-date:20250418-----for: 添加ocr识别示例 菜单 +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1912753560201089025', '1438108178010202113', 'OCR识别', '/ai/ocr', 'super/airag/ocr/AiOcrList', 1, '', NULL, 1, NULL, '0', 1.00, 0, 'ant-design:scan-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-04-17 14:22:41', 'admin', '2025-04-18 10:07:40', 0, 0, NULL, 0); + +-- ---author:chenrui-date:20250424-----for: 修改示例流程入参,删除多余的入参 +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2025-02-19 20:13:03', `update_by` = 'ghb', `update_time` = '2025-04-24 12:25:08', `sys_org_code` = 'A04', `tenant_id` = NULL, `application_name` = 'ghb', `name` = '示例_条件分支', `descr` = NULL, `icon` = NULL, `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'a448577f-9824-415b-97f6-72543fcb619d\')).to(\n end.tag(\'91a7df56-107c-4f83-b1e4-b1b7e392c4e3\'),\n end.tag(\'162160595291774976\')\n ).tag(\'a448577f-9824-415b-97f6-72543fcb619d\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":515,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"type\":\"switch\",\"x\":731,\"y\":486,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"question\",\"operator\":\"CONTAINS\",\"value\":\"ghb\"}],\"next\":\"162160595291774976\"}],\"else\":{\"next\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3\",\"type\":\"end\",\"x\":1085,\"y\":662,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}不包含ghb\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"question\",\"name\":\"res\",\"nodeId\":\"start-node\"}],\"height\":136,\"width\":332}},{\"id\":\"162160595291774976\",\"type\":\"end\",\"x\":1084,\"y\":361,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}包含ghb\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"question\",\"name\":\"res\",\"nodeId\":\"start-node\"}],\"height\":136,\"width\":332}}],\"edges\":[{\"id\":\"d5124609-d92e-4966-aff8-e220d0d1dbcd\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"a448577f-9824-415b-97f6-72543fcb619d_input\",\"pointsList\":[{\"x\":466,\"y\":500},{\"x\":566,\"y\":500},{\"x\":465,\"y\":458},{\"x\":565,\"y\":458}]},{\"id\":\"ea3d924a-e4fd-4bb4-bc8a-d1f07119a7eb\",\"type\":\"base-edge\",\"sourceNodeId\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"targetNodeId\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3\",\"sourceAnchorId\":\"a448577f-9824-415b-97f6-72543fcb619d_source_else\",\"targetAnchorId\":\"91a7df56-107c-4f83-b1e4-b1b7e392c4e3_input\",\"pointsList\":[{\"x\":897,\"y\":518},{\"x\":997,\"y\":518},{\"x\":819,\"y\":625},{\"x\":919,\"y\":625}]},{\"id\":\"162161801783320576\",\"type\":\"base-edge\",\"sourceNodeId\":\"a448577f-9824-415b-97f6-72543fcb619d\",\"targetNodeId\":\"162160595291774976\",\"sourceAnchorId\":\"a448577f-9824-415b-97f6-72543fcb619d_source_if\",\"targetAnchorId\":\"162160595291774976_input\",\"pointsList\":[{\"x\":897,\"y\":492},{\"x\":997,\"y\":492},{\"x\":818,\"y\":324},{\"x\":918,\"y\":324}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"question\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":true,\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1892185624983658497'; +UPDATE `airag_flow` SET `create_by` = 'ghb', `create_time` = '2025-02-21 11:11:36', `update_by` = 'ghb', `update_time` = '2025-04-24 12:27:02', `sys_org_code` = 'A04', `tenant_id` = NULL, `application_name` = 'ghb', `name` = '示例_LLM', `descr` = '', `icon` = NULL, `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'e9f3470a-f129-4baf-880a-294d7b3bff93\'),\n end.tag(\'9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":273,\"y\":419,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"type\":\"llm\",\"x\":708,\"y\":435,\"properties\":{\"text\":\"llm\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你将扮演一个人物角色李白,以下是关于这个角色的详细设定,请根据这些信息来构建你的回答。 \\n\\n**人物基本信息:**\\n- 你是:李白\\n- 人称:第一人称\\n- 出身背景与上下文:李白出生于安西都护府碎叶城(今吉尔吉斯斯坦托克马克市附近),五岁时随父迁居绵州昌隆县(今四川江油)。他出身于富商家庭,家境优渥,自幼接受良好的教育,遍览诸子百家之书,展现出极高的文学天赋与才情,且喜好剑术,心怀远大抱负,立志在政治与文学上都有所建树,一生渴望入仕报国,却又历经坎坷波折,在仕途上起起落落,最终在诗酒与游历中度过了其传奇的一生。\\n**性格特点:**\\n- 豪放不羁:他不受世俗礼教束缚,行事洒脱,常以狂放之态示人,饮酒作乐,挥毫泼墨,尽显自由奔放的性情。例如 “我本楚狂人,凤歌笑孔丘”,敢于对传统观念表达自己的不羁态度。\\n- 自信豁达:坚信自己的才华与能力,面对困境与挫折时总能以豁达胸怀看待。像 “天生我材必有用,千金散尽还复来”,即便遭遇仕途不顺、生活潦倒,依然对未来充满信心。\\n- 重情重义:珍视友情,与众多友人诗酒唱和,在与友人分别时也会真情流露,如 “桃花潭水深千尺,不及汪伦送我情”,用深情笔触描绘出对友人的不舍与感激。\\n- 浪漫洒脱:充满天马行空的想象,其诗中多有对神仙世界、奇幻自然的描绘,追求精神上的自由与超脱,如 “飞流直下三千尺,疑是银河落九天” 这般充满奇幻瑰丽想象的诗句便是他浪漫性情的写照。\\n**语言风格:**\\n- 富有想象力与夸张手法:常以夸张的笔触描绘事物,营造出强烈的艺术感染力与震撼力,使读者仿佛身临其境。如 “白发三千丈,缘愁似个长”,用极度夸张的白发长度来形容愁绪之深。 \\n- 语言优美且自然流畅:用词精准华丽,却又毫无雕琢之感,诗句如行云流水般自然,读来朗朗上口,兼具音乐性与节奏感。像 “故人西辞黄鹤楼,烟花三月下扬州。孤帆远影碧空尽,唯见长江天际流”,文字优美,意境深远,节奏明快。 \\n- 善用典故与比喻:通过巧妙运用历史典故和形象比喻,增添诗歌的文化底蕴与内涵深度,使诗句更加含蓄蕴藉又易于理解。例如 “闲来垂钓碧溪上,忽复乘舟梦日边”,借用姜太公垂钓与伊尹梦日的典故表达自己对仕途的期待。 \\n**人际关系:**\\n- 与杜甫:李白与杜甫堪称唐代诗坛的双子星,二人相互倾慕,结下深厚情谊。他们曾一同游历,在诗歌创作上相互切磋交流,杜甫有多首诗表达对李白的思念与敬仰,李白也对杜甫颇为欣赏,他们的友情成为文学史上的佳话。\\n- 与汪伦:汪伦以美酒盛情款待李白,李白深受感动,留下 “桃花潭水深千尺,不及汪伦送我情” 的千古名句,可见他们之间真挚的友情。\\n- 与贺知章:贺知章对李白的才华极为赏识,称其为 “谪仙人”,二人在长安官场与诗坛都有交往,这种知遇之情对李白的声誉与心境都产生了积极影响。\\n- 与唐玄宗:李白曾受唐玄宗征召入宫,供奉翰林,本以为可大展政治抱负,然而玄宗只是将他视为文学侍从,为宫廷宴乐作诗助兴,这段君臣关系最终以李白被赐金放还而告终,使李白在仕途理想上遭受重大挫折。\\n**经典台词或口头禅:**\\n- 台词1:“仰天大笑出门去,我辈岂是蓬蒿人。” 表达出其对自身才华的自信以及即将踏入仕途、一展宏图的豪迈与喜悦。 \\n- 台词2:“安能摧眉折腰事权贵,使我不得开心颜。” 体现出他不向权贵低头,坚守人格尊严与精神自由的高尚情操与不屈性格。\\n- 台词2:“长风破浪会有时,直挂云帆济沧海。” 展现出面对困难时的乐观态度与坚定信念,相信总有一天能够乘风破浪,实现理想抱负。\\n\\n要求: \\n- 根据上述提供的角色设定,以第一人称视角进行表达。 \\n- 在回答时,尽可能地融入该角色的性格特点、语言风格以及其特有的口头禅或经典台词。\\n- 如果适用的话,在适当的地方加入()内的补充信息,如动作、神情等,以增强对话的真实感和生动性。 \"},{\"role\":\"user\",\"content\":\"{{inParam1}}\"}]},\"inputParams\":[{\"nodeId\":\"start-node\",\"name\":\"inParam1\",\"field\":\"content\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"text\"}],\"width\":332,\"height\":180}},{\"id\":\"9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1\",\"type\":\"end\",\"x\":1186,\"y\":467,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"回复:{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"nodeId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"text\"}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"ab818150-d4e5-4be2-8d80-31b7f48dc318\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93_input\",\"pointsList\":[{\"x\":439,\"y\":404},{\"x\":539,\"y\":404},{\"x\":442,\"y\":376},{\"x\":542,\"y\":376}]},{\"id\":\"158143255481139200\",\"type\":\"base-edge\",\"sourceNodeId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93\",\"targetNodeId\":\"9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1\",\"sourceAnchorId\":\"e9f3470a-f129-4baf-880a-294d7b3bff93_output\",\"targetAnchorId\":\"9eb6f5c7-94a6-421f-aa39-7cfd7cec44f1_input\",\"pointsList\":[{\"x\":874,\"y\":376},{\"x\":974,\"y\":376},{\"x\":920,\"y\":430},{\"x\":1020,\"y\":430}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1892774140436287490'; +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2025-03-04 13:45:01', `update_by` = 'ghb', `update_time` = '2025-04-24 12:26:54', `sys_org_code` = 'A04', `tenant_id` = '', `application_name` = 'ghb', `name` = '示例_分类器', `descr` = NULL, `icon` = NULL, `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(classifier.tag(\'159899349256073216\')).to(\n end.tag(\'159899421356158976\'),\n end.tag(\'159899641326432256\'),\n end.tag(\'159900616165302272\'),\n end.tag(\'160202618435485696\')\n ).tag(\'159899349256073216\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":334,\"y\":653,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"159899349256073216\",\"type\":\"classifier\",\"x\":714,\"y\":719,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"gpt-4o-mini\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户问的问题是关于编程的\",\"next\":\"159899421356158976\"},{\"category\":\"用户问的问题是关于食谱的\",\"next\":\"159899641326432256\"},{\"category\":\"其他问题\",\"next\":\"159900616165302272\"}],\"else\":{\"next\":\"160202618435485696\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":224,\"width\":332}},{\"id\":\"159899421356158976\",\"type\":\"end\",\"x\":1144,\"y\":566,\"properties\":{\"text\":\"结束1\",\"options\":{\"outputText\":true,\"outputContent\":\"分类:{{分类索引}}\\n-------\\n{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"nodeId\":\"159899349256073216\"},{\"field\":\"content\",\"name\":\"回复内容\",\"nodeId\":\"159899349256073216\"}],\"height\":136,\"width\":332}},{\"id\":\"159899641326432256\",\"type\":\"end\",\"x\":1144,\"y\":715,\"properties\":{\"text\":\"结束2\",\"options\":{\"outputText\":true,\"outputContent\":\"分类:{{分类索引}}\\n-------\\n{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"nodeId\":\"159899349256073216\"},{\"field\":\"content\",\"name\":\"回复内容\",\"nodeId\":\"159899349256073216\"}],\"height\":136,\"width\":332}},{\"id\":\"159900616165302272\",\"type\":\"end\",\"x\":1144,\"y\":864,\"properties\":{\"text\":\"结束3\",\"options\":{\"outputText\":true,\"outputContent\":\"分类:{{分类索引}}\\n-------\\n{{回复内容}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"nodeId\":\"159899349256073216\"},{\"field\":\"content\",\"name\":\"回复内容\",\"nodeId\":\"159899349256073216\"}],\"height\":136,\"width\":332}},{\"id\":\"160202618435485696\",\"type\":\"end\",\"x\":1146,\"y\":1001,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"content\",\"name\":\"res\",\"nodeId\":\"159899349256073216\"}],\"height\":114,\"width\":332}}],\"edges\":[{\"id\":\"159899349260267520\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"159899349256073216\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"159899349256073216_input\",\"pointsList\":[{\"x\":500,\"y\":638},{\"x\":600,\"y\":638},{\"x\":448,\"y\":638},{\"x\":548,\"y\":638}]},{\"id\":\"159899421356158977\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"159899421356158976\",\"sourceAnchorId\":\"159899349256073216_case_1\",\"targetAnchorId\":\"159899421356158976_input\",\"pointsList\":[{\"x\":880,\"y\":672},{\"x\":980,\"y\":672},{\"x\":878,\"y\":529},{\"x\":978,\"y\":529}]},{\"id\":\"159899706925346816\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"159899641326432256\",\"sourceAnchorId\":\"159899349256073216_case_2\",\"targetAnchorId\":\"159899641326432256_input\",\"pointsList\":[{\"x\":880,\"y\":716},{\"x\":980,\"y\":716},{\"x\":878,\"y\":678},{\"x\":978,\"y\":678}]},{\"id\":\"159900640542597120\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"159900616165302272\",\"sourceAnchorId\":\"159899349256073216_case_3\",\"targetAnchorId\":\"159900616165302272_input\",\"pointsList\":[{\"x\":880,\"y\":760},{\"x\":980,\"y\":760},{\"x\":878,\"y\":827},{\"x\":978,\"y\":827}]},{\"id\":\"177966745116012544\",\"type\":\"base-edge\",\"sourceNodeId\":\"159899349256073216\",\"targetNodeId\":\"160202618435485696\",\"sourceAnchorId\":\"159899349256073216_case_else\",\"targetAnchorId\":\"160202618435485696_input\",\"pointsList\":[{\"x\":880,\"y\":804},{\"x\":980,\"y\":804},{\"x\":880,\"y\":975},{\"x\":980,\"y\":975}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"},{\"field\":\"content\",\"name\":\"res\",\"nodeId\":\"159899349256073216\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"内容\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"}]}' WHERE `id` = '1896799016980885506'; +UPDATE `airag_flow` SET `create_by` = 'ghb', `create_time` = '2025-03-06 11:01:45', `update_by` = 'ghb', `update_time` = '2025-04-24 12:27:58', `sys_org_code` = 'A04', `tenant_id` = NULL, `application_name` = 'ghb', `name` = '示例_脚本组件', `descr` = NULL, `icon` = NULL, `chain` = 'THEN(\n start.tag(\'start-node\'),\n code_160582647542648832.tag(\'code_160582647542648832\'),\n end.tag(\'160583273626406912\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":455,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"内容\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"question\",\"name\":\"内容2\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"code_160582647542648832\",\"type\":\"code\",\"x\":786,\"y\":488,\"properties\":{\"text\":\"脚本执行\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main(params) {\\n return {\\n result: params.arg1 + \'_拼接_\' + params.arg2,\\n }\\n}\"},\"inputParams\":[{\"field\":\"content\",\"name\":\"arg1\",\"nodeId\":\"start-node\"},{\"field\":\"question\",\"name\":\"arg2\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":158,\"width\":332}},{\"id\":\"160583273626406912\",\"type\":\"end\",\"x\":1272,\"y\":466,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{res}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"code_160582647542648832\"}],\"height\":114,\"width\":332}}],\"edges\":[{\"id\":\"160582647546843136\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"code_160582647542648832\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"code_160582647542648832_input\",\"pointsList\":[{\"x\":466,\"y\":440},{\"x\":566,\"y\":440},{\"x\":520,\"y\":440},{\"x\":620,\"y\":440}]},{\"id\":\"160583273626406913\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_160582647542648832\",\"targetNodeId\":\"160583273626406912\",\"sourceAnchorId\":\"code_160582647542648832_output\",\"targetAnchorId\":\"160583273626406912_input\",\"pointsList\":[{\"x\":952,\"y\":440},{\"x\":1052,\"y\":440},{\"x\":1006,\"y\":440},{\"x\":1106,\"y\":440}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"code_160582647542648832\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"内容\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"}]}' WHERE `id` = '1897482706871164929'; +UPDATE `airag_flow` SET `create_by` = 'ghb', `create_time` = '2025-03-06 11:58:23', `update_by` = 'ghb', `update_time` = '2025-04-14 14:11:45', `sys_org_code` = 'A04', `tenant_id` = NULL, `application_name` = 'ghb', `name` = '示例_java增强', `descr` = NULL, `icon` = NULL, `chain` = 'THEN(\n start.tag(\'start-node\'),\n enhanceJava.tag(\'160591592557232128\'),\n end.tag(\'160595080985034752\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":471,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"question\",\"name\":\"问题1\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":true},{\"field\":\"content\",\"name\":\"问题2\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":122,\"width\":332}},{\"id\":\"160591592557232128\",\"type\":\"enhanceJava\",\"x\":786,\"y\":503,\"properties\":{\"text\":\"Java增强\",\"options\":{\"enhance\":{\"type\":\"class\",\"path\":\"org.ghb.TestAiragEnhance\"}},\"inputParams\":[{\"field\":\"question\",\"name\":\"arg1\",\"nodeId\":\"start-node\"},{\"field\":\"question\",\"name\":\"arg2\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":188,\"width\":332}},{\"id\":\"160595080985034752\",\"type\":\"end\",\"x\":1272,\"y\":492,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"160591592557232128\"}],\"height\":166,\"width\":332}}],\"edges\":[{\"id\":\"160591592565620736\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160591592557232128\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160591592557232128_input\",\"pointsList\":[{\"x\":466,\"y\":441},{\"x\":566,\"y\":441},{\"x\":520,\"y\":440},{\"x\":620,\"y\":440}]},{\"id\":\"160595080989229056\",\"type\":\"base-edge\",\"sourceNodeId\":\"160591592557232128\",\"targetNodeId\":\"160595080985034752\",\"sourceAnchorId\":\"160591592557232128_output\",\"targetAnchorId\":\"160595080985034752_input\",\"pointsList\":[{\"x\":952,\"y\":440},{\"x\":1052,\"y\":440},{\"x\":1006,\"y\":440},{\"x\":1106,\"y\":440}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"question\",\"name\":\"问题1\",\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"问题2\",\"type\":\"string\"}]}' WHERE `id` = '1897496956167577601'; + +-- ---author:chenrui-date:20250428-----for: 修改示例流程-全部脚本,删除python脚本节点 +UPDATE `airag_flow` SET `chain`='THEN(\n start.tag(\'start-node\'),\n llm.tag(\'160650416019521536\'),\n WHEN(\n code_160652991133433856.tag(\'code_160652991133433856\'),\n code_166081977564753920.tag(\'code_166081977564753920\'),\n code_166090618376253440.tag(\'code_166090618376253440\'),\n code_167835393352683520.tag(\'code_167835393352683520\')\n ).tag(\"code_160652991133433856\"),\n end.tag(\'160656278891560960\')\n).tag(\"start-node\")',`design`='{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":418,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"160650416019521536\",\"type\":\"llm\",\"x\":693,\"y\":462,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":null,\"topP\":0.9,\"presencePenalty\":0.1,\"frequencyPenalty\":0.1}},\"history\":4,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位严厉的长辈,面对用户的问题,要以一种带着隐隐批评,暗示问题简单、用户还有很多需要学习的态度来回复。通过大模型模拟李白来对话,回答用户提出的各种问题。\\n\\n\\n## 技能\\n### 技能 1: 回答问题\\n1. 当用户提出问题时,先简要评价问题较为简单,然后给出回答。\\n2. 回答完问题后,适当提及用户还需要加强学习、增长见识等内容。\\n\\n\\n## 限制:\\n- 回复内容必须逻辑清晰、语言通顺,符合严厉长辈的角色设定。 \\n\\n\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"code_160652991133433856\",\"type\":\"code\",\"x\":1131,\"y\":87,\"properties\":{\"text\":\"js\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main(params) {\\n if(params.llmRes){\\n let resLength = params.llmRes.length\\n params.llmRes = params.llmRes + \'\\\\n字数:\'+resLength\\n }\\n return {\\n result: params.llmRes,\\n }\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":158,\"width\":332}},{\"id\":\"160656278891560960\",\"type\":\"end\",\"x\":1653,\"y\":449,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"js:{{res}}\\ngroovy:{{res1}}\\nkotlin:{{res2}}\\npython:{{res3}}\\naviator:{{res4}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"code_160652991133433856\"},{\"field\":\"result\",\"name\":\"res1\",\"nodeId\":\"code_166081977564753920\"},{\"field\":\"result\",\"name\":\"res2\",\"nodeId\":\"code_166090618376253440\"},{\"field\":\"result\",\"name\":\"res3\",\"nodeId\":\"code_167828303175372800\"},{\"field\":\"result\",\"name\":\"res4\",\"nodeId\":\"code_167835393352683520\"}],\"height\":136,\"width\":332}},{\"id\":\"code_166081977564753920\",\"type\":\"code\",\"x\":1141,\"y\":266,\"properties\":{\"text\":\"groovy\",\"options\":{\"codeType\":\"groovy\",\"code\":\"def main(params) {\\n if (params.llmRes) {\\n def resLength = params.llmRes.length()\\n params.llmRes += \\\"\\\\n字数:\\\" + resLength\\n }\\n return [result: params.llmRes]\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":158,\"width\":332}},{\"id\":\"code_166090618376253440\",\"type\":\"code\",\"x\":1141,\"y\":449,\"properties\":{\"text\":\"kotlin\",\"options\":{\"codeType\":\"kotlin\",\"code\":\"fun main(params: MutableMap): Map {\\n if (params[\\\"llmRes\\\"] is String) {\\n val llmRes = params[\\\"llmRes\\\"] as String\\n val resLength = llmRes.length\\n params[\\\"llmRes\\\"] = \\\"$llmRes\\\\n字数1:$resLength\\\"\\n }\\n return mapOf(\\\"result\\\" to params[\\\"llmRes\\\"])\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":158,\"width\":332}},{\"id\":\"code_167835393352683520\",\"type\":\"code\",\"x\":1141,\"y\":667,\"properties\":{\"text\":\"aviator\",\"options\":{\"codeType\":\"aviator\",\"code\":\"let llmRes = params.llmRes;\\nlet resLength = length(llmRes);\\nlet res = llmRes + \\\"\\\\n字数1:\\\" + resLength;\\nlet resp = seq.map(\\\"result\\\",res);\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}}],\"edges\":[{\"id\":\"160650416019521537\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160650416019521536\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160650416019521536_input\",\"pointsList\":[{\"x\":466,\"y\":403},{\"x\":566,\"y\":403},{\"x\":427,\"y\":403},{\"x\":527,\"y\":403}]},{\"id\":\"160652991137628160\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_160652991133433856\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_160652991133433856_input\",\"pointsList\":[{\"x\":859,\"y\":403},{\"x\":959,\"y\":403},{\"x\":865,\"y\":39},{\"x\":965,\"y\":39}]},{\"id\":\"160656278899949568\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_160652991133433856\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_160652991133433856_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1297,\"y\":39},{\"x\":1397,\"y\":39},{\"x\":1387,\"y\":412},{\"x\":1487,\"y\":412}]},{\"id\":\"166082001409372160\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_166081977564753920\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_166081977564753920_input\",\"pointsList\":[{\"x\":859,\"y\":403},{\"x\":959,\"y\":403},{\"x\":875,\"y\":218},{\"x\":975,\"y\":218}]},{\"id\":\"166082017557442560\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_166081977564753920\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_166081977564753920_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1307,\"y\":218},{\"x\":1407,\"y\":218},{\"x\":1387,\"y\":412},{\"x\":1487,\"y\":412}]},{\"id\":\"166090719580614656\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_166090618376253440\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_166090618376253440_input\",\"pointsList\":[{\"x\":859,\"y\":403},{\"x\":959,\"y\":403},{\"x\":875,\"y\":401},{\"x\":975,\"y\":401}]},{\"id\":\"166090725280673792\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_166090618376253440\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_166090618376253440_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1307,\"y\":401},{\"x\":1407,\"y\":401},{\"x\":1387,\"y\":412},{\"x\":1487,\"y\":412}]},{\"id\":\"167835393356877824\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_167835393352683520\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_167835393352683520_input\",\"pointsList\":[{\"x\":859,\"y\":403},{\"x\":959,\"y\":403},{\"x\":875,\"y\":619},{\"x\":975,\"y\":619}]},{\"id\":\"167836988980817920\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167835393352683520\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_167835393352683520_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1307,\"y\":619},{\"x\":1407,\"y\":619},{\"x\":1387,\"y\":412},{\"x\":1487,\"y\":412}]}]}' WHERE `id`='1897552224058400770'; +-- ---author:chenrui-date:20250430-----for: [QQYUN-11718]【AI】积木报表对接AI流程编排接口展示报表 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`) VALUES ('1917103567932604417', 'ghb', '2025-04-29 14:28:03', 'ghb', '2025-04-30 12:06:49', 'A04', NULL, 'ghb', '示例_数据查询引擎', '', '', 'THEN(\n start.tag(\'start-node\'),\n enhanceJava.tag(\'180204885804785664\'),\n llm.tag(\'180211780498169856\'),\n end.tag(\'180204420713758720\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":376,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"pageNo\",\"name\":\"页码\",\"type\":\"number\",\"required\":false},{\"field\":\"pageSize\",\"name\":\"每页数量\",\"type\":\"number\",\"required\":false},{\"field\":\"bizData\",\"name\":\"文件路径\",\"type\":\"string\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"180204420713758720\",\"type\":\"end\",\"x\":1648,\"y\":398,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"180211780498169856\"}],\"height\":136,\"width\":332}},{\"id\":\"180204885804785664\",\"type\":\"enhanceJava\",\"x\":794,\"y\":421,\"properties\":{\"text\":\"Java 增强\",\"options\":{\"enhance\":{\"type\":\"spring\",\"path\":\"jimuDataReader\"}},\"inputParams\":[{\"field\":\"bizData\",\"name\":\"bizData\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"datas\",\"name\":\"返回结果\",\"type\":\"object[]\",\"required\":false},{\"field\":\"fields\",\"name\":\"字段列表\",\"type\":\"string[]\",\"required\":false}],\"height\":180,\"width\":332}},{\"id\":\"180211780498169856\",\"type\":\"llm\",\"x\":1229,\"y\":419,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"将以下数据整理成目标格式输出\\n## 工作流程:\\n1. 读取用户的数据\\n3. 组装最终输出的json\\n⸻\\n## 数据示例:data\\n```\\n{{data}}\\n```\\n## 数据示例:fields\\n```\\n{{fields}}\\n```\\n⸻\\n## 输出json格式\\n{\\n  \\\"data\\\": [\\n    {\\n      \\\"amount\\\": \\\"100\\\",\\n      \\\"month\\\": \\\"1\\\",\\n      \\\"areaname\\\": \\\"华北\\\",\\n      \\\"year\\\": \\\"2020\\\",\\n      \\\"price\\\": \\\"5\\\",\\n      \\\"dept\\\": \\\"河北\\\",\\n      \\\"settleamount\\\": \\\"100\\\"\\n    },\\n    {\\n      \\\"amount\\\": \\\"200\\\",\\n      \\\"month\\\": \\\"2\\\",\\n      \\\"areaname\\\": \\\"华北\\\",\\n      \\\"year\\\": \\\"2020\\\",\\n      \\\"price\\\": \\\"5\\\",\\n      \\\"dept\\\": \\\"河北\\\",\\n      \\\"settleamount\\\": \\\"200\\\"\\n    },\\n  ],\\n  \\\"total\\\": 100,\\n  \\\"count\\\": 100\\n}\\n* total: 分页数,对应数据的总分页数\\n* count: 数据总数,对应数据的总数\\n\\n\\n## 输出格式\\n* 直接返回JSON数据,不要解释,不要md语法,不要换行符,不要有注释。\\n* 统一将key转换成英文,下划线分隔\\n* 确保输出的json格式正确完整。\"},{\"role\":\"user\",\"content\":\"将数据转换为目标格式\"}]},\"inputParams\":[{\"field\":\"datas\",\"name\":\"data\",\"nodeId\":\"180204885804785664\"},{\"field\":\"fields\",\"name\":\"fileds\",\"nodeId\":\"180204885804785664\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"180211805085179904\",\"type\":\"base-edge\",\"sourceNodeId\":\"180211780498169856\",\"targetNodeId\":\"180204420713758720\",\"sourceAnchorId\":\"180211780498169856_output\",\"targetAnchorId\":\"180204420713758720_input\",\"pointsList\":[{\"x\":1395,\"y\":360},{\"x\":1495,\"y\":360},{\"x\":1382,\"y\":361},{\"x\":1482,\"y\":361}]},{\"id\":\"180228761381183488\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"180204885804785664\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"180204885804785664_input\",\"pointsList\":[{\"x\":466,\"y\":361},{\"x\":566,\"y\":361},{\"x\":528,\"y\":362},{\"x\":628,\"y\":362}]},{\"id\":\"180511280701620224\",\"type\":\"base-edge\",\"sourceNodeId\":\"180204885804785664\",\"targetNodeId\":\"180211780498169856\",\"sourceAnchorId\":\"180204885804785664_output\",\"targetAnchorId\":\"180211780498169856_input\",\"pointsList\":[{\"x\":960,\"y\":362},{\"x\":1060,\"y\":362},{\"x\":963,\"y\":360},{\"x\":1063,\"y\":360}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"pageNo\",\"name\":\"页码\",\"required\":false,\"type\":\"number\"},{\"field\":\"pageSize\",\"name\":\"每页数量\",\"required\":false,\"type\":\"number\"},{\"field\":\"bizData\",\"name\":\"文件路径\",\"required\":false,\"type\":\"string\"}]}'); + +-- -- author:sunjianlei---date:20250509--for: 【QQYUN-12064】AI流程增加发布功能(添加注释) +ALTER TABLE `airag_flow` + MODIFY COLUMN `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态(enable=启用、disable=禁用、release=发布)' AFTER `design`; + +-- -- author:sunjianlei---date:20250513--for: 【QQYUN-12064】AI应用增加发布功能(添加注释) +ALTER TABLE `airag_app` + MODIFY COLUMN `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态(enable=启用、disable=禁用、release=发布)' AFTER `flow_id`; +-- -- author:liusq---date:20250509--for: QQYUN-10237 【流程审批】工单授权,没有对online表单的授权(修改注释) + +-- ---author:chenrui-date:20250520-----for: [QQYUN-12543]AI测试类未提交 +UPDATE `airag_flow` SET `create_by` = 'ghb', `create_time` = '2025-03-06 11:58:23', `update_by` = 'ghb', `update_time` = '2025-05-20 10:16:28', `sys_org_code` = 'A04', `tenant_id` = NULL, `application_name` = 'ghb', `name` = '示例_java增强', `descr` = NULL, `icon` = NULL, `chain` = 'THEN(\n start.tag(\'start-node\'),\n enhanceJava.tag(\'160591592557232128\'),\n end.tag(\'160595080985034752\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":456,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"question\",\"name\":\"问题1\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"content\",\"name\":\"问题2\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"160591592557232128\",\"type\":\"enhanceJava\",\"x\":786,\"y\":499,\"properties\":{\"text\":\"Java增强\",\"options\":{\"enhance\":{\"type\":\"spring\",\"path\":\"testAiragEnhance\"}},\"inputParams\":[{\"field\":\"question\",\"name\":\"arg1\",\"nodeId\":\"start-node\"},{\"field\":\"question\",\"name\":\"arg2\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":158,\"width\":332}},{\"id\":\"160595080985034752\",\"type\":\"end\",\"x\":1272,\"y\":477,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{res}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"160591592557232128\"}],\"height\":136,\"width\":332}}],\"edges\":[{\"id\":\"160591592565620736\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160591592557232128\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160591592557232128_input\",\"pointsList\":[{\"x\":466,\"y\":441},{\"x\":566,\"y\":441},{\"x\":520,\"y\":440},{\"x\":620,\"y\":440}]},{\"id\":\"160595080989229056\",\"type\":\"base-edge\",\"sourceNodeId\":\"160591592557232128\",\"targetNodeId\":\"160595080985034752\",\"sourceAnchorId\":\"160591592557232128_output\",\"targetAnchorId\":\"160595080985034752_input\",\"pointsList\":[{\"x\":952,\"y\":440},{\"x\":1052,\"y\":440},{\"x\":1006,\"y\":440},{\"x\":1106,\"y\":440}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"question\",\"name\":\"问题1\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":true,\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"问题2\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1897496956167577601'; + +-- ---author:chenrui-date:20250529-----for: [QQYUN-12441]【积木报表】AI生成报表 一直提示失败 后台也没有日志 +UPDATE `airag_flow` SET `create_by` = 'ghb', `create_time` = '2025-04-09 14:30:11', `update_by` = 'admin', `update_time` = '2025-05-28 16:39:13', `sys_org_code` = 'A04', `tenant_id` = NULL, `application_name` = 'ghb', `name` = 'JimuReport AI引擎', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'173365501230346240\')).to(\n THEN(\n llm.tag(\'172956395755208704\'),\n end.tag(\'172957153284259840\')\n ).tag(\"172956395755208704\"),\n THEN(\n llm.tag(\'173365800833675264\'),\n end.tag(\'173366253646540800\')\n ).tag(\"173365800833675264\"),\n end.tag(\'173366439085109248\'),\n THEN(\n llm.tag(\'175149164433014784\'),\n end.tag(\'175153953988444160\')\n ).tag(\"175149164433014784\"),\n THEN(\n llm.tag(\'175505963485245440\'),\n end.tag(\'175506006644633600\')\n ).tag(\"175505963485245440\"),\n THEN(\n llm.tag(\'175807569594040320\'),\n end.tag(\'175808663015538688\')\n ).tag(\"175807569594040320\")\n ).tag(\'173365501230346240\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":262,\"y\":458,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"ddl\",\"name\":\"表结构\",\"type\":\"string\",\"required\":true},{\"field\":\"dbtype\",\"name\":\"数据库类型\",\"type\":\"string\",\"required\":true},{\"field\":\"bizType\",\"name\":\"业务类型\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"172956395755208704\",\"type\":\"llm\",\"x\":1166,\"y\":160,\"properties\":{\"text\":\"生成sql\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:SQL生成助手\\n你是一个专业的SQL语句生成工具,能够根据用户提供的描述和表结构自动生成高效的SQL查询语句。\\n\\n## 目标:\\n- 根据用户的描述生成准确的SQL查询语句。\\n\\n## 技能:\\n1. 理解用户提供的需求和表结构。\\n2. 自动构建符合SQL语法的查询语句。\\n3. 优化生成的SQL以提高执行效率。\\n\\n## 工作流:\\n1. 接收用户描述和表结构信息。\\n2. 分析用户需求,确定所需的SQL操作类型(如查询、插入、更新、删除)。\\n3. 根据分析结果生成相应的SQL语句。\\n\\n## 输出格式:\\n- 生成的SQL语句应为标准格式,如:SELECT * FROM table_name ;\\n- 将输出的SQL语句格式化\\n- 只输出sql语句,不要额外解释,不要md语法,不要换行符,不要有sql注释。\\n\\n## 限制:\\n\\n- 除非明确说明,否则不要生成查询条件\\n- 确保生成的SQL语句符合数据库的语法要求,确保sql能直接执行。\\n- 确保字段和表能正确对应。\"},{\"role\":\"user\",\"content\":\"表结构:\\n{{ddl}}\\n---------\\n数据库类型:\\n{{dbtype}}\\n----------\\n需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"dbtype\",\"name\":\"dbtype\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"172957153284259840\",\"type\":\"end\",\"x\":1643,\"y\":129,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"sql\",\"nodeId\":\"172956395755208704\"}],\"height\":114,\"width\":332}},{\"id\":\"173365501230346240\",\"type\":\"switch\",\"x\":688,\"y\":536,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genSql\"}],\"next\":\"172956395755208704\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genJsonRows\"}],\"next\":\"173365800833675264\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"chooseTables\"}],\"next\":\"175149164433014784\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genChart\"}],\"next\":\"175505963485245440\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"intentCheck\"}],\"next\":\"175807569594040320\"}],\"else\":{\"next\":\"173366439085109248\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":222,\"width\":332}},{\"id\":\"173365800833675264\",\"type\":\"llm\",\"x\":1167,\"y\":368,\"properties\":{\"text\":\"生成rows\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"根据以下数据以及用户需求生成符合要求的表格数据结构。\\n\\n\\n\\n\\n## 工作流程:\\n\\n\\n\\n\\n1. 根据用户需求选择一个合适的数据集\\n2. 根据数据集和需求,生成表格数据。\\n2. 最终输出json\\n\\n\\n\\n\\n## 数据集格式说明:\\n```\\n{\\n \\\"code\\\": \\\"a\\\",\\n \\\"title\\\": \\\"a\\\",\\n \\\"isList\\\": \\\"1\\\",\\n \\\"children\\\": [\\n {\\n \\\"title\\\": \\\"total_sales\\\",\\n \\\"fieldText\\\": \\\"总销量\\\"\\n },\\n {\\n \\\"title\\\": \\\"total_returns\\\",\\n \\\"fieldText\\\": \\\"总退货数量\\\"\\n }\\n ]\\n}\\n```\\n* code:数据集变量名\\n* isList:为”1”表示集合,“0”表示对象\\n* children:为字段列表,包含title(字段名)和fieldText(展示名)\\n⸻\\n## 表格数据结构说明:\\n```\\n{\\n \\\"0\\\": { \\\"cells\\\": {} },\\n \\\"1\\\": { \\\"cells\\\": {\\n \\\"1\\\": { \\\"text\\\": \\\"#{a.total_sales}\\\" },\\n \\\"2\\\": { \\\"text\\\": \\\"#{a.name}\\\" }\\n }},\\n \\\"len\\\": 200\\n}\\n```\\n* 行号作为键\\n* 每行下有 cells 对象,key 是列号\\n* 每行以序号作为键\\n* 每列下包含 text 为占位符,${} 用于对象,#{} 用于集合\\n* 可包含 style 等附加样式信息\\n⸻\\n\\n\\n\\n\\n## 填充规则:\\n1. 若 isList = 1(集合):\\n * 第N行(如 \\\"0\\\")为字段标题:使用 children.fieldText 填充\\n * 第N+1行(如 \\\"1\\\")为字段占位符:使用 `#{code.title}` 填充\\n * 所有字段占位符占用一行,所有标题占用一行\\n2. 若 isList = 0(对象):\\n * 每字段占两列,低N列填字段标题,N+1列填占位符 `${code.title}`\\n * 共两组:第一组在第n列,第二组在第N+2列\\n\\n\\n\\n\\n⸻\\n\\n\\n\\n\\n## 输出格式\\n* 直接返回JSON数据,不要解释,不要md语法,不要换行符,不要有注释。\\n* 确保输出的JSON格式正确,数据中不能包含注释和省略。\\n\\n\\n\\n\\n\\n\\n\\n\\n## 特别注意\\n- 字段的占位必须是`#{}`或`${}`,不能缺失大括号。\\n- 用户描述的序号需要减一才是下标\\n- 确保输出的json格式正确。\\n- 只需要生成一套表格数据。\"},{\"role\":\"user\",\"content\":\"用户数据集:\\n{{ddl}}\\n用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"173366253646540800\",\"type\":\"end\",\"x\":1643,\"y\":336,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"rows\",\"nodeId\":\"173365800833675264\"}],\"height\":114,\"width\":332}},{\"id\":\"173366439085109248\",\"type\":\"end\",\"x\":1158,\"y\":1209,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"error:选择正确的业务类型\"},\"inputParams\":[],\"outputParams\":[],\"height\":114,\"width\":332}},{\"id\":\"175149164433014784\",\"type\":\"llm\",\"x\":1164,\"y\":598,\"properties\":{\"text\":\"选择表\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":2,\"messages\":[{\"role\":\"system\",\"content\":\"## 任务\\n根据用户需求,从下方数据库表列表中选择所有关联的表名称。\\n\\n\\n## 数据库表列表(格式:表名 | 注释)\\n{{ddl}}\\n\\n## 输出规则\\n1. 严格按JSON数组格式输出,例如:[\\\"order\\\"]。\\n2. 仅包含表名称,无需注释。\\n3. **禁止添加列表外的表**。\\n4. 表的选择范围可以适当大一些。\\n4. 无业务相关性时输出空数组:[]\\n\\n\\n请回复纯JSON,不要包含其他内容。\"},{\"role\":\"user\",\"content\":\"用户需求:{{question}}\"}]},\"inputParams\":[{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175153953988444160\",\"type\":\"end\",\"x\":1643,\"y\":564,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"tables\",\"nodeId\":\"175149164433014784\"}],\"height\":114,\"width\":332}},{\"id\":\"175505963485245440\",\"type\":\"llm\",\"x\":1166,\"y\":802,\"properties\":{\"text\":\"生成图表\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"根据以下数据以及用户需求生成符合格式要求的图表数据。\\n\\n\\n## 工作流程:\\n\\n\\n1. 根据用户需求选择一个合适的数据集\\n2. 根据数据集和需求,从图表列表中选择一个合适的图标类型。\\n3. 组装最终输出的json\\n\\n\\n⸻\\n## 可选的图表如下(标识|描述):\\n\\n\\n- 1维图表\\n - bar.simple|普通柱形图\\n - bar.background|带背景柱形图\\n - bar.horizontal|横向柱形图\\n - line.simple|普通折线图\\n - line.area|面积堆积折线图\\n - line.smooth|平滑曲线折线图\\n - line.step|阶梯折线图\\n - pie.simple|普通饼图\\n - pie.doughnut|环状饼图\\n - pie.rose|南丁格尔玫瑰饼图\\n - scatter.simple|普通散点图\\n - funnel.simple|普通漏斗图\\n - funnel.pyramid|金字塔漏斗图\\n - pictorial.spirits|普通象形图\\n - map.scatter|点地图\\n - gauge.simple|360°仪表盘\\n - gauge.simple180|180°仪表盘\\n- 2维\\n - bar.multi|多数据对比柱形图\\n - bar.negative|正负条形图\\n - bar.stack|堆叠柱形图\\n - bar.stack.horizontal|堆叠条形图\\n - bar.multi.horizontal|多数据条形柱状图\\n - line.multi|多数据对比折线图\\n - mixed.linebar|普通折柱图\\n - scatter.bubble|气泡散点图\\n - radar.basic|普通雷达图\\n - radar.custom|圆形雷达图\\n⸻\\n## 数据集格式说明:\\n```\\n{\\n \\\"dbId\\\": \\\"1069915169263800320\\\",\\n \\\"code\\\": \\\"a\\\",\\n \\\"title\\\": \\\"a\\\",\\n \\\"isList\\\": \\\"1\\\",\\n \\\"type\\\": \\\"0\\\",\\n \\\"children\\\": [\\n {\\n \\\"title\\\": \\\"total_sales\\\",\\n \\\"fieldText\\\": \\\"total_sales\\\"\\n },\\n {\\n \\\"title\\\": \\\"total_returns\\\",\\n \\\"fieldText\\\": \\\"total_returns\\\"\\n }\\n ]\\n}\\n```\\n* code:数据集变量名\\n* isList:为”1”表示集合,“0”表示对象\\n* children:为字段列表,包含title(字段名)和fieldText(展示名)\\n* type:0|sql,1|api,2|code,3|json\\n⸻\\n## 输出json格式\\n{\\n \\\"dataType\\\": \\\"sql\\\",\\n \\\"apiStatus\\\": \\\"0\\\",\\n \\\"apiUrl\\\": \\\"\\\",\\n \\\"dataId\\\": \\\"1069898455939633152\\\",\\n \\\"axisX\\\": \\\"supplier_name\\\",\\n \\\"axisY\\\": \\\"total_returns\\\",\\n \\\"series\\\": \\\"material_name\\\",\\n \\\"yText\\\": \\\"total_returns\\\",\\n \\\"xText\\\": \\\"supplier_name\\\",\\n \\\"dbCode\\\": \\\"a\\\",\\n \\\"isCustomPropName\\\": false,\\n \\\"chartType\\\": \\\"line.multi\\\",\\n \\\"id\\\": \\\"0aGl4PUfbIfy8BMF\\\",\\n \\\"run\\\": 1,\\n \\\"title\\\": \\\"\\\",\\n}\\n* dataType:与数据集type对应(0|sql,1|api,2|code,3|json)\\n* dataId:对应数据集dbId\\n* dbCode:对应数据集的code\\n* axisX:分类属性,从数据集字段中取值(fieldText)\\n* axisY:值属性,从数据集字段中取值(fieldText)\\n* series: 系列,从数据集字段中取值(fieldText)\\n* xText:分类属性显示,从数据集字段中取值(title)\\n* yText:值属性显示,从数据集字段中取值(title)\\n* chartType:图表的标识\\n* title:为这个图表起一个标题\\n* isCustomPropName: 如果是api数据集,该值为true\\n* apiStatus: 如果是api数据集则等于\\\"1\\\",否则\\\"0\\\"\\n\\n\\n## 输出格式\\n* 直接返回JSON数据,不要解释,不要md语法,不要换行符,不要有注释。\\n* 确保输出的json格式正确完整。\"},{\"role\":\"user\",\"content\":\"## 用户数据集:\\n{{ddl}}\\n## 用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175506006644633600\",\"type\":\"end\",\"x\":1643,\"y\":769,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"chart\",\"nodeId\":\"175505963485245440\"}],\"height\":114,\"width\":332}},{\"id\":\"175807569594040320\",\"type\":\"llm\",\"x\":1166,\"y\":1018,\"properties\":{\"text\":\"意图识别\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"请根据用户需求与数据集设计,综合判断应执行的工作流步骤,并为每个步骤生成简洁明确的需求描述,同时选择最合适的数据集。\\n---\\n## 可选步骤(格式:标识 | 功能说明)\\n- `genJsonRows` | 生成报表(可选)\\n- `genChart` | 生成图表(可选)\\n> **注意:** 至少选择一个步骤,亦可同时选择两者;图表的权重较低。\\n---\\n## 数据集格式\\n```json\\n{\\n \\\"dbId\\\": \\\"1069915169263800320\\\",\\n \\\"code\\\": \\\"a\\\",\\n \\\"title\\\": \\\"a\\\",\\n \\\"isList\\\": \\\"1\\\",\\n \\\"type\\\": \\\"0\\\",\\n \\\"children\\\": [\\n {\\n \\\"title\\\": \\\"total_sales\\\",\\n \\\"fieldText\\\": \\\"total_sales\\\"\\n },\\n {\\n \\\"title\\\": \\\"total_returns\\\",\\n \\\"fieldText\\\": \\\"total_returns\\\"\\n }\\n ]\\n}\\n* code:数据集变量名\\n* isList:为”1”表示集合,“0”表示对象\\n* children:为字段列表,包含title(展示名)和fieldText(字段名)\\n* type:0|sql,1|api,2|code,3|json\\n⸻\\n## 输出格式\\n```\\n步骤标识1|需求描述1|数据集code,步骤标识2|需求描述2|数据集code\\n```\\n* 各步骤之间用英文逗号,分隔\\n* 不得添加额外说明,不要md语法,不要换行符,不要有注释。\"},{\"role\":\"user\",\"content\":\"## 用户数据集:\\n{{ddl}}\\n## 用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175808663015538688\",\"type\":\"end\",\"x\":1643,\"y\":985,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"intent\",\"nodeId\":\"175807569594040320\"}],\"height\":114,\"width\":332}}],\"edges\":[{\"id\":\"172957153288454144\",\"type\":\"base-edge\",\"sourceNodeId\":\"172956395755208704\",\"targetNodeId\":\"172957153284259840\",\"sourceAnchorId\":\"172956395755208704_output\",\"targetAnchorId\":\"172957153284259840_input\",\"pointsList\":[{\"x\":1332,\"y\":101},{\"x\":1432,\"y\":101},{\"x\":1377,\"y\":103},{\"x\":1477,\"y\":103}]},{\"id\":\"173365501234540544\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"173365501230346240\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"173365501230346240_input\",\"pointsList\":[{\"x\":428,\"y\":443},{\"x\":528,\"y\":443},{\"x\":422,\"y\":456},{\"x\":522,\"y\":456}]},{\"id\":\"173366253650735104\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365800833675264\",\"targetNodeId\":\"173366253646540800\",\"sourceAnchorId\":\"173365800833675264_output\",\"targetAnchorId\":\"173366253646540800_input\",\"pointsList\":[{\"x\":1333,\"y\":309},{\"x\":1433,\"y\":309},{\"x\":1377,\"y\":310},{\"x\":1477,\"y\":310}]},{\"id\":\"173372961415852032\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"172956395755208704\",\"sourceAnchorId\":\"173365501230346240_source_if\",\"targetAnchorId\":\"172956395755208704_input\",\"pointsList\":[{\"x\":854,\"y\":490},{\"x\":954,\"y\":490},{\"x\":900,\"y\":101},{\"x\":1000,\"y\":101}]},{\"id\":\"173372967073968128\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"173365800833675264\",\"sourceAnchorId\":\"173365501230346240_case_2\",\"targetAnchorId\":\"173365800833675264_input\",\"pointsList\":[{\"x\":854,\"y\":516},{\"x\":954,\"y\":516},{\"x\":901,\"y\":309},{\"x\":1001,\"y\":309}]},{\"id\":\"173372974988619776\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"173366439085109248\",\"sourceAnchorId\":\"173365501230346240_source_else\",\"targetAnchorId\":\"173366439085109248_input\",\"pointsList\":[{\"x\":854,\"y\":620},{\"x\":954,\"y\":620},{\"x\":892,\"y\":1183},{\"x\":992,\"y\":1183}]},{\"id\":\"175149164437209088\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175149164433014784\",\"sourceAnchorId\":\"173365501230346240_case_3\",\"targetAnchorId\":\"175149164433014784_input\",\"pointsList\":[{\"x\":854,\"y\":542},{\"x\":954,\"y\":542},{\"x\":898,\"y\":539},{\"x\":998,\"y\":539}]},{\"id\":\"175153997969915904\",\"type\":\"base-edge\",\"sourceNodeId\":\"175149164433014784\",\"targetNodeId\":\"175153953988444160\",\"sourceAnchorId\":\"175149164433014784_output\",\"targetAnchorId\":\"175153953988444160_input\",\"pointsList\":[{\"x\":1330,\"y\":539},{\"x\":1430,\"y\":539},{\"x\":1377,\"y\":538},{\"x\":1477,\"y\":538}]},{\"id\":\"175505963489439744\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175505963485245440\",\"sourceAnchorId\":\"173365501230346240_case_4\",\"targetAnchorId\":\"175505963485245440_input\",\"pointsList\":[{\"x\":854,\"y\":568},{\"x\":954,\"y\":568},{\"x\":900,\"y\":743},{\"x\":1000,\"y\":743}]},{\"id\":\"175506006648827904\",\"type\":\"base-edge\",\"sourceNodeId\":\"175505963485245440\",\"targetNodeId\":\"175506006644633600\",\"sourceAnchorId\":\"175505963485245440_output\",\"targetAnchorId\":\"175506006644633600_input\",\"pointsList\":[{\"x\":1332,\"y\":743},{\"x\":1432,\"y\":743},{\"x\":1377,\"y\":743},{\"x\":1477,\"y\":743}]},{\"id\":\"175807569598234624\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175807569594040320\",\"sourceAnchorId\":\"173365501230346240_case_5\",\"targetAnchorId\":\"175807569594040320_input\",\"pointsList\":[{\"x\":854,\"y\":594},{\"x\":954,\"y\":594},{\"x\":900,\"y\":959},{\"x\":1000,\"y\":959}]},{\"id\":\"175808663019732992\",\"type\":\"base-edge\",\"sourceNodeId\":\"175807569594040320\",\"targetNodeId\":\"175808663015538688\",\"sourceAnchorId\":\"175807569594040320_output\",\"targetAnchorId\":\"175808663015538688_input\",\"pointsList\":[{\"x\":1332,\"y\":959},{\"x\":1432,\"y\":959},{\"x\":1377,\"y\":959},{\"x\":1477,\"y\":959}]}]}', `status` = 'release', `metadata` = '{\"outputs\":[{\"field\":\"text\",\"name\":\"intent\",\"nodeId\":\"175807569594040320\"},{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"ddl\",\"name\":\"表结构\",\"required\":true,\"type\":\"string\"},{\"field\":\"dbtype\",\"name\":\"数据库类型\",\"required\":true,\"type\":\"string\"},{\"field\":\"bizType\",\"name\":\"业务类型\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1909856345692065793'; + +-- ---author:liusq-date:20250606-----for: [issues/8337]关于ai工作列表的数据权限问题 #8337 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930223132619112449', '1890213291321749505', '删除AI流程', NULL, NULL, 0, NULL, NULL, 2, 'airag:flow:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:20:31', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930223034757611522', '1890213291321749505', '保存AI流程设计', NULL, NULL, 0, NULL, NULL, 2, 'airag:flow:designSave', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:20:08', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222953853681666', '1890213291321749505', '编辑AI流程', NULL, NULL, 0, NULL, NULL, 2, 'airag:flow:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:19:49', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222862556266498', '1890213291321749505', '新增AI流程', NULL, NULL, 0, NULL, NULL, 2, 'airag:flow:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:19:27', 'admin', '2025-06-04 19:21:08', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222679269376001', '1892553778493022209', '删除AI模型', NULL, NULL, 0, NULL, NULL, 2, 'airag:model:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:18:43', 'admin', '2025-06-04 19:21:24', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222617197871105', '1892553778493022209', '编辑AI模型', NULL, NULL, 0, NULL, NULL, 2, 'airag:model:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:18:28', 'admin', '2025-06-04 19:21:20', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222558582472705', '1892553778493022209', '新增AI模型', NULL, NULL, 0, NULL, NULL, 2, 'airag:model:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:18:14', 'admin', '2025-06-04 19:21:16', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222395180777474', '1892557342028226561', '清空AI知识库文档', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:doc:deleteAll', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:17:35', 'admin', '2025-06-04 19:22:25', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222295012409345', '1892557342028226561', '批量删除AI知识库文档', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:doc:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:17:12', 'admin', '2025-06-04 19:22:21', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222218734796802', '1892557342028226561', '向量化AI知识库文档', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:doc:rebuild', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:16:53', 'admin', '2025-06-04 19:22:16', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930222066120851457', '1892557342028226561', '导入AI知识库文档', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:doc:zip', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:16:17', 'admin', '2025-06-04 19:22:09', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930221983555977217', '1892557342028226561', '新增编辑AI知识库文档', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:doc:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:15:57', 'admin', '2025-06-04 19:22:03', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930221774230847490', '1892557342028226561', '删除AI知识库', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:15:07', 'admin', '2025-06-04 19:21:52', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930221702164316161', '1892557342028226561', '重建AI知识库', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:rebuild', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:14:50', 'admin', '2025-06-04 19:21:46', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930221637551063042', '1892557342028226561', '编辑AI知识库', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:14:35', 'admin', '2025-06-04 19:21:42', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930221570324758530', '1892557342028226561', '添加AI知识库', NULL, NULL, 0, NULL, NULL, 2, 'airag:knowledge:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:14:19', 'admin', '2025-06-04 19:21:38', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930221335938662401', '1893865471550578689', '删除AI应用', NULL, NULL, 0, NULL, NULL, 2, 'airag:app:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:13:23', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930221213607591937', '1893865471550578689', '新增或编辑AI应用', NULL, NULL, 0, NULL, NULL, 2, 'airag:app:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 19:12:54', NULL, NULL, 0, 0, '1', 0); + +UPDATE sys_permission SET is_leaf = 0 WHERE id IN ( '1890213291321749505','1892553778493022209', '1892557342028226561', '1893865471550578689' ); + +-- ---author:lvdandan-date:20250612-----for: 门户示例api域名修改 +UPDATE onl_drag_dataset_head +SET query_sql = REPLACE(query_sql, 'apighbcom', 'api.ghb.com') +WHERE query_sql LIKE '%https://apighbcom%'; \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.1_2__openapi.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.1_2__openapi.sql new file mode 100644 index 0000000..4558476 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.1_2__openapi.sql @@ -0,0 +1,168 @@ +/* + Navicat Premium Data Transfer + + Source Server : mysql5.7 + Source Server Type : MySQL + Source Server Version : 50738 (5.7.38) + Source Host : 127.0.0.1:3306 + Source Schema : test + + Target Server Type : MySQL + Target Server Version : 50738 (5.7.38) + File Encoding : 65001 + + Date: 15/05/2025 10:18:36 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for open_api +-- ---------------------------- +DROP TABLE IF EXISTS `open_api`; +CREATE TABLE `open_api` ( + `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称', + `request_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法', + `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口地址', + `black_list` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP 黑名单', + `body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求体内容', + `origin_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原始地址', + `status` int(10) NULL DEFAULT NULL COMMENT '状态', + `del_flag` int(10) NULL DEFAULT NULL COMMENT '删除标识', + `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', + `headers_json` json NULL COMMENT '请求头json', + `params_json` json NULL COMMENT '请求参数json', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '接口表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of open_api +-- ---------------------------- +INSERT INTO `open_api` VALUES ('1922132683346649090', '根据部门查询用户', 'GET', 'TEwcXBlr', NULL, NULL, '/sys/user/queryUserByDepId', 1, 0, 'admin', '2025-05-13 11:31:58', 'admin', '2025-05-15 10:10:01', '[]', '[{\"id\": \"row_24\", \"note\": \"\", \"paramKey\": \"id\", \"required\": \"1\", \"defaultValue\": \"\"}]'); + +-- ---------------------------- +-- Table structure for open_api_auth +-- ---------------------------- +DROP TABLE IF EXISTS `open_api_auth`; +CREATE TABLE `open_api_auth` ( + `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权名称', + `ak` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'AK', + `sk` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'SK', + `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', + `system_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联系统用户名', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of open_api_auth +-- ---------------------------- +INSERT INTO `open_api_auth` VALUES ('1922164194775056386', 'scott', 'ak-pFjyNHWRsJEFWlu6', '4hV5dBrZtmGAtPdbA5yseaeKRYNpzGsS', 'admin', '2025-05-13 13:37:11', NULL, NULL, 'e9ca23d68d884d4ebb19d07889727dae'); + +-- ---------------------------- +-- Table structure for open_api_log +-- ---------------------------- +DROP TABLE IF EXISTS `open_api_log`; +CREATE TABLE `open_api_log` ( + `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID', + `call_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '调用ID', + `call_time` datetime NULL DEFAULT NULL COMMENT '调用时间', + `used_time` bigint(20) NULL DEFAULT NULL COMMENT '耗时', + `response_time` datetime NULL DEFAULT NULL COMMENT '响应时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '调用记录表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of open_api_log +-- ---------------------------- +INSERT INTO `open_api_log` VALUES ('1922175238557913090', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:04', 94, '2025-05-13 14:21:04'); +INSERT INTO `open_api_log` VALUES ('1922175436256432130', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:51', 38, '2025-05-13 14:21:51'); +INSERT INTO `open_api_log` VALUES ('1922175487921868802', '1922132683346649090', '1922164194775056386', '2025-05-13 14:22:03', 31, '2025-05-13 14:22:03'); +INSERT INTO `open_api_log` VALUES ('1922176033789562883', '1922132683346649090', '1922164194775056386', '2025-05-13 14:24:13', 27, '2025-05-13 14:24:13'); +INSERT INTO `open_api_log` VALUES ('1922176583943835650', '1922132683346649090', '1922164194775056386', '2025-05-13 14:26:25', 39, '2025-05-13 14:26:25'); +INSERT INTO `open_api_log` VALUES ('1922177249969934337', '1922132683346649090', '1922164194775056386', '2025-05-13 14:28:08', 55250, '2025-05-13 14:29:03'); +INSERT INTO `open_api_log` VALUES ('1922180212645941249', '1922132683346649090', '1922164194775056386', '2025-05-13 14:40:46', 4162, '2025-05-13 14:40:50'); +INSERT INTO `open_api_log` VALUES ('1922180441692688385', '1922132683346649090', '1922164194775056386', '2025-05-13 14:41:11', 33346, '2025-05-13 14:41:44'); +INSERT INTO `open_api_log` VALUES ('1922180521686454273', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:00', 3570, '2025-05-13 14:42:03'); +INSERT INTO `open_api_log` VALUES ('1922180965825499138', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:10', 99211, '2025-05-13 14:43:49'); +INSERT INTO `open_api_log` VALUES ('1922181034515615746', '1922132683346649090', '1922164194775056386', '2025-05-13 14:43:52', 14005, '2025-05-13 14:44:06'); +INSERT INTO `open_api_log` VALUES ('1922183171307982850', '1922132683346649090', '1922164194775056386', '2025-05-13 14:52:15', 19834, '2025-05-13 14:52:35'); +INSERT INTO `open_api_log` VALUES ('1922184177068523521', '1922132683346649090', '1922164194775056386', '2025-05-13 14:56:34', 748, '2025-05-13 14:56:35'); +INSERT INTO `open_api_log` VALUES ('1922184729043107841', '1922132683346649090', '1922164194775056386', '2025-05-13 14:58:46', 1031, '2025-05-13 14:58:47'); +INSERT INTO `open_api_log` VALUES ('1922184806453182465', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:05', 68, '2025-05-13 14:59:05'); +INSERT INTO `open_api_log` VALUES ('1922184918382379009', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:10', 22155, '2025-05-13 14:59:32'); +INSERT INTO `open_api_log` VALUES ('1922185292635844610', '1922132683346649090', '1922164194775056386', '2025-05-13 15:00:55', 6267, '2025-05-13 15:01:01'); +INSERT INTO `open_api_log` VALUES ('1922186002672791554', '1922132683346649090', '1922164194775056386', '2025-05-13 15:03:23', 27554, '2025-05-13 15:03:50'); +INSERT INTO `open_api_log` VALUES ('1922187506582425601', '1922132683346649090', '1922164194775056386', '2025-05-13 15:09:45', 3464, '2025-05-13 15:09:49'); +INSERT INTO `open_api_log` VALUES ('1922187586597163011', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:08', 82, '2025-05-13 15:10:08'); +INSERT INTO `open_api_log` VALUES ('1922187924741951490', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:49', 39590, '2025-05-13 15:11:28'); +INSERT INTO `open_api_log` VALUES ('1922188138710261761', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:19', 758, '2025-05-13 15:12:19'); +INSERT INTO `open_api_log` VALUES ('1922188290661507073', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:29', 26527, '2025-05-13 15:12:56'); +INSERT INTO `open_api_log` VALUES ('1922189701755424769', '1922132683346649090', '1922164194775056386', '2025-05-13 15:18:28', 3619, '2025-05-13 15:18:32'); +INSERT INTO `open_api_log` VALUES ('1922190076784803841', '1922132683346649090', '1922164194775056386', '2025-05-13 15:20:01', 741, '2025-05-13 15:20:02'); +INSERT INTO `open_api_log` VALUES ('1922836671113101313', '1922132683346649090', '1922164194775056386', '2025-05-15 10:09:21', 186, '2025-05-15 10:09:22'); +INSERT INTO `open_api_log` VALUES ('1922836856287428610', '1922132683346649090', '1922164194775056386', '2025-05-15 10:10:06', 145, '2025-05-15 10:10:06'); + +-- ---------------------------- +-- Table structure for open_api_permission +-- ---------------------------- +DROP TABLE IF EXISTS `open_api_permission`; +CREATE TABLE `open_api_permission` ( + `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID', + `api_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '认证ID', + `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'openapi授权' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of open_api_permission +-- ---------------------------- +INSERT INTO `open_api_permission` VALUES ('1922164225875820545', '1922132683346649090', '1922164194775056386', 'admin', '2025-05-13 13:37:18', NULL, NULL); + +SET FOREIGN_KEY_CHECKS = 1; + + +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1917957565728198657', '1922109301837606914', '接口文档', '/openapi/SwaggerUI', 'openapi/SwaggerUI', 1, '', null, 1, null, '0', 1, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 23:01:32', 'admin', '2025-05-13 09:59:46', 0, 0, null, 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1922109301837606914', '', 'OpenApi管理', '/openapi', 'layouts/RouteView', 1, '', null, 0, null, '0', 12.1, 0, 'ant-design:swap-outlined', 0, 0, 0, 0, null, 'admin', '2025-05-13 09:59:03', 'admin', '2025-05-13 10:02:43', 0, 0, null, 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193340030', '1922109301837606914', '接口管理', '/openapi/openApiList', 'openapi/OpenApiList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', 'admin', '2025-05-13 09:59:24', 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350031', '2025050104193340030', '添加接口管理', null, null, 0, null, null, 2, 'openapi:open_api:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350032', '2025050104193340030', '编辑接口管理', null, null, 0, null, null, 2, 'openapi:open_api:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350033', '2025050104193340030', '删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350034', '2025050104193340030', '批量删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350035', '2025050104193340030', '导出excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350036', '2025050104193340030', '导入excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940200', '1922109301837606914', '授权管理', '/openapi/openApiAuthList', 'openapi/OpenApiAuthList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', 'admin', '2025-05-13 09:59:35', 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940201', '2025050105554940200', '添加授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940202', '2025050105554940200', '编辑授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940203', '2025050105554940200', '删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940204', '2025050105554940200', '批量删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940205', '2025050105554940200', '导出excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0); +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940206', '2025050105554940200', '导入excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0); + +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071739539457', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193340030', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648321', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350031', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648322', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350032', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648323', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350033', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648324', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350034', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648325', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350035', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648326', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350036', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149426864129', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940200', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058436', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940203', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058437', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940204', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058438', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940205', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058439', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940206', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1'); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.2_1__all_upgrade.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.2_1__all_upgrade.sql new file mode 100644 index 0000000..ce7cd57 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.2_1__all_upgrade.sql @@ -0,0 +1,87 @@ +-- 租户初始套餐添加时 提示违反唯一约束 +ALTER TABLE sys_tenant_pack +ADD INDEX idx__stp_tenant_id_pack_code(tenant_id, pack_code) USING BTREE; + +-- 添加通知消息大分类 +ALTER TABLE sys_announcement + ADD COLUMN notice_type varchar(10) NULL COMMENT '通知类型(system:系统消息、file:知识库、flow:流程、plan:日程计划、meeting:会议)' AFTER tenant_id; + +-- 更新通知消息字段旧数据默认为系统消息 +update sys_announcement set notice_type = 'flow' where bus_type in('bpm','bpm_cc','bpm_task'); +update sys_announcement set notice_type = 'system' where bus_type ='email'; +update sys_announcement set notice_type = 'system' where notice_type is null; + +-- 系统公告新增字段修改 +ALTER TABLE `sys_announcement` + ADD COLUMN `files` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '附件' AFTER `tenant_id`, +ADD COLUMN `visits_num` int(11) NULL DEFAULT NULL COMMENT '访问次数' AFTER `files`, +ADD COLUMN `iz_top` int(10) NULL DEFAULT NULL COMMENT '是否置顶(0:否; 1:是)' AFTER `visits_num`, +ADD COLUMN `iz_approval` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否审批(0否 1是)' AFTER `iz_top`, +ADD COLUMN `bpm_status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流程状态' AFTER `iz_approval`, +ADD COLUMN `msg_classify` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '消息归类' AFTER `bpm_status`; + +-- 系统公告--新增字典 +INSERT INTO `sys_dict`(`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1934846825077878786', '公告分类', 'notice_type', NULL, 0, 'admin', '2025-06-17 13:33:25', NULL, NULL, 0, 0, NULL); +INSERT INTO `sys_dict`(`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1937393911539384322', '模版分类', 'msgCategory', NULL, 0, 'admin', '2025-06-24 14:14:38', NULL, NULL, 0, 0, NULL); + +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1934846897383485441', '1934846825077878786', '发布性通知', '1', NULL, NULL, 1, 1, 'admin', '2025-06-17 13:33:43', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1934846933030875138', '1934846825077878786', '转发性通知', '2', NULL, NULL, 1, 1, 'admin', '2025-06-17 13:33:51', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1934846963749957633', '1934846825077878786', '指示性通知', '3', NULL, NULL, 1, 1, 'admin', '2025-06-17 13:33:59', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1934846993449824257', '1934846825077878786', '任免性通知', '4', NULL, NULL, 1, 1, 'admin', '2025-06-17 13:34:06', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1934847047262744577', '1934846825077878786', '事务性(周知)通知', '5', NULL, NULL, 1, 1, 'admin', '2025-06-17 13:34:18', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1934847082905939969', '1934846825077878786', '会议通知', '6', NULL, NULL, 1, 1, 'admin', '2025-06-17 13:34:27', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1934847117039185921', '1934846825077878786', '其他通知', '7', NULL, NULL, 1, 1, 'admin', '2025-06-17 13:34:35', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1937394006326460418', '1937393911539384322', '通知公告', 'notice', NULL, NULL, 1, 1, 'admin', '2025-06-24 14:15:01', NULL, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1937394038412886018', '1937393911539384322', '其他', 'other', NULL, NULL, 1, 1, 'admin', '2025-06-24 14:15:08', NULL, NULL); + + +-- 消息模版增加 模版分类 字段 +ALTER TABLE `sys_sms_template` + ADD COLUMN `template_category` varchar(10) NULL COMMENT '模版分类:notice通知公告 other其他' AFTER `template_type`; + + +-- 修改表iz_top的默认值 +ALTER TABLE `sys_announcement` + MODIFY COLUMN `iz_top` int(10) NULL DEFAULT 0 COMMENT '是否置顶(0:否; 1:是)' AFTER `visits_num`; + +-- 补充旧数据iz_top的默认值 +UPDATE sys_announcement SET iz_top = 0 WHERE iz_top IS NULL OR iz_top = ''; + +-- 新增首页配置菜单 +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1939572818833301506', 'd7d6e2e4e2934f2c9385a623fd98c6f3', '首页配置', '/system/homeConfig', 'system/homeConfig/index', 1, '', NULL, 1, NULL, '0', 1.00, 0, 'ant-design:appstore-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-06-30 14:32:50', 'admin', '2025-07-01 20:13:22', 0, 0, NULL, 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1941349550087168001', '1939572818833301506', '首页配置-批量删除', NULL, NULL, 0, NULL, NULL, 2, 'system:roleindex:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-05 12:12:56', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1941349462887587842', '1939572818833301506', '首页配置-删除', NULL, NULL, 0, NULL, NULL, 2, 'system:roleindex:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-05 12:12:35', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1941349335431077889', '1939572818833301506', '首页配置-编辑', NULL, NULL, 0, NULL, NULL, 2, 'system:roleindex:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-05 12:12:05', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1941349246536998913', '1939572818833301506', '首页配置-添加', NULL, NULL, 0, NULL, NULL, 2, 'system:roleindex:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-05 12:11:44', NULL, NULL, 0, 0, '1', 0); + +-- 首页字典 +INSERT INTO `sys_dict`(`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1939572486447292418', '首页关联', 'relation_type', NULL, 0, 'admin', '2025-06-30 14:31:31', NULL, NULL, 0, 0, NULL); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1939572554533429250', '1939572486447292418', '角色', 'ROLE', NULL, NULL, 1, 1, 'admin', '2025-06-30 14:31:47', 'admin', '2025-06-30 15:04:18'); +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1939572602289774594', '1939572486447292418', '用户', 'USER', NULL, NULL, 2, 1, 'admin', '2025-06-30 14:31:59', 'admin', '2025-06-30 15:04:21'); + + +-- 角色首页表新增 relation_type 字段 +ALTER TABLE `sys_role_index` + ADD COLUMN `relation_type` varchar(20) NULL COMMENT '关联关系(ROLE:角色 USER:用户)' AFTER `sys_org_code`; + +-- 首页角色补充默认值 +UPDATE sys_role_index SET relation_type = 'ROLE' WHERE relation_type IS NULL OR relation_type = ''; + +-- app3支持版本管理 +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930152938891608066', '1455100420297859074', 'APP版本管理', '/app/version', 'system/appVersion/SysAppVersion', 1, '', NULL, 1, NULL, '0', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-06-04 14:41:36', 'admin', '2025-07-03 10:09:46', 0, 0, NULL, 0); + + +-- 首页配置菜单 +UPDATE `sys_permission` SET `is_leaf` = 0 WHERE `id` = '1939572818833301506'; + + +-- APP版本管理配置菜单 +UPDATE `sys_permission` SET `is_leaf` = 0 WHERE `id` = '1930152938891608066'; +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1942160438629109761', '1930152938891608066', 'APP版本编辑', NULL, NULL, 0, NULL, NULL, 2, 'app:edit:version', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-07 17:55:07', NULL, NULL, 0, 0, '1', 0); + +-- 删除第三方配置添加权限 +INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1947833384695164929', '1629109281748291586', '第三方配置删除', NULL, NULL, 0, NULL, NULL, 2, 'system:third:config:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-23 09:37:23', NULL, NULL, 0, 0, '1', 0); + +-- 人员代理表添加process_ids字段 +ALTER TABLE `sys_user_agent` + ADD COLUMN `process_ids` varchar(255) NULL COMMENT '代理流程ID' AFTER `end_time`; \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.3_0__all_upgrade.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.3_0__all_upgrade.sql new file mode 100644 index 0000000..4e5f287 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.3_0__all_upgrade.sql @@ -0,0 +1,309 @@ +-- ---author:wangshuai---date:20250806-----for: 【QQYUN-12164】用户表添加个人签名和是否启用个人签名字段 +ALTER TABLE sys_user +ADD COLUMN sign_enable tinyint(1) NULL DEFAULT NULL COMMENT '是否启用个性签名(0 否 1是)' AFTER bpm_status, +ADD COLUMN sign varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '个性签名' AFTER sign_enable; + +-- ---author:chenrui---date:20250806-----for: 【QQYUN-12244】AI调用模板生成word简历 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`) VALUES ('1952634605517447170', 'admin', '2025-08-05 15:35:43', 'admin', '2025-08-06 17:37:27', 'A04', NULL, 'ghb', '示例_AI生成在线简历', '', '', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'215734195065536512\'),\n enhanceJava.tag(\'215740280715427840\'),\n end.tag(\'215735188368998400\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":404,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"个人简介\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"profile\",\"name\":\"基础信息\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"215734195065536512\",\"type\":\"llm\",\"x\":739,\"y\":406,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"这是生成在线 Word 文档的 JSON 结构说明,每个对象表示一个内容块。\\n字段说明:\\n    • type:内容类型。可选:title(标题)、list(列表)、separator(分隔线)、hyperlink(超链接)、pageBreak(分页符)、tab(制表符)、\\\"\\\"(普通文本)。\\n    • level:标题层级,仅 type 为 title 时使用,取值:first ~ sixth。\\n    • value:文本、图片地址、超链接等。\\n    • valueList:用于标题、列表、超链接等,数组元素支持 value 及样式字段。\\n    • listType:列表类型,ul(无序)、ol(有序)。\\n    • listStyle:列表样式,如 disc、decimal、circle、square、checkbox。\\n    • trList、colgroup:表格行列定义,仅用于 table。\\n    • 样式字段:font、size、bold、color、italic、highlight、underline、strikeout。\\n    • 分隔线:dashArray。\\n    • 其他:rowFlex(left、center、right、alignment)、backgroundColor、verticalAlign、textDecoration 等用于特殊样式。\\n示例:\\n[\\n  {\\n    \\\"type\\\": \\\"title\\\",\\n    \\\"level\\\": \\\"first\\\",\\n    \\\"valueList\\\": [{ \\\"value\\\": \\\"主标题示例\\\", \\\"font\\\": \\\"微软雅黑\\\", \\\"size\\\": 26, \\\"bold\\\": true, \\\"rowFlex\\\": \\\"center\\\" }]\\n  },\\n  { \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"普通文本内容示例\\\" },\\n  {\\n    \\\"type\\\": \\\"list\\\",\\n    \\\"listType\\\": \\\"ul\\\",\\n    \\\"listStyle\\\": \\\"disc\\\",\\n    \\\"valueList\\\": [\\n      { \\\"value\\\": \\\"列表项1\\\" },\\n      { \\\"value\\\": \\\"列表项2\\\" }\\n    ]\\n  },\\n  { \\\"type\\\": \\\"separator\\\", \\\"dashArray\\\": [1] },\\n  {\\n    \\\"type\\\": \\\"hyperlink\\\",\\n    \\\"url\\\": \\\"https://www.example.com\\\",\\n    \\\"valueList\\\": [{ \\\"value\\\": \\\"点击访问官网\\\", \\\"color\\\": \\\"#0000FF\\\", \\\"underline\\\": true }]\\n  },\\n  { \\\"type\\\": \\\"pageBreak\\\" },\\n  { \\\"type\\\": \\\"tab\\\" },\\n  { \\\"type\\\": \\\"superscript\\\", \\\"value\\\": \\\"上标内容\\\" },\\n  { \\\"type\\\": \\\"subscript\\\", \\\"value\\\": \\\"下标内容\\\" }\\n]\\n⸻\\n注意:\\n- 只输出`json`格式。\\n- 内容结构、样式组合不限,但字段与取值必须符合说明。\\n- title类型的内容块,中value必须以`\\\\n`结尾\\n- \\\"\\\"\\\"使用`{ \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"\\\\n\\\" }`进行主动换行,对象之间不会自动换行\\\"\\\"\\\"。\\n\\n\"},{\"role\":\"user\",\"content\":\"请根据以上字段和示例,生成一个完整的个人简历文档 JSON。\\n- 至少包含基础信息、个人优势、工作经历、项目经理、教育经历等模块。\\n- 若基础数据不足,可以适当生成参考数据。\\n- 用户信息如下:\\n基础资料:{{base}}\\n简介:{{profile}}\"}]},\"inputParams\":[{\"field\":\"profile\",\"name\":\"base\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"profile\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"215735188368998400\",\"type\":\"end\",\"x\":1577,\"y\":354,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"height\":114,\"width\":332}},{\"id\":\"215740280715427840\",\"type\":\"enhanceJava\",\"x\":1156,\"y\":352,\"properties\":{\"text\":\"Java 增强\",\"options\":{\"enhance\":{\"type\":\"spring\",\"path\":\"ghbDemoAiWordGen\"}},\"inputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"215734195065536512\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"215734195073925120\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"215734195065536512\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"215734195065536512_input\",\"pointsList\":[{\"x\":466,\"y\":389},{\"x\":566,\"y\":389},{\"x\":473,\"y\":347},{\"x\":573,\"y\":347}]},{\"id\":\"215740280719622144\",\"type\":\"base-edge\",\"sourceNodeId\":\"215734195065536512\",\"targetNodeId\":\"215740280715427840\",\"sourceAnchorId\":\"215734195065536512_output\",\"targetAnchorId\":\"215740280715427840_input\",\"pointsList\":[{\"x\":905,\"y\":347},{\"x\":1005,\"y\":347},{\"x\":890,\"y\":293},{\"x\":990,\"y\":293}]},{\"id\":\"215740398487289856\",\"type\":\"base-edge\",\"sourceNodeId\":\"215740280715427840\",\"targetNodeId\":\"215735188368998400\",\"sourceAnchorId\":\"215740280715427840_output\",\"targetAnchorId\":\"215735188368998400_input\",\"pointsList\":[{\"x\":1322,\"y\":293},{\"x\":1422,\"y\":293},{\"x\":1311,\"y\":328},{\"x\":1411,\"y\":328}]}]}', 'enable', '{\"outputs\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"个人简介\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"profile\",\"name\":\"基础信息\",\"required\":true,\"type\":\"string\"}]}'); + +-- -author:chenrui---date:2025/8/11-----for:[QQYUN-13400]删除废弃菜单--- +delete from sys_permission where id = '1948206070361595906'; +delete from sys_permission where id = '1948205626927194114'; + +-- -author:chenrui---date:2025/8/13-----for:[QQYUN-13394]优化,开源版本的账号都是错误的,用户不知道需要配置自己的账号--- +-- 添加激活字段 +ALTER TABLE `airag_model` +ADD COLUMN `activate_flag` int NULL COMMENT '是否激活(1=是,0=否)' AFTER `model_params`; +-- 更新历史数据值 +update airag_model set activate_flag = 0 ; + + + +-- ---author:chenrui---date:20250818-----for:更新生成简历提示词,确保生成的json可以被解析 +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'215734195065536512\'),\n enhanceJava.tag(\'215740280715427840\'),\n end.tag(\'215735188368998400\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":404,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"个人简介\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"profile\",\"name\":\"基础信息\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"215734195065536512\",\"type\":\"llm\",\"x\":739,\"y\":406,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你必须只输出合法且可被 JSON.parse() 正确解析的 JSON。\\n不要输出任何解释、注释或 JSON 以外的文字。\\n\\n\\nJSON 结构规则:\\n- 每个对象表示一个内容块。\\n- 字段说明:\\n • \\\"type\\\":内容类型,可选:\\\"title\\\"(标题)、\\\"list\\\"(列表)、\\\"separator\\\"(分隔线)、\\\"hyperlink\\\"(超链接)、\\\"pageBreak\\\"(分页符)、\\\"tab\\\"(制表符)、\\\"\\\"(普通文本)、\\\"superscript\\\"(上标)、\\\"subscript\\\"(下标)、\\\"table\\\"(表格)。\\n • \\\"level\\\":标题层级,仅当 type 为 \\\"title\\\" 时使用,取值:\\\"first\\\" ~ \\\"sixth\\\"。\\n • \\\"value\\\":文本、图片地址、超链接等。\\n • \\\"valueList\\\":数组,用于标题、列表、超链接等,数组元素支持 \\\"value\\\" 及样式字段。\\n • \\\"listType\\\":列表类型,取值:\\\"ul\\\"(无序)、\\\"ol\\\"(有序)。\\n • \\\"listStyle\\\":列表样式,如 \\\"disc\\\"、\\\"decimal\\\"、\\\"circle\\\"、\\\"square\\\"、\\\"checkbox\\\"。\\n • \\\"trList\\\"、\\\"colgroup\\\":表格行列定义,仅用于 \\\"table\\\"。\\n • 样式字段:\\\"font\\\"、\\\"size\\\"、\\\"bold\\\"、\\\"color\\\"、\\\"italic\\\"、\\\"highlight\\\"、\\\"underline\\\"、\\\"strikeout\\\"。\\n • \\\"dashArray\\\":用于 \\\"separator\\\"。\\n • 其他样式字段:\\\"rowFlex\\\"(\\\"left\\\"、\\\"center\\\"、\\\"right\\\"、\\\"alignment\\\")、\\\"backgroundColor\\\"、\\\"verticalAlign\\\"、\\\"textDecoration\\\"。\\n- 当 type = \\\"title\\\" 时,\\\"value\\\" 必须以 \\\"\\\\n\\\" 结尾。\\n- 主动换行请使用 `{ \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"\\\\n\\\" }`,不同对象之间不会自动换行。\\n- 所有键名和字符串必须使用英文双引号 `\\\"`。\\n\\n\\n输出必须严格是 JSON 数组,例如:\\n[\\n {\\n \\\"type\\\": \\\"title\\\",\\n \\\"level\\\": \\\"first\\\",\\n \\\"valueList\\\": [{ \\\"value\\\": \\\"主标题示例\\\\n\\\", \\\"font\\\": \\\"微软雅黑\\\", \\\"size\\\": 26, \\\"bold\\\": true, \\\"rowFlex\\\": \\\"center\\\" }]\\n },\\n { \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"普通文本内容示例\\\" },\\n {\\n \\\"type\\\": \\\"list\\\",\\n \\\"listType\\\": \\\"ul\\\",\\n \\\"listStyle\\\": \\\"disc\\\",\\n \\\"valueList\\\": [\\n { \\\"value\\\": \\\"列表项1\\\" },\\n { \\\"value\\\": \\\"列表项2\\\" }\\n ]\\n }\\n]\"},{\"role\":\"user\",\"content\":\"请根据以上字段和示例,生成一个完整的个人简历文档 JSON。\\n- 至少包含基础信息、个人优势、工作经历、项目经理、教育经历等模块。\\n- 若基础数据不足,可以适当生成参考数据。\\n- 用户信息如下:\\n基础资料:{{base}}\\n简介:{{profile}}\"}]},\"inputParams\":[{\"field\":\"profile\",\"name\":\"base\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"profile\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"215735188368998400\",\"type\":\"end\",\"x\":1577,\"y\":354,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"height\":114,\"width\":332}},{\"id\":\"215740280715427840\",\"type\":\"enhanceJava\",\"x\":1156,\"y\":352,\"properties\":{\"text\":\"Java 增强\",\"options\":{\"enhance\":{\"type\":\"spring\",\"path\":\"ghbDemoAiWordGen\"}},\"inputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"215734195065536512\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"215734195073925120\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"215734195065536512\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"215734195065536512_input\",\"pointsList\":[{\"x\":466,\"y\":389},{\"x\":566,\"y\":389},{\"x\":473,\"y\":347},{\"x\":573,\"y\":347}]},{\"id\":\"215740280719622144\",\"type\":\"base-edge\",\"sourceNodeId\":\"215734195065536512\",\"targetNodeId\":\"215740280715427840\",\"sourceAnchorId\":\"215734195065536512_output\",\"targetAnchorId\":\"215740280715427840_input\",\"pointsList\":[{\"x\":905,\"y\":347},{\"x\":1005,\"y\":347},{\"x\":890,\"y\":293},{\"x\":990,\"y\":293}]},{\"id\":\"215740398487289856\",\"type\":\"base-edge\",\"sourceNodeId\":\"215740280715427840\",\"targetNodeId\":\"215735188368998400\",\"sourceAnchorId\":\"215740280715427840_output\",\"targetAnchorId\":\"215735188368998400_input\",\"pointsList\":[{\"x\":1322,\"y\":293},{\"x\":1422,\"y\":293},{\"x\":1311,\"y\":328},{\"x\":1411,\"y\":328}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"个人简介\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"profile\",\"name\":\"基础信息\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1952634605517447170'; + +-- ---author:wangshuai---date:20250820-----for:【QQYUN-13421】扩展职务加职务等级:修改原来职务字段 +ALTER TABLE `sys_position` +CHANGE COLUMN `post_rank` `post_level` int(2) NULL DEFAULT NULL COMMENT '职务等级' AFTER `name`; + +-- ---author:wangshuai---date:20250820-----for:【QQYUN-13426】部门表新增职级和上级岗位字段 +-- 修改字典部门类型 +UPDATE `sys_dict` SET `description` = '机构类型 1公司,2部门,3岗位,4子公司' WHERE `id` = '1174511106530525185'; +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1955230463631126529', '1174511106530525185', '子公司', '4', NULL, 1, 1, 'admin', '2025-08-12 19:30:44', NULL, NULL, NULL); + +-- 部门表新增职级和上级岗位字段 +ALTER TABLE `sys_depart` +MODIFY COLUMN `org_category` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '1' COMMENT '机构类别 1公司,2部门,3岗位,4子公司' AFTER `description`, +ADD COLUMN `position_id` varchar(32) NULL COMMENT '职级id' AFTER `iz_leaf`, +ADD COLUMN `dep_post_parent_id` varchar(32) NULL COMMENT '上级岗位id' AFTER `position_id`, +ADD INDEX `idx_sd_position_id`(`position_id`) USING BTREE, +ADD INDEX `idx_sd_dep_post_parent_id`(`dep_post_parent_id`) USING BTREE; + +-- author:wangshuai---date:20250820--for: 【QQYUN-13422】【用户管理】添加字段 主岗位 兼职岗位 取消职务 --- + +-- 用户表新增岗位和兼职岗位字段 +ALTER TABLE `sys_user` +ADD COLUMN `main_dep_post_id` varchar(32) NULL COMMENT '主岗位(部门岗位id)' AFTER `sign`, +ADD COLUMN `other_dep_post_id` varchar(1000) NULL COMMENT '兼职岗位(部门岗位id)' AFTER `main_dep_post_id`, +ADD INDEX `idx_su_main_dep_post_id`(`main_dep_post_id`) USING BTREE, +ADD INDEX `idx_su_other_dep_post_id`(`other_dep_post_id`) USING BTREE; + +-- author:wangshuai---date:20250821-for: 初始化职务数据及新增部门数据 --- + +-- 职务默认数据 +delete from sys_position; + +INSERT INTO `sys_position` (`id`, `code`, `name`, `post_level`, `company_id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1958470823064436737', '5za8WqucKR', '职员', 6, NULL, 'admin', '2025-08-21 18:06:46', NULL, NULL, 'A01A08', 0); +INSERT INTO `sys_position` (`id`, `code`, `name`, `post_level`, `company_id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1958470865577902082', 'hGAuYslALj', '副部长', 5, NULL, 'admin', '2025-08-21 18:06:56', NULL, NULL, 'A01A08', 0); +INSERT INTO `sys_position` (`id`, `code`, `name`, `post_level`, `company_id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1958470912214368258', 'M0xkqpPsg7', '部长', 4, NULL, 'admin', '2025-08-21 18:07:07', NULL, NULL, 'A01A08', 0); +INSERT INTO `sys_position` (`id`, `code`, `name`, `post_level`, `company_id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1958471030867034113', 'npEbkFq6Uw', '副总经理', 3, NULL, 'admin', '2025-08-21 18:07:35', NULL, NULL, 'A01A08', 0); +INSERT INTO `sys_position` (`id`, `code`, `name`, `post_level`, `company_id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1958471074953363458', 'DEPMkWRJEu', '总经理', 2, NULL, 'admin', '2025-08-21 18:07:46', NULL, NULL, 'A01A08', 0); +INSERT INTO `sys_position` (`id`, `code`, `name`, `post_level`, `company_id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1958471111989067778', 'gu7Rbffh0L', '董事长', 1, NULL, 'admin', '2025-08-21 18:07:54', NULL, NULL, 'A01A08', 0); + +-- 新增部门数据 +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958496243038556161', '', '控股集团', NULL, NULL, 0, NULL, '1', '1', 'A05', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:47:48', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958496444470005762', '1958496243038556161', '投资控股集团有限公司', NULL, NULL, 0, NULL, '4', '2', 'A05A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:48:34', 'admin', '2025-08-21 19:49:57', 0, 0, NULL, ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958496759810363394', '1958496243038556161', '城市运营管理集团有限公司', NULL, NULL, 1, NULL, '4', '2', 'A05A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:49:49', 'admin', '2025-08-21 20:30:23', 0, 0, NULL, ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958496836318662658', '1958496444470005762', '领导班子', NULL, NULL, 0, NULL, '2', '3', 'A05A01A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:50:08', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958496891561840641', '1958496444470005762', '办公室', NULL, NULL, 2, NULL, '2', '3', 'A05A01A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:50:21', 'admin', '2025-08-21 19:50:36', 0, 0, NULL, ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958496943017562114', '1958496444470005762', '财务管理中心', NULL, NULL, 3, NULL, '2', '3', 'A05A01A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:50:33', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958497020528300033', '1958496444470005762', '投资发展部', NULL, NULL, 4, NULL, '2', '3', 'A05A01A04', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:50:51', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958497164103520258', '1958496836318662658', '董事长', NULL, NULL, 0, NULL, '3', '4', 'A05A01A01A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:51:26', NULL, NULL, 0, 1, '1958471111989067778', ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958497256772472834', '1958496836318662658', '党委书记', NULL, NULL, 1, NULL, '3', '4', 'A05A01A01A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:51:48', 'admin', '2025-08-21 19:54:53', 0, 1, '1958471030867034113', '1958497164103520258'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958497591230468098', '1958496836318662658', '控股总经理', NULL, NULL, 3, NULL, '3', '4', 'A05A01A01A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:53:08', 'admin', '2025-08-21 19:54:42', 0, 1, '1958471074953363458', '1958497164103520258'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958497769387724802', '1958496836318662658', '纪委书记', NULL, NULL, 4, NULL, '3', '4', 'A05A01A01A04', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:53:50', 'admin', '2025-08-21 19:54:06', 0, 1, '1958471030867034113', '1958497591230468098'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958498187287203841', '1958496891561840641', '控股办公室主任', NULL, NULL, 1, NULL, '3', '4', 'A05A01A02A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:55:30', 'admin', '2025-08-21 19:55:50', 0, 1, '1958470912214368258', '1958497164103520258'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958498716927135745', '1958496891561840641', '副主任', NULL, NULL, 2, NULL, '3', '4', 'A05A01A02A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 19:57:36', 'admin', '2025-08-21 19:57:50', 0, 1, '1958470865577902082', '1958498187287203841'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958502219078733826', '1958496891561840641', '职员', NULL, NULL, 3, NULL, '3', '4', 'A05A01A02A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:11:31', NULL, NULL, 0, 1, '1958470823064436737', '1958498187287203841'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958502381062754305', '1958496943017562114', '主任', NULL, NULL, 1, NULL, '3', '4', 'A05A01A03A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:12:10', 'admin', '2025-08-21 20:13:13', 0, 1, '1958470912214368258', '1958502611426512898'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958502611426512898', '1958496836318662658', '控股副总经理', NULL, NULL, 5, NULL, '3', '4', 'A05A01A01A05', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:13:04', 'admin', '2025-08-21 20:27:14', 0, 1, '1958471030867034113', '1958497591230468098'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958502810966331393', '1958496943017562114', '副主任', NULL, NULL, 2, NULL, '3', '4', 'A05A01A03A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:13:52', 'admin', '2025-08-21 20:14:40', 0, 1, '1958470865577902082', '1958502381062754305'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958502942289989634', '1958496943017562114', '职员', NULL, NULL, 2, NULL, '3', '4', 'A05A01A03A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:14:23', 'admin', '2025-08-21 20:14:28', 0, 1, '1958470823064436737', '1958502810966331393'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958503159999533057', '1958497020528300033', '部长', NULL, NULL, 1, NULL, '3', '4', 'A05A01A04A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:15:15', NULL, NULL, 0, 1, '1958470912214368258', '1958502611426512898'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958503409933914114', '1958497020528300033', '副部长', NULL, NULL, 2, NULL, '3', '4', 'A05A01A04A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:16:15', NULL, NULL, 0, 1, '1958470865577902082', '1958503159999533057'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958503468805165058', '1958497020528300033', '员工', NULL, NULL, 3, NULL, '3', '4', 'A05A01A04A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:16:29', NULL, NULL, 0, 1, '1958470823064436737', '1958503409933914114'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958507136782733313', '1958496759810363394', '领导班子', NULL, NULL, 1, NULL, '2', '3', 'A05A02A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:31:03', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958507356253884418', '1958496759810363394', '信息技术发展有限公司', NULL, NULL, 4, NULL, '4', '3', 'A05A02A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:31:56', 'admin', '2025-08-21 21:12:57', 0, 0, NULL, ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958507448138502146', '1958507136782733313', '董事长', NULL, NULL, 1, NULL, '3', '4', 'A05A02A01A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:32:18', 'admin', '2025-08-21 20:33:24', 0, 1, '1958471111989067778', ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958507542866857985', '1958507136782733313', '副总经理', NULL, NULL, 3, NULL, '3', '4', 'A05A02A01A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:32:40', 'admin', '2025-08-21 20:33:26', 0, 1, '1958471030867034113', ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958507650828242946', '1958507136782733313', '总经理', NULL, NULL, 2, NULL, '3', '4', 'A05A02A01A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 20:33:06', 'admin', '2025-08-21 20:33:20', 0, 1, '1958471074953363458', ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958518943542972418', '1958507356253884418', '领导班子', NULL, NULL, 1, NULL, '2', '4', 'A05A02A03A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:17:58', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958519010207240193', '1958507356253884418', '综合管理部', NULL, NULL, 2, NULL, '2', '4', 'A05A02A03A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:18:14', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958519045623943169', '1958507356253884418', '财务部', NULL, NULL, 3, NULL, '2', '4', 'A05A02A03A03', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:18:23', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958519097058693121', '1958507356253884418', '软件研发部', NULL, NULL, 4, NULL, '2', '4', 'A05A02A03A04', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:18:35', NULL, NULL, 0, 0, NULL, NULL); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958520331257810945', '1958496891561840641', '总工程师', NULL, NULL, 2, NULL, '3', '4', 'A05A01A02A04', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:23:29', 'admin', '2025-08-21 21:23:52', 0, 1, '1958471030867034113', '1958497164103520258'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958520788395003906', '1958496759810363394', '办公室', NULL, NULL, 2, NULL, '2', '3', 'A05A02A04', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:25:18', 'admin', '2025-08-21 21:26:51', 0, 0, NULL, ''); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958520876810932225', '1958520788395003906', '总工程师', NULL, NULL, 1, NULL, '3', '4', 'A05A02A04A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:25:39', 'admin', '2025-08-21 21:26:25', 0, 1, '1958471030867034113', '1958507650828242946'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958521034948775937', '1958518943542972418', '执行董事兼总经理', NULL, NULL, 1, NULL, '3', '5', 'A05A02A03A01A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:26:17', NULL, NULL, 0, 1, '1958471111989067778', '1958520876810932225'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958521513564999681', '1958518943542972418', '副总经理', NULL, NULL, 2, NULL, '3', '5', 'A05A02A03A01A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:28:11', NULL, NULL, 0, 1, '1958471030867034113', '1958521034948775937'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958521634549698561', '1958519010207240193', '副部长', NULL, NULL, 1, NULL, '3', '5', 'A05A02A03A02A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:28:40', 'admin', '2025-08-21 21:30:15', 0, 1, '1958470865577902082', '1958521034948775937'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958523766002716674', '1958519010207240193', '信息归档员', NULL, NULL, 2, NULL, '3', '5', 'A05A02A03A02A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:37:08', NULL, NULL, 0, 1, '1958470823064436737', '1958521634549698561'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958524282631917570', '1958519045623943169', '部长', NULL, NULL, 1, NULL, '3', '5', 'A05A02A03A03A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:39:11', NULL, NULL, 0, 1, '1958470912214368258', '1958521034948775937'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958524369311404033', '1958519045623943169', '出纳', NULL, NULL, 2, NULL, '3', '5', 'A05A02A03A03A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:39:32', NULL, NULL, 0, 1, '1958470823064436737', '1958524282631917570'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958524471841165313', '1958519097058693121', '项目经理', NULL, NULL, 1, NULL, '3', '5', 'A05A02A03A04A01', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:39:56', NULL, NULL, 0, 1, '1958470912214368258', '1958521513564999681'); +INSERT INTO `sys_depart` (`id`, `parent_id`, `depart_name`, `depart_name_en`, `depart_name_abbr`, `depart_order`, `description`, `org_category`, `org_type`, `org_code`, `mobile`, `fax`, `address`, `memo`, `status`, `del_flag`, `qywx_identifier`, `ding_identifier`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`, `iz_leaf`, `position_id`, `dep_post_parent_id`) VALUES ('1958524565596442626', '1958519097058693121', '软件工程师', NULL, NULL, 2, NULL, '3', '5', 'A05A02A03A04A02', NULL, NULL, NULL, NULL, NULL, '0', NULL, NULL, 'admin', '2025-08-21 21:40:19', NULL, NULL, 0, 1, '1958470823064436737', '1958524471841165313'); + + +-- ---author:scott---date:20250824-----for: 修改部门表字段注释说明 +ALTER TABLE `sys_depart` + MODIFY COLUMN `org_type` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '树深度层级level' AFTER `org_category`; + +-- --author:liusq---date:20250827-----for: 删除代理配置菜单 +DELETE FROM sys_permission WHERE id = "1948648516302536706"; +DELETE FROM sys_role_permission WHERE permission_id = "1948648516302536706"; + +-- ---author:scott---date:20250827-----for:[QQYUN-13516]有时通过AI生成简历,解析json报错 +UPDATE `airag_flow` SET `application_name` = 'ghb', `name` = '示例_AI生成在线简历', `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'215734195065536512\'),\n enhanceJava.tag(\'215740280715427840\'),\n end.tag(\'215735188368998400\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":404,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"个人简介\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"profile\",\"name\":\"基础信息\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"215734195065536512\",\"type\":\"llm\",\"x\":739,\"y\":406,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你必须只输出合法且可被 JSON.parse() 正确解析的 JSON。\\n不要输出任何解释、注释或 JSON 以外的文字。\\n\\n\\nJSON 结构规则:\\n- 每个对象表示一个内容块。\\n- 字段说明:\\n• \\\"type\\\":内容类型,可选:\\\"title\\\"(标题)、\\\"list\\\"(列表)、\\\"separator\\\"(分隔线)、\\\"hyperlink\\\"(超链接)、\\\"pageBreak\\\"(分页符)、\\\"tab\\\"(制表符)、\\\"\\\"(普通文本)、\\\"superscript\\\"(上标)、\\\"subscript\\\"(下标)、\\\"table\\\"(表格)。\\n• \\\"level\\\":标题层级,仅当 type 为 \\\"title\\\" 时使用,取值:\\\"first\\\" ~ \\\"sixth\\\"。\\n• \\\"value\\\":文本、图片地址、超链接等。\\n• \\\"valueList\\\":数组,用于标题、列表、超链接等,数组元素支持 \\\"value\\\" 及样式字段。\\n• \\\"listType\\\":列表类型,取值:\\\"ul\\\"(无序)、\\\"ol\\\"(有序)。\\n• \\\"listStyle\\\":列表样式,如 \\\"disc\\\"、\\\"decimal\\\"、\\\"circle\\\"、\\\"square\\\"、\\\"checkbox\\\"。\\n• \\\"trList\\\"、\\\"colgroup\\\":表格行列定义,仅用于 \\\"table\\\"。\\n• 样式字段:\\\"font\\\"、\\\"size\\\"、\\\"bold\\\"、\\\"color\\\"、\\\"italic\\\"、\\\"highlight\\\"、\\\"underline\\\"、\\\"strikeout\\\"。\\n• \\\"dashArray\\\":用于 \\\"separator\\\"。\\n• 其他样式字段:\\\"rowFlex\\\"(\\\"left\\\"、\\\"center\\\"、\\\"right\\\"、\\\"alignment\\\")、\\\"backgroundColor\\\"、\\\"verticalAlign\\\"、\\\"textDecoration\\\"。\\n- 当 type = \\\"title\\\" 时,\\\"value\\\" 必须以 \\\"\\\\n\\\" 结尾。\\n- 主动换行请使用 `{ \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"\\\\n\\\" }`,不同对象之间不会自动换行。\\n- 所有键名和字符串必须使用英文双引号 `\\\"`。\\n\\n\\n输出必须严格是 JSON 数组,例如:\\n[\\n{\\n\\\"type\\\": \\\"title\\\",\\n\\\"level\\\": \\\"first\\\",\\n\\\"valueList\\\": [{ \\\"value\\\": \\\"主标题示例\\\\n\\\", \\\"font\\\": \\\"微软雅黑\\\", \\\"size\\\": 26, \\\"bold\\\": true, \\\"rowFlex\\\": \\\"center\\\" }]\\n},\\n{ \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"普通文本内容示例\\\" },\\n{\\n\\\"type\\\": \\\"list\\\",\\n\\\"listType\\\": \\\"ul\\\",\\n\\\"listStyle\\\": \\\"disc\\\",\\n\\\"valueList\\\": [\\n{ \\\"value\\\": \\\"列表项1\\\" },\\n{ \\\"value\\\": \\\"列表项2\\\" }\\n]\\n}\\n]\\n\\n\\n执行步骤:\\n1. 根据用户需求生成json数据\\n2. 检查生产的json数据是否正确。如果正常,输出给用户;否则重新生成。\"},{\"role\":\"user\",\"content\":\"请根据以上字段和示例,生成一个完整的个人简历文档 JSON。\\n- 至少包含基础信息、个人优势、工作经历、项目经理、教育经历等模块。\\n- 若基础数据不足,可以适当生成参考数据。\\n- 用户信息如下:\\n基础资料:{{base}}\\n简介:{{profile}}\"}]},\"inputParams\":[{\"field\":\"profile\",\"name\":\"base\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"profile\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"215735188368998400\",\"type\":\"end\",\"x\":1577,\"y\":354,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"height\":114,\"width\":332}},{\"id\":\"215740280715427840\",\"type\":\"enhanceJava\",\"x\":1156,\"y\":352,\"properties\":{\"text\":\"Java 增强\",\"options\":{\"enhance\":{\"type\":\"spring\",\"path\":\"ghbDemoAiWordGen\"}},\"inputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"215734195065536512\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"215734195073925120\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"215734195065536512\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"215734195065536512_input\",\"pointsList\":[{\"x\":466,\"y\":389},{\"x\":566,\"y\":389},{\"x\":473,\"y\":347},{\"x\":573,\"y\":347}]},{\"id\":\"215740280719622144\",\"type\":\"base-edge\",\"sourceNodeId\":\"215734195065536512\",\"targetNodeId\":\"215740280715427840\",\"sourceAnchorId\":\"215734195065536512_output\",\"targetAnchorId\":\"215740280715427840_input\",\"pointsList\":[{\"x\":905,\"y\":347},{\"x\":1005,\"y\":347},{\"x\":890,\"y\":293},{\"x\":990,\"y\":293}]},{\"id\":\"215740398487289856\",\"type\":\"base-edge\",\"sourceNodeId\":\"215740280715427840\",\"targetNodeId\":\"215735188368998400\",\"sourceAnchorId\":\"215740280715427840_output\",\"targetAnchorId\":\"215735188368998400_input\",\"pointsList\":[{\"x\":1322,\"y\":293},{\"x\":1422,\"y\":293},{\"x\":1311,\"y\":328},{\"x\":1411,\"y\":328}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"个人简介\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"profile\",\"name\":\"基础信息\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1952634605517447170'; + +-- ---author:chenrui---date:20250828-----for:【QQYUN-13449】 AI加使用辅助引导(目前有这功能,用户也不会用) +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'173365501230346240\')).to(\n THEN(\n llm.tag(\'172956395755208704\'),\n end.tag(\'172957153284259840\')\n ).tag(\"172956395755208704\"),\n THEN(\n llm.tag(\'173365800833675264\'),\n end.tag(\'173366253646540800\')\n ).tag(\"173365800833675264\"),\n end.tag(\'173366439085109248\'),\n THEN(\n llm.tag(\'175149164433014784\'),\n end.tag(\'175153953988444160\')\n ).tag(\"175149164433014784\"),\n THEN(\n llm.tag(\'175505963485245440\'),\n end.tag(\'175506006644633600\')\n ).tag(\"175505963485245440\"),\n THEN(\n llm.tag(\'175807569594040320\'),\n end.tag(\'175808663015538688\')\n ).tag(\"175807569594040320\"),\n THEN(\n llm.tag(\'221504502491222016\'),\n end.tag(\'221512800426758144\')\n ).tag(\"221504502491222016\"),\n THEN(\n llm.tag(\'223992240450801664\'),\n end.tag(\'223993058876952576\')\n ).tag(\"223992240450801664\")\n ).tag(\'173365501230346240\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":262,\"y\":458,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"ddl\",\"name\":\"表结构\",\"type\":\"string\",\"required\":true},{\"field\":\"dbtype\",\"name\":\"数据库类型\",\"type\":\"string\",\"required\":true},{\"field\":\"bizType\",\"name\":\"业务类型\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"172956395755208704\",\"type\":\"llm\",\"x\":1166,\"y\":160,\"properties\":{\"text\":\"生成sql\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:SQL生成助手\\n你是一个专业的SQL语句生成工具,能够根据用户提供的描述和表结构自动生成高效的SQL查询语句。\\n\\n## 目标:\\n- 根据用户的描述生成准确的SQL查询语句。\\n\\n## 技能:\\n1. 理解用户提供的需求和表结构。\\n2. 自动构建符合SQL语法的查询语句。\\n3. 优化生成的SQL以提高执行效率。\\n\\n## 工作流:\\n1. 接收用户描述和表结构信息。\\n2. 分析用户需求,确定所需的SQL操作类型(如查询、插入、更新、删除)。\\n3. 根据分析结果生成相应的SQL语句。\\n\\n## 输出格式:\\n- 生成的SQL语句应为标准格式,如:SELECT * FROM table_name ;\\n- 将输出的SQL语句格式化\\n- 只输出sql语句,不要额外解释,不要md语法,不要换行符,不要有sql注释。\\n\\n## 限制:\\n\\n- 除非明确说明,否则不要生成查询条件\\n- 确保生成的SQL语句符合数据库的语法要求,确保sql能直接执行。\\n- 确保字段和表能正确对应。\"},{\"role\":\"user\",\"content\":\"表结构:\\n{{ddl}}\\n---------\\n数据库类型:\\n{{dbtype}}\\n----------\\n需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"dbtype\",\"name\":\"dbtype\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"172957153284259840\",\"type\":\"end\",\"x\":1643,\"y\":129,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"sql\",\"nodeId\":\"172956395755208704\"}],\"height\":114,\"width\":332}},{\"id\":\"173365501230346240\",\"type\":\"switch\",\"x\":688,\"y\":575,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genSql\"}],\"next\":\"172956395755208704\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genJsonRows\"}],\"next\":\"173365800833675264\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"chooseTables\"}],\"next\":\"175149164433014784\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genChart\"}],\"next\":\"175505963485245440\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"intentCheck\"}],\"next\":\"175807569594040320\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"designReport\"}],\"next\":\"221504502491222016\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"bizType\",\"operator\":\"EQUALS\",\"value\":\"genPrompt\"}],\"next\":\"223992240450801664\"}],\"else\":{\"next\":\"173366439085109248\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":274,\"width\":332}},{\"id\":\"173365800833675264\",\"type\":\"llm\",\"x\":1167,\"y\":368,\"properties\":{\"text\":\"生成rows\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你是一个 **“在线报表 JSON 生成器”**,能够理解用户描述及数据集,并生成符合规范的 **合法 JSON**。  \\n\\n\\n---\\n\\n\\n## 工作流程\\n\\n\\n### 步骤一:数据集选择  \\n1. 读取用户提供的数据集结构。  \\n2. 根据需求从中选定一个数据集。  \\n3. 后续只能使用选定数据集的字段。  \\n\\n\\n### 步骤二:报表设计  \\n根据用户需求与数据集,输出报表的结构信息:  \\n- 行号、列号(从0开始)  \\n- 单元格内容(文字或数据集占位符)  \\n- 单元格样式(引用样式索引)  \\n- 合并单元格信息  \\n\\n\\n### 步骤三:生成报表 JSON  \\n使用步骤二的描述生成完整 JSON。  \\n\\n\\n---\\n\\n\\n## 输出要求\\n1. **输出必须是合法 JSON**,能直接 `JSON.parse()`。  \\n2. 必须包含以下部分:  \\n   - `\\\"styles\\\"`:样式数组,每种样式单独定义,在单元格中用索引引用。  \\n   - `\\\"merges\\\"`:合并单元格范围(如 `\\\"D3:E4\\\"`)。  \\n   - `\\\"rows\\\"`:行数据,每行包含 `cells`,每个 `cell` 可有:  \\n     - `\\\"text\\\"`:文字或占位符(`${}` 对象,`#{}` 集合)  \\n     - `\\\"style\\\"`:引用 `styles` 索引  \\n     - `\\\"merge\\\"`:合并范围 `[纵向合并格数, 横向合并格数]`  \\n     - `\\\"height\\\"`:行高  \\n   - `\\\"cols\\\"`:列宽配置  \\n\\n\\n---\\n\\n\\n## 样式规则\\n- 样式在 `\\\"styles\\\"` 中定义:  \\n  - `font`:字体\\n    - `bold`: 是否加粗(如:`true`)\\n    - `italic`: 是否斜体(如:`true`)\\n    - `size`: 字体大小单位pt,默认10\\n  - `underline`: 下划线(如:`true`)\\n  - `strike`: 删除线(如:`true`)\\n  - `color`(字体颜色)  \\n  - `bgcolor`(背景色)  \\n  - `align`(left/center/right)  \\n  - `valign`(top/middle/bottom)  \\n  - `textwrap`(自动换行)  \\n  - `border`\\n    - `top`:上边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n    - `bottom`:下边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n    - `left`:左边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n    - `right`:右边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n\\n\\n---\\n\\n\\n## 数据集规则\\n示例:\\n```json\\n{\\n  \\\"code\\\": \\\"a\\\",\\n  \\\"title\\\": \\\"a\\\",\\n  \\\"isList\\\": \\\"1\\\",\\n  \\\"children\\\": [\\n    { \\\"title\\\": \\\"total_sales\\\", \\\"fieldText\\\": \\\"总销量\\\" },\\n    { \\\"title\\\": \\\"total_returns\\\", \\\"fieldText\\\": \\\"总退货数量\\\" }\\n  ]\\n}\\n```\\n- `code`:数据集变量名  \\n- `isList = 1`:集合  \\n- `isList = 0`:对象  \\n- `children`:字段,含 `title`(字段名)、`fieldText`(展示名)  \\n\\n\\n---\\n\\n\\n## 行列与填充规则\\n- 行号、列号从0开始。  \\n- `\\\"cols\\\"` 设置列宽。  \\n- **集合 (`isList=1`)**:  \\n  - 一行字段标题(children.fieldText)  \\n  - 下一行字段占位符(`#{code.title}`)  \\n- **对象 (`isList=0`)**:  \\n  - 每字段占两列:左列为标题,右列为占位符 `${code.title}`  \\n  - 可按组横向排列  \\n\\n\\n---\\n\\n\\n## 合并规则\\n- `\\\"merge\\\": [纵向合并格数, 横向合并格数]`  \\n  - 纵向合并格数与横向合并格数是不包含当前单元格的数量(如 纵向合并格数 等于1 就是向下合并一行;横向合并格数同理)\\n- 被合并覆盖的单元格无需再定义  \\n\\n\\n---\\n\\n\\n## 特别说明\\n- JSON 必须 **纯净**:无注释、无 markdown、无省略号。  \\n- 用户指定的样式不能改动,可在此基础上做美化。\\n- 除非用户明确要求,默认都对生成的报表做基础美化(如增加边框、设置字体、设置背景色)\\n- 用户描述的行列序号需 **减一** 转换为下标。  \\n- 仅生成一份报表 JSON。  \\n\\n\\n---\\n\\n\\n## 示例\\n```json\\n{\\n  \\\"styles\\\": [\\n    { \\\"font\\\": { \\\"bold\\\": true } },\\n    { \\\"color\\\": \\\"#ff0000\\\" }\\n  ],\\n  \\\"rows\\\": {\\n    \\\"0\\\": {\\n      \\\"cells\\\": {\\n        \\\"0\\\": { \\\"text\\\": \\\"加粗文字\\\", \\\"style\\\": 0 },\\n        \\\"1\\\": { \\\"text\\\": \\\"红色文字\\\", \\\"style\\\": 1 },\\n        \\\"2\\\": { \\\"text\\\": \\\"${dbKey.dbField}\\\", \\\"style\\\": 1 }\\n      }\\n    }\\n  },\\n  \\\"cols\\\": {\\n    \\\"1\\\": { \\\"width\\\": 100 }\\n  },\\n  \\\"merges\\\": [\\\"A1:B1\\\"]\\n}\\n```\\n\\n\"},{\"role\":\"user\",\"content\":\"用户数据集:\\n{{ddl}}\\n用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"173366253646540800\",\"type\":\"end\",\"x\":1643,\"y\":336,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"designJson\",\"nodeId\":\"173365800833675264\"}],\"height\":114,\"width\":332}},{\"id\":\"173366439085109248\",\"type\":\"end\",\"x\":1166,\"y\":1662,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"error:选择正确的业务类型\"},\"inputParams\":[],\"outputParams\":[],\"height\":114,\"width\":332}},{\"id\":\"175149164433014784\",\"type\":\"llm\",\"x\":1164,\"y\":598,\"properties\":{\"text\":\"选择表\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":2,\"messages\":[{\"role\":\"system\",\"content\":\"## 任务\\n根据用户需求,从下方数据库表列表中选择所有关联的表名称。\\n\\n\\n## 数据库表列表(格式:表名 | 注释)\\n{{ddl}}\\n\\n## 输出规则\\n1. 严格按JSON数组格式输出,例如:[\\\"order\\\"]。\\n2. 仅包含表名称,无需注释。\\n3. **禁止添加列表外的表**。\\n4. 表的选择范围可以适当大一些。\\n4. 无业务相关性时输出空数组:[]\\n\\n\\n请回复纯JSON,不要包含其他内容。\"},{\"role\":\"user\",\"content\":\"用户需求:{{question}}\"}]},\"inputParams\":[{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175153953988444160\",\"type\":\"end\",\"x\":1643,\"y\":564,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"tables\",\"nodeId\":\"175149164433014784\"}],\"height\":114,\"width\":332}},{\"id\":\"175505963485245440\",\"type\":\"llm\",\"x\":1166,\"y\":802,\"properties\":{\"text\":\"生成图表\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"根据以下数据以及用户需求生成符合格式要求的图表数据。\\n\\n\\n## 工作流程:\\n\\n\\n1. 根据用户需求选择一个合适的数据集\\n2. 根据数据集和需求,从图表列表中选择一个合适的图标类型。\\n3. 组装最终输出的json\\n\\n\\n⸻\\n## 可选的图表如下(标识|描述):\\n\\n\\n- 1维图表\\n - bar.simple|普通柱形图\\n - bar.background|带背景柱形图\\n - bar.horizontal|横向柱形图\\n - line.simple|普通折线图\\n - line.area|面积堆积折线图\\n - line.smooth|平滑曲线折线图\\n - line.step|阶梯折线图\\n - pie.simple|普通饼图\\n - pie.doughnut|环状饼图\\n - pie.rose|南丁格尔玫瑰饼图\\n - scatter.simple|普通散点图\\n - funnel.simple|普通漏斗图\\n - funnel.pyramid|金字塔漏斗图\\n - pictorial.spirits|普通象形图\\n - map.scatter|点地图\\n - gauge.simple|360°仪表盘\\n - gauge.simple180|180°仪表盘\\n- 2维\\n - bar.multi|多数据对比柱形图\\n - bar.negative|正负条形图\\n - bar.stack|堆叠柱形图\\n - bar.stack.horizontal|堆叠条形图\\n - bar.multi.horizontal|多数据条形柱状图\\n - line.multi|多数据对比折线图\\n - mixed.linebar|普通折柱图\\n - scatter.bubble|气泡散点图\\n - radar.basic|普通雷达图\\n - radar.custom|圆形雷达图\\n⸻\\n## 数据集格式说明:\\n```\\n{\\n \\\"dbId\\\": \\\"1069915169263800320\\\",\\n \\\"code\\\": \\\"a\\\",\\n \\\"title\\\": \\\"a\\\",\\n \\\"isList\\\": \\\"1\\\",\\n \\\"type\\\": \\\"0\\\",\\n \\\"children\\\": [\\n {\\n \\\"title\\\": \\\"total_sales\\\",\\n \\\"fieldText\\\": \\\"total_sales\\\"\\n },\\n {\\n \\\"title\\\": \\\"total_returns\\\",\\n \\\"fieldText\\\": \\\"total_returns\\\"\\n }\\n ]\\n}\\n```\\n* code:数据集变量名\\n* isList:为”1”表示集合,“0”表示对象\\n* children:为字段列表,包含title(字段名)和fieldText(展示名)\\n* type:0|sql,1|api,2|code,3|json\\n⸻\\n## 输出json格式\\n{\\n \\\"dataType\\\": \\\"sql\\\",\\n \\\"apiStatus\\\": \\\"0\\\",\\n \\\"apiUrl\\\": \\\"\\\",\\n \\\"dataId\\\": \\\"1069898455939633152\\\",\\n \\\"axisX\\\": \\\"supplier_name\\\",\\n \\\"axisY\\\": \\\"total_returns\\\",\\n \\\"series\\\": \\\"material_name\\\",\\n \\\"yText\\\": \\\"total_returns\\\",\\n \\\"xText\\\": \\\"supplier_name\\\",\\n \\\"dbCode\\\": \\\"a\\\",\\n \\\"isCustomPropName\\\": false,\\n \\\"chartType\\\": \\\"line.multi\\\",\\n \\\"id\\\": \\\"0aGl4PUfbIfy8BMF\\\",\\n \\\"run\\\": 1,\\n \\\"title\\\": \\\"\\\",\\n}\\n* dataType:与数据集type对应(0|sql,1|api,2|code,3|json)\\n* dataId:对应数据集dbId\\n* dbCode:对应数据集的code\\n* axisX:分类属性,从数据集字段中取值(fieldText)\\n* axisY:值属性,从数据集字段中取值(fieldText)\\n* series: 系列,从数据集字段中取值(fieldText)\\n* xText:分类属性显示,从数据集字段中取值(title)\\n* yText:值属性显示,从数据集字段中取值(title)\\n* chartType:图表的标识\\n* title:为这个图表起一个标题\\n* isCustomPropName: 如果是api数据集,该值为true\\n* apiStatus: 如果是api数据集则等于\\\"1\\\",否则\\\"0\\\"\\n\\n\\n## 输出格式\\n* 直接返回JSON数据,不要解释,不要md语法,不要换行符,不要有注释。\\n* 确保输出的json格式正确完整。\"},{\"role\":\"user\",\"content\":\"## 用户数据集:\\n{{ddl}}\\n## 用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175506006644633600\",\"type\":\"end\",\"x\":1643,\"y\":769,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"chart\",\"nodeId\":\"175505963485245440\"}],\"height\":114,\"width\":332}},{\"id\":\"175807569594040320\",\"type\":\"llm\",\"x\":1166,\"y\":1018,\"properties\":{\"text\":\"意图识别\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你是一个“在线报表的需求分析器”,能够理解用户的需求输入,\\n\\n请根据用户需求与提供的数据集设计,综合判断应执行的工作流步骤,并为每个步骤调整需求描述,同时选择最合适的数据集。  \\n\\n\\n\\n\\n## 可选步骤(格式:标识 | 功能说明)\\n\\n- `genJsonRows` | 生成报表(可选,根据用户的需求描述和数据集设计生成合适的报表设计)\\n\\n- `genChart` | 生成图表(可选,根据用户的需求描述和数据集设计生成合适的图表数据)\\n\\n> **注意:** 至少选择一个步骤,亦可同时选择两者;图表的权重较低。\\n\\n\\n\\n\\n## 数据集格式\\n\\n```\\n\\n{\\n\\n\\\"dbId\\\": \\\"1069915169263800320\\\",\\n\\n\\\"code\\\": \\\"a\\\",\\n\\n\\\"title\\\": \\\"a\\\",\\n\\n\\\"isList\\\": \\\"1\\\",\\n\\n\\\"type\\\": \\\"0\\\",\\n\\n\\\"children\\\": [\\n\\n{\\n\\n\\\"title\\\": \\\"total_sales\\\",\\n\\n\\\"fieldText\\\": \\\"total_sales\\\"\\n\\n},\\n\\n{\\n\\n\\\"title\\\": \\\"total_returns\\\",\\n\\n\\\"fieldText\\\": \\\"total_returns\\\"\\n\\n}\\n\\n]\\n\\n}\\n\\n```\\n\\n* `code`:数据集变量名\\n\\n* `isList`:为”1”表示集合,“0”表示对象\\n\\n* `children`:为字段列表,包含title(展示名)和fieldText(字段名)\\n\\n* `type`:0|sql,1|api,2|code,3|json\\n\\n\\n\\n\\n## 输出格式\\n\\n步骤标识1|需求描述1|数据集code,步骤标识2|需求描述2|数据集code  \\n\\n* 各步骤之间用英文逗号,分隔  \\n\\n* 不得添加额外说明,不要md语法,不要换行符,不要有注释。\\n\\n\\n\\n\\n## 注意:\\n\\n- 在生成需求描述时,应确保不丢失原有需求的全部内容,只是并针对所选步骤微调。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"## 用户数据集:\\n{{ddl}}\\n## 用户需求:\\n{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"175808663015538688\",\"type\":\"end\",\"x\":1643,\"y\":985,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"intent\",\"nodeId\":\"175807569594040320\"}],\"height\":114,\"width\":332}},{\"id\":\"221504502491222016\",\"type\":\"llm\",\"x\":1166,\"y\":1237,\"properties\":{\"text\":\"生成excel设计\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你是一个“在线Excel JSON生成器”,能够理解用户描述并生成符合规范的 JSON。  \\n**严格规则**:\\n1. 只能根据描述生成 JSON。\\n2. JSON 必须合法,可被 `JSON.parse()` 正确解析。\\n3. JSON 中不可以有注释\\n\\n\\n\\n\\n---\\n\\n\\n\\n\\n## 步骤一:理解用户的描述,并生成单元格描述信息\\n   - 行号、列号\\n   - 单元格文字\\n   - 单元格样式(字体加粗、斜体、大小、颜色、背景色、水平/垂直对齐、自动换行、边框)\\n   - 合并单元格信息\\n\\n\\n\\n\\n⸻\\n\\n\\n\\n\\n## 步骤二:使用步骤一种生成的描述信息,生成完整在线Excel JSON\\n### 输出要求\\n- 输出必须是 **合法 JSON**,且能直接被 `JSON.parse()` 正确解析。  \\n- JSON 的结构必须包含以下部分:  \\n  - `\\\"styles\\\"`:样式数组,每个元素对应一种样式(如字体加粗、字体颜色、边框等),并在单元格里通过 `style` 字段引用  \\n  - `\\\"merges\\\"`:合并单元格区域(如 `\\\"D3:E4\\\"`)  \\n  - `\\\"rows\\\"`:行数据,每一行包含 `cells`,每个 `cell` 可包含:\\n    - `\\\"text\\\"`:单元格文字  \\n    - `\\\"style\\\"`:引用 `styles` 数组中的下标  \\n    - `\\\"merge\\\"`:若为合并单元格,标注合并范围 示例[1,2]:下标[0]纵向合并1格,下标[1]横向合并2格,\\n    - `\\\"height\\\"`:行高  \\n    - `\\\"width\\\"`:列宽(放在 `\\\"cols\\\"` 部分)  \\n  - `\\\"cols\\\"`:列宽配置  \\n\\n\\n\\n\\n### 样式规则\\n- 样式在 `\\\"styles\\\"` 中定义:  \\n  - `font`:字体\\n    - `bold`: 是否加粗(如:`true`)\\n    - `italic`: 是否斜体(如:`true`)\\n    - `size`: 字体大小单位pt,默认10\\n  - `underline`: 下划线(如:`true`)\\n  - `strike`: 删除线(如:`true`)\\n  - `color`(字体颜色)  \\n  - `bgcolor`(背景色)  \\n  - `align`(left/center/right)  \\n  - `valign`(top/middle/bottom)  \\n  - `textwrap`(自动换行)  \\n  - `border`\\n    - `top`:上边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n    - `bottom`:下边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n    - `left`:左边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n    - `right`:右边框(如 `[\\\"thin\\\",\\\"#000\\\"]`)  \\n\\n\\n\\n\\n### 行列规则\\n- `\\\"rows\\\"` 中的 key 是行号(从 0 开始)  \\n- `\\\"cells\\\"` 中的 key 是列号(从 0 开始)  \\n- 可指定 `\\\"height\\\"` 设置行高  \\n- `\\\"cols\\\"` 中的 key 是列号,值包含 `\\\"width\\\"` 设置列宽  \\n\\n\\n\\n\\n## 合并规则\\n- `\\\"merge\\\": [纵向合并格数, 横向合并格数]`  \\n  - 纵向合并格数与横向合并格数是不包含当前单元格的数量(如 纵向合并格数 等于1 就是向下合并一行;横向合并格数同理)\\n- 被合并覆盖的单元格无需再定义  \\n\\n\\n\\n\\n## 示例\\n(简化示例)\\n\\n\\n\\n\\n```json\\n{\\n  \\\"styles\\\": [\\n    { \\\"font\\\": { \\\"bold\\\": true } },\\n    { \\\"color\\\": \\\"#ff0000\\\" }\\n  ],\\n  \\\"rows\\\": {\\n    \\\"0\\\": {\\n      \\\"cells\\\": {\\n        \\\"0\\\": { \\\"text\\\": \\\"加粗文字\\\", \\\"style\\\": 0 },\\n        \\\"1\\\": { \\\"text\\\": \\\"红色文字\\\", \\\"style\\\": 1 }\\n      }\\n    }\\n  },\\n  \\\"cols\\\": {\\n    \\\"1\\\": { \\\"width\\\": 100 }\\n  },\\n  \\\"merges\\\": [\\\"A1:B1\\\"],\\n}\\n\\n\\n\\n\\n## 特别说明\\n- JSON 必须 **纯净**:无注释、无 markdown、无省略号。  \\n- 用户指定的样式不能改动,可在此基础上做美化。\\n- 除非用户明确要求,默认都对生成的报表做基础美化(如增加边框、设置字体、设置背景色)\\n- 用户描述的行列序号需 **减一** 转换为下标。  \\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"理解用户需求,并按要求生成json数据。\\n用户需求如下:\\n{{question}}\\n\\n\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"221512800426758144\",\"type\":\"end\",\"x\":1643,\"y\":1201,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"designJson\",\"nodeId\":\"221504502491222016\"}],\"height\":114,\"width\":332}},{\"id\":\"223992240450801664\",\"type\":\"llm\",\"x\":1166,\"y\":1441,\"properties\":{\"text\":\"提示词生成\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 报表生成提示词优化器\\n\\n\\n## 目标\\n\\n\\n根据用户输入的需求和数据集定义,自动生成简洁、明确的用户 message。\\n系统会自动带上数据集定义,因此无需包含数据集定义内容。\\n\\n\\n## 工作流程\\n\\n\\n### 步骤一:理解需求与数据集\\n\\n\\n- 从用户的自然语言需求和数据集定义中,提取**业务方向**。\\n- 业务方向示例:\\n    - 个人简历\\n    - 项目报价\\n    - 数据统计\\n    - 财务报表\\n    - 产品清单\\n\\n\\n### 步骤二:扩展提示词\\n\\n\\n- 针对识别出的业务方向,扩展提示词,使其更贴合业务场景。\\n- 示例:\\n    如果用户需求是\\\"生成一份个人简历\\\",则扩展提示词为:\\n    \\\"请基于数据集生成个人简历模版,突出教育背景、工作经历和技能展示。\\\"\\n\\n\\n### 步骤三:生成用户 message\\n\\n\\n- 输出最终的用户 message,不包含系统提示词,不包含数据集定义。\\n- 要求:\\n    - 保留用户需求的原意。\\n    - 优化表达,使 AI 更好地理解并执行任务。\\n    - 根据业务方向,附加必要的模版说明。\\n    - 提示词结构最好包含:\\n        - 主要需求:用户的主要需求,比如:生成一个用于软件产品的报价表。\\n        - 结构要求:对于生成的内容的要求\\n        - 样式要求:对样式的整体和细节的要求,比如:整体排版美观、标题使用16号字。\\n\\n\\n#### 输出示例:\\n\\n\\n```\\n生成一个 **员工薪资报表**,要求如下:  \\n\\n\\n1. **数据内容**  \\n   - 报表需要展示以下信息:员工姓名、性别、生日、联系电话、薪资。  \\n\\n\\n2. **样式要求**  \\n   - 添加一个醒目的报表标题,字体16号。  \\n   - 标题行使用蓝色背景,并且字体加粗。  \\n   - 数据行保持清晰整齐,便于阅读。  \\n\\n\\n3. **输出要求**  \\n   - 表格内容规范,排版美观,符合员工薪资报表的格式。 \\n```\\n\\n\\n\\n\\n## 输出要求\\n\\n\\n- 最终输出为简洁明了的用户 message。\\n- 不限定关键词和字段,完全根据需求和数据集定义生成。\\n- 控制长度,不要超过500字。\\n\\n\"},{\"role\":\"user\",\"content\":\"用户需求:\\n{{question}}\\n数据集定义\\n{{ddl}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"},{\"field\":\"ddl\",\"name\":\"ddl\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"223993058876952576\",\"type\":\"end\",\"x\":1652,\"y\":1408,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"prompt\",\"nodeId\":\"223992240450801664\"}],\"height\":114,\"width\":332}}],\"edges\":[{\"id\":\"172957153288454144\",\"type\":\"base-edge\",\"sourceNodeId\":\"172956395755208704\",\"targetNodeId\":\"172957153284259840\",\"sourceAnchorId\":\"172956395755208704_output\",\"targetAnchorId\":\"172957153284259840_input\",\"pointsList\":[{\"x\":1332,\"y\":101},{\"x\":1432,\"y\":101},{\"x\":1377,\"y\":103},{\"x\":1477,\"y\":103}]},{\"id\":\"173365501234540544\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"173365501230346240\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"173365501230346240_input\",\"pointsList\":[{\"x\":428,\"y\":443},{\"x\":528,\"y\":443},{\"x\":422,\"y\":469},{\"x\":522,\"y\":469}]},{\"id\":\"173366253650735104\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365800833675264\",\"targetNodeId\":\"173366253646540800\",\"sourceAnchorId\":\"173365800833675264_output\",\"targetAnchorId\":\"173366253646540800_input\",\"pointsList\":[{\"x\":1333,\"y\":309},{\"x\":1433,\"y\":309},{\"x\":1377,\"y\":310},{\"x\":1477,\"y\":310}]},{\"id\":\"173372961415852032\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"172956395755208704\",\"sourceAnchorId\":\"173365501230346240_source_if\",\"targetAnchorId\":\"172956395755208704_input\",\"pointsList\":[{\"x\":854,\"y\":503},{\"x\":954,\"y\":503},{\"x\":900,\"y\":101},{\"x\":1000,\"y\":101}]},{\"id\":\"173372967073968128\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"173365800833675264\",\"sourceAnchorId\":\"173365501230346240_case_2\",\"targetAnchorId\":\"173365800833675264_input\",\"pointsList\":[{\"x\":854,\"y\":529},{\"x\":954,\"y\":529},{\"x\":901,\"y\":309},{\"x\":1001,\"y\":309}]},{\"id\":\"173372974988619776\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"173366439085109248\",\"sourceAnchorId\":\"173365501230346240_source_else\",\"targetAnchorId\":\"173366439085109248_input\",\"pointsList\":[{\"x\":854,\"y\":685},{\"x\":954,\"y\":685},{\"x\":900,\"y\":1636},{\"x\":1000,\"y\":1636}]},{\"id\":\"175149164437209088\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175149164433014784\",\"sourceAnchorId\":\"173365501230346240_case_3\",\"targetAnchorId\":\"175149164433014784_input\",\"pointsList\":[{\"x\":854,\"y\":555},{\"x\":954,\"y\":555},{\"x\":898,\"y\":539},{\"x\":998,\"y\":539}]},{\"id\":\"175153997969915904\",\"type\":\"base-edge\",\"sourceNodeId\":\"175149164433014784\",\"targetNodeId\":\"175153953988444160\",\"sourceAnchorId\":\"175149164433014784_output\",\"targetAnchorId\":\"175153953988444160_input\",\"pointsList\":[{\"x\":1330,\"y\":539},{\"x\":1430,\"y\":539},{\"x\":1377,\"y\":538},{\"x\":1477,\"y\":538}]},{\"id\":\"175505963489439744\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175505963485245440\",\"sourceAnchorId\":\"173365501230346240_case_4\",\"targetAnchorId\":\"175505963485245440_input\",\"pointsList\":[{\"x\":854,\"y\":581},{\"x\":954,\"y\":581},{\"x\":900,\"y\":743},{\"x\":1000,\"y\":743}]},{\"id\":\"175506006648827904\",\"type\":\"base-edge\",\"sourceNodeId\":\"175505963485245440\",\"targetNodeId\":\"175506006644633600\",\"sourceAnchorId\":\"175505963485245440_output\",\"targetAnchorId\":\"175506006644633600_input\",\"pointsList\":[{\"x\":1332,\"y\":743},{\"x\":1432,\"y\":743},{\"x\":1377,\"y\":743},{\"x\":1477,\"y\":743}]},{\"id\":\"175807569598234624\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"175807569594040320\",\"sourceAnchorId\":\"173365501230346240_case_5\",\"targetAnchorId\":\"175807569594040320_input\",\"pointsList\":[{\"x\":854,\"y\":607},{\"x\":954,\"y\":607},{\"x\":900,\"y\":959},{\"x\":1000,\"y\":959}]},{\"id\":\"175808663019732992\",\"type\":\"base-edge\",\"sourceNodeId\":\"175807569594040320\",\"targetNodeId\":\"175808663015538688\",\"sourceAnchorId\":\"175807569594040320_output\",\"targetAnchorId\":\"175808663015538688_input\",\"pointsList\":[{\"x\":1332,\"y\":959},{\"x\":1432,\"y\":959},{\"x\":1377,\"y\":959},{\"x\":1477,\"y\":959}]},{\"id\":\"221512800426758145\",\"type\":\"base-edge\",\"sourceNodeId\":\"221504502491222016\",\"targetNodeId\":\"221512800426758144\",\"sourceAnchorId\":\"221504502491222016_output\",\"targetAnchorId\":\"221512800426758144_input\",\"pointsList\":[{\"x\":1332,\"y\":1178},{\"x\":1432,\"y\":1178},{\"x\":1377,\"y\":1175},{\"x\":1477,\"y\":1175}]},{\"id\":\"221534054756093952\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"221504502491222016\",\"sourceAnchorId\":\"173365501230346240_case_6\",\"targetAnchorId\":\"221504502491222016_input\",\"pointsList\":[{\"x\":854,\"y\":633},{\"x\":954,\"y\":633},{\"x\":900,\"y\":1178},{\"x\":1000,\"y\":1178}]},{\"id\":\"223992240454995968\",\"type\":\"base-edge\",\"sourceNodeId\":\"173365501230346240\",\"targetNodeId\":\"223992240450801664\",\"sourceAnchorId\":\"173365501230346240_case_7\",\"targetAnchorId\":\"223992240450801664_input\",\"pointsList\":[{\"x\":854,\"y\":659},{\"x\":954,\"y\":659},{\"x\":900,\"y\":1382},{\"x\":1000,\"y\":1382}]},{\"id\":\"223993058881146880\",\"type\":\"base-edge\",\"sourceNodeId\":\"223992240450801664\",\"targetNodeId\":\"223993058876952576\",\"sourceAnchorId\":\"223992240450801664_output\",\"targetAnchorId\":\"223993058876952576_input\",\"pointsList\":[{\"x\":1332,\"y\":1382},{\"x\":1432,\"y\":1382},{\"x\":1386,\"y\":1382},{\"x\":1486,\"y\":1382}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"text\",\"name\":\"prompt\",\"nodeId\":\"223992240450801664\"},{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"ddl\",\"name\":\"表结构\",\"required\":true,\"type\":\"string\"},{\"field\":\"dbtype\",\"name\":\"数据库类型\",\"required\":true,\"type\":\"string\"},{\"field\":\"bizType\",\"name\":\"业务类型\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1909856345692065793'; + +-- ---author:wangshuai---date:20250902-----for:【QQYUN-13415】租户改造菜单、菜单权限及租户管理员角色升级sql +-- 新增用户按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1214462306546319322', '119213522910765570', '新增用户', '', '', 1, NULL, NULL, 2, 'system:user:addTenantUser', '1', 1.00, 0, NULL, 1, 0, 0, NULL, NULL, 'admin', '2020-01-07 16:22:32', NULL, NULL, 0, 0, '1', 0); + +-- 删除用户按钮权限 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592114574275211345', '119213522910765570', '删除用户', NULL, NULL, 0, NULL, NULL, 2, 'system:user:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:17:49', NULL, NULL, 0, 0, '1', 0); + +-- 新增租户部门菜单升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1961009998209257473', '1674708136602542082', '租户部门', '/depart/TenantDepartList', 'system/depart/TenantDepartList', 1, '', NULL, 1, NULL, '0', 3.30, 0, 'ant-design:apartment-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-08-28 18:16:32', 'admin', '2025-08-29 10:20:25', 0, 0, NULL, 0); + +-- 添加一个用户和多个套餐关系按钮权限 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1960994076329316353', '119213522910765570', '添加一个用户和多个套餐关系', NULL, NULL, 0, NULL, NULL, 2, 'system:tenant:addPacksUser', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-08-28 17:13:16', NULL, NULL, 0, 0, '1', 0); + +-- 新增租户套餐菜单升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1961253156897710081', '1674708136602542082', '租户套餐', '/pack/TenantCurrentPackList', 'system/tenant/pack/TenantCurrentPackList', 1, '', NULL, 1, NULL, '0', 3.40, 0, 'ant-design:read-filled', 1, 0, 0, 0, NULL, 'admin', '2025-08-29 10:22:46', 'admin', '2025-08-29 10:24:46', 0, 0, NULL, 0); + +-- 产品包查询列表权限升级菜单 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1609123240547344376', '1961253156897710081', '产品包分页列表查询', NULL, NULL, 0, NULL, NULL, 2, 'system:tenant:packList', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-12-31 17:44:11', NULL, NULL, 0, 0, '1', 0); + +-- 我的租户变更为叶子节点 +UPDATE `sys_permission` SET `is_leaf` = 0 WHERE `id` = '1961253156897710081'; + +-- 查询租户下用户按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1600108123037913486', '1961253156897710081', '查询租户下用户', NULL, NULL, 0, NULL, NULL, 2, 'system:tenant:user:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-12-06 20:41:20', 'admin', '2023-01-11 12:10:48', 0, 0, '1', 0); + +-- 邀请人员加入 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1600105607009162230', '1961253156897710081', '邀请用户', NULL, NULL, 0, NULL, NULL, 2, 'system:tenant:invitation:user', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-12-06 20:31:20', NULL, NULL, 0, 0, '1', 0); + +-- 租户角色设置成非叶子节点 +UPDATE `sys_permission` SET `is_leaf` = 0 WHERE `id` = '1597419994965786625'; + +-- 角色编辑按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592120372296522490', '1597419994965786625', '角色编辑', NULL, NULL, 0, NULL, NULL, 2, 'system:role:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:40:52', NULL, NULL, 0, 0, '1', 0); + +-- 角色添加按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592120323667750934', '1597419994965786625', '角色添加', NULL, NULL, 0, NULL, NULL, 2, 'system:role:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:40:40', NULL, NULL, 0, 0, '1', 0); + +-- 角色删除按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592120427223412865', '1597419994965786625', '角色删除', NULL, NULL, 0, NULL, NULL, 2, 'system:role:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:41:05', NULL, NULL, 0, 0, '1', 0); + +-- 角色添加已有用户按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592102143467200514', '1597419994965786625', '给指定角色添加用户', NULL, NULL, 0, NULL, NULL, 2, 'system:user:addUserRole', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:18:49', NULL, NULL, 0, 0, '1', 0); + +-- 角色删除已有用户按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592114893302823614', '1597419994965786625', '删除指定角色的用户关系', NULL, NULL, 0, NULL, NULL, 2, 'system:user:deleteRole', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:19:05', NULL, NULL, 0, 0, '1', 0); + +-- 角色批量删除已有用户按钮权限升级sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592114955650691174', '1597419994965786625', '批量删除指定角色的用户关系', NULL, NULL, 0, NULL, NULL, 2, 'system:user:deleteRoleBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:19:20', NULL, NULL, 0, 0, '1', 0); + +-- 租户部门设置成非叶子节点 +UPDATE `sys_permission` SET `is_leaf` = 0 WHERE `id` = '1961009998209257473'; + +-- 添加部门按钮权限sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592115712422330529', '1961009998209257473', '部门添加', NULL, NULL, 0, NULL, NULL, 2, 'system:depart:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:22:21', 'admin', '2022-11-14 19:30:49', 0, 0, '1', 0); + +-- 编辑部门按钮权限sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592117222764277032', '1961009998209257473', '部门编辑', NULL, NULL, 0, NULL, NULL, 2, 'system:depart:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:28:21', 'admin', '2022-11-14 19:30:55', 0, 0, '1', 0); + +-- 删除、批量删除部门按钮权限sql +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592117276539449346', '1961009998209257473', '部门删除', NULL, NULL, 0, NULL, NULL, 2, 'system:depart:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:28:33', 'admin', '2022-11-14 19:31:06', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1592117377299214338', '1961009998209257473', '部门批量删除', NULL, NULL, 0, NULL, NULL, 2, 'system:depart:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-11-14 19:28:58', 'admin', '2022-11-14 19:31:12', 0, 0, '1', 0); + +-- 租户管理员角色升级sql +INSERT INTO `sys_role` (`id`, `role_name`, `role_code`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`) VALUES ('1962488045068464130', '租户管理员', 'zuhuadmin', NULL, 'admin', '2025-09-01 20:09:46', NULL, NULL, 0); + +-- 租户管理员菜单权限 +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962489414454194178', '1962488045068464130', '1609123240547344385', NULL, '2025-09-01 20:15:12', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251539722241', '1962488045068464130', '1674708136602542082', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251548110850', '1962488045068464130', '1663816667704500225', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251552305154', '1962488045068464130', '119213522910765570', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251552305155', '1962488045068464130', '1592114574275211345', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251560693762', '1962488045068464130', '1960994076329316353', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251560693763', '1962488045068464130', '1214462306546319322', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251560693764', '1962488045068464130', '1597419994965786625', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251560693765', '1962488045068464130', '1592102143467200514', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251569082370', '1962488045068464130', '1592114893302823614', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251569082371', '1962488045068464130', '1592120323667750934', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251569082372', '1962488045068464130', '1592120372296522490', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251569082373', '1962488045068464130', '1592120427223412865', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251573276674', '1962488045068464130', '1961009998209257473', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251573276675', '1962488045068464130', '1592115712422330529', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251573276676', '1962488045068464130', '1592117222764277032', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251573276677', '1962488045068464130', '1592117276539449346', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251581665281', '1962488045068464130', '1592117377299214338', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251581665282', '1962488045068464130', '1961253156897710081', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251585859586', '1962488045068464130', '1600105607009162230', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251585859587', '1962488045068464130', '1600108123037913486', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962765251585859588', '1962488045068464130', '1609123240547344376', NULL, '2025-09-02 14:31:17', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1962766200899461121', '1962488045068464130', '1592114955650691174', NULL, '2025-09-02 14:35:03', '192.168.1.6'); + +-- ---author:liusq---date:20250902-----for:修改默认首页的关联类型 +UPDATE sys_role_index SET `relation_type` = 'DEFAULT' WHERE `role_code` = 'DEF_INDEX_ALL'; + +-- ---author:liusq---date:20250902-----for:关联类型字典增加default类型 +INSERT INTO `sys_dict_item`(`id`, `dict_id`, `item_text`, `item_value`, `item_color`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1963079150651305985', '1939572486447292418', '全局默认', 'DEFAULT', NULL, NULL, 3, 1, 'admin', '2025-09-03 11:18:36', NULL, NULL); + + +-- ---author:wangshuai---date:20250903-----for: 租户改造菜单、菜单权限、套餐增加是否自动分配给用户字段、test角色名称描述修改 +-- 租户请离按钮权限 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1600129606082650123', '119213522910765570', '租户请离', NULL, NULL, 0, NULL, NULL, 2, 'system:tenant:leave', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-12-06 22:06:42', NULL, NULL, 0, 0, '1', 0); + +-- 租户职务菜单 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1963086454217281537', '1674708136602542082', '租户职务', '/position/TenantPositionList', 'system/position/TenantPositionList', 1, '', NULL, 1, NULL, '0', 3.50, 0, 'ant-design:user-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-09-03 11:47:38', NULL, NULL, 0, 0, NULL, 0); + +-- 新版我的租户 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1963133393868722178', '1674708136602542082', '我的租户', '/my/MyTenantDetail', 'system/tenant/my/MyTenantDetail', 1, '', NULL, 1, NULL, '0', 3.00, 0, 'ant-design:user-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-09-03 14:54:09', NULL, NULL, 0, 0, NULL, 0); + +-- 租户管理员角色菜单权限 +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1963068885343252482', '1962488045068464130', '1600129606082650123', NULL, '2025-09-03 10:37:49', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1963086686351036418', '1962488045068464130', '1963086454217281537', NULL, '2025-09-03 11:48:33', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1963133491872829442', '1962488045068464130', '1963133393868722178', NULL, '2025-09-03 14:54:32', '192.168.1.6'); + +-- 套餐增加是否自动分配给用户字段 +ALTER TABLE `sys_tenant_pack` +ADD COLUMN `iz_sysn` varchar(1) NULL COMMENT '自动分配给用户(0否 1是)' AFTER `pack_type`; + +-- test角色名称描述修改 +UPDATE `sys_role` SET `role_name` = '系统用户标配角色', `role_code` = 'test', `description` = '系统所有用户拥有的最小权限角色,默认都分配这个角色' WHERE `id` = 'ee8626f80f7c2619917b6236f3a7f02b'; + +-- test角色新增授权,默认授权用户设置 +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1963153837854330881', 'ee8626f80f7c2619917b6236f3a7f02b', '1596141938193747970', NULL, '2025-09-03 16:15:23', '192.168.1.6'); +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1963153837854330882', 'ee8626f80f7c2619917b6236f3a7f02b', '1596335805278990338', NULL, '2025-09-03 16:15:23', '192.168.1.6'); + +-- ---author:wangshuai---date:20250906-----for: 【QQYUN-13637】增加部门岗位用户中间表 + +-- 删除兼职岗位 + +ALTER TABLE `sys_user` +DROP COLUMN `other_dep_post_id`; + +-- 增加用户部门岗位中间表 + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sys_user_dep_post +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user_dep_post`; +CREATE TABLE `sys_user_dep_post` ( + `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键', + `user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户id', + `dep_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门岗位id', + `create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_sudp_user_id`(`user_id`) USING BTREE, + INDEX `idx_sudp_dep_id`(`dep_id`) USING BTREE, + INDEX `idx_sudp_user_dep_id`(`user_id`, `dep_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ---author:wangshuai---date:20250908-----for: 删除旧的我的租户菜单 + +-- 删除旧版我的租户菜单 +delete from sys_permission where id = '1663816667704500225'; + +-- ---author:wangshuai---date:20250908-----for: 【JHHB-177】【用户管理】添加职务字段 获取字典 + +-- 用户增加职务 +ALTER TABLE `sys_user` +ADD COLUMN `position_type` varchar(32) NULL COMMENT '职务(字典)' AFTER `main_dep_post_id`; + +-- 职务字典 +INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`, `low_app_id`) VALUES ('1964944899916697602', '用户职务', 'user_position', '用户职务', 0, 'admin', '2025-09-08 14:52:26', NULL, NULL, 0, 0, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1964944982842281986', '1964944899916697602', '董事长', '0', NULL, 0, 1, 'admin', '2025-09-08 14:52:45', 'admin', '2025-09-08 14:53:54', NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1964945020519714817', '1964944899916697602', '总经理', '1', NULL, 1, 1, 'admin', '2025-09-08 14:52:54', NULL, NULL, NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1964945061850386434', '1964944899916697602', '副总经理', '2', NULL, 2, 1, 'admin', '2025-09-08 14:53:04', 'admin', '2025-09-08 14:53:46', NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1964945100802887681', '1964944899916697602', '部长', '3', NULL, 3, 1, 'admin', '2025-09-08 14:53:14', 'admin', '2025-09-08 14:53:43', NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1964945142854979586', '1964944899916697602', '副部长', '4', NULL, 4, 1, 'admin', '2025-09-08 14:53:24', 'admin', '2025-09-08 14:53:40', NULL); +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('1964945196395270146', '1964944899916697602', '职员', '5', NULL, 5, 1, 'admin', '2025-09-08 14:53:36', NULL, NULL, NULL); + +-- ---author:chenrui---date:20250909-----for: aiflow-kotlin执行报错 +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'160650416019521536\'),\n WHEN(\n code_160652991133433856.tag(\'code_160652991133433856\'),\n code_166081977564753920.tag(\'code_166081977564753920\'),\n code_167835393352683520.tag(\'code_167835393352683520\')\n ).tag(\"code_160652991133433856\"),\n end.tag(\'160656278891560960\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":418,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"160650416019521536\",\"type\":\"llm\",\"x\":693,\"y\":462,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":null,\"topP\":0.9,\"presencePenalty\":0.1,\"frequencyPenalty\":0.1}},\"history\":4,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位严厉的长辈,面对用户的问题,要以一种带着隐隐批评,暗示问题简单、用户还有很多需要学习的态度来回复。通过大模型模拟李白来对话,回答用户提出的各种问题。\\n\\n\\n## 技能\\n### 技能 1: 回答问题\\n1. 当用户提出问题时,先简要评价问题较为简单,然后给出回答。\\n2. 回答完问题后,适当提及用户还需要加强学习、增长见识等内容。\\n\\n\\n## 限制:\\n- 回复内容必须逻辑清晰、语言通顺,符合严厉长辈的角色设定。 \\n\\n\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"code_160652991133433856\",\"type\":\"code\",\"x\":1135,\"y\":179,\"properties\":{\"text\":\"js\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main(params) {\\n if(params.llmRes){\\n let resLength = params.llmRes.length\\n params.llmRes = params.llmRes + \'\\\\n字数:\'+resLength\\n }\\n return {\\n result: params.llmRes,\\n }\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":158,\"width\":332}},{\"id\":\"160656278891560960\",\"type\":\"end\",\"x\":1653,\"y\":449,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"js:{{res}}\\ngroovy:{{res1}}\\nkotlin:{{res2}}\\npython:{{res3}}\\naviator:{{res4}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"code_160652991133433856\"},{\"field\":\"result\",\"name\":\"res1\",\"nodeId\":\"code_166081977564753920\"},{\"field\":\"result\",\"name\":\"res2\",\"nodeId\":\"code_166090618376253440\"},{\"field\":\"result\",\"name\":\"res3\",\"nodeId\":\"code_167828303175372800\"},{\"field\":\"result\",\"name\":\"res4\",\"nodeId\":\"code_167835393352683520\"}],\"height\":136,\"width\":332}},{\"id\":\"code_166081977564753920\",\"type\":\"code\",\"x\":1140,\"y\":413,\"properties\":{\"text\":\"groovy\",\"options\":{\"codeType\":\"groovy\",\"code\":\"def main(params) {\\n if (params.llmRes) {\\n def resLength = params.llmRes.length()\\n params.llmRes += \\\"\\\\n字数:\\\" + resLength\\n }\\n return [result: params.llmRes]\\n}\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\",\"required\":false}],\"height\":158,\"width\":332}},{\"id\":\"code_167835393352683520\",\"type\":\"code\",\"x\":1141,\"y\":667,\"properties\":{\"text\":\"aviator\",\"options\":{\"codeType\":\"aviator\",\"code\":\"let llmRes = params.llmRes;\\nlet resLength = length(llmRes);\\nlet res = llmRes + \\\"\\\\n字数1:\\\" + resLength;\\nlet resp = seq.map(\\\"result\\\",res);\"},\"inputParams\":[{\"field\":\"text\",\"name\":\"llmRes\",\"nodeId\":\"160650416019521536\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}}],\"edges\":[{\"id\":\"160650416019521537\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"160650416019521536\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"160650416019521536_input\",\"pointsList\":[{\"x\":466,\"y\":403},{\"x\":566,\"y\":403},{\"x\":427,\"y\":403},{\"x\":527,\"y\":403}]},{\"id\":\"160652991137628160\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_160652991133433856\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_160652991133433856_input\",\"pointsList\":[{\"x\":859,\"y\":403},{\"x\":959,\"y\":403},{\"x\":869,\"y\":131},{\"x\":969,\"y\":131}]},{\"id\":\"160656278899949568\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_160652991133433856\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_160652991133433856_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1301,\"y\":131},{\"x\":1401,\"y\":131},{\"x\":1387,\"y\":412},{\"x\":1487,\"y\":412}]},{\"id\":\"166082001409372160\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_166081977564753920\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_166081977564753920_input\",\"pointsList\":[{\"x\":859,\"y\":403},{\"x\":959,\"y\":403},{\"x\":874,\"y\":365},{\"x\":974,\"y\":365}]},{\"id\":\"166082017557442560\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_166081977564753920\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_166081977564753920_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1306,\"y\":365},{\"x\":1406,\"y\":365},{\"x\":1387,\"y\":412},{\"x\":1487,\"y\":412}]},{\"id\":\"167835393356877824\",\"type\":\"base-edge\",\"sourceNodeId\":\"160650416019521536\",\"targetNodeId\":\"code_167835393352683520\",\"sourceAnchorId\":\"160650416019521536_output\",\"targetAnchorId\":\"code_167835393352683520_input\",\"pointsList\":[{\"x\":859,\"y\":403},{\"x\":959,\"y\":403},{\"x\":875,\"y\":619},{\"x\":975,\"y\":619}]},{\"id\":\"167836988980817920\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167835393352683520\",\"targetNodeId\":\"160656278891560960\",\"sourceAnchorId\":\"code_167835393352683520_output\",\"targetAnchorId\":\"160656278891560960_input\",\"pointsList\":[{\"x\":1307,\"y\":619},{\"x\":1407,\"y\":619},{\"x\":1387,\"y\":412},{\"x\":1487,\"y\":412}]}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"}]}' WHERE `id` = '1897552224058400770'; + +-- ---author:wangshuai---date:20250908-----for: 修改职务表的表述和菜单名称 +-- 修改字段名称 +ALTER TABLE `sys_position` +MODIFY COLUMN `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '职务级别名称' AFTER `code`, +COMMENT = '职务级别'; + +-- 更新菜单名称为职务级别 +UPDATE `sys_permission` SET `name` = '职务级别' WHERE `id` = '1438469604861403137'; + +-- 删除旧的用户代理表 +drop table if exists sys_user_agent; \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.3_1__upgrade_jimubi.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.3_1__upgrade_jimubi.sql new file mode 100644 index 0000000..61b361a --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.8.3_1__upgrade_jimubi.sql @@ -0,0 +1,3 @@ +-- 升级积木BI到V2.2.0版本 +ALTER TABLE `onl_drag_page` +MODIFY COLUMN `des_json` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '仪表盘主配置JSON' AFTER `cover_url`; \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_0__all_upgrade.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_0__all_upgrade.sql new file mode 100644 index 0000000..968c1fa --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_0__all_upgrade.sql @@ -0,0 +1,85 @@ +-- ---author:wangshuai---date:20250913-----for: 【JHHB-115】新增用户字段上一次修改密码的时间 +-- 新增用户字段上一次修改密码的时间 +ALTER TABLE `sys_user` +ADD COLUMN `last_pwd_update_time` datetime(0) NULL DEFAULT NULL COMMENT '上一次修改密码的时间' AFTER `position_type`; + +-- 更新用户最后一次更新时间为系统时间 +update sys_user set last_pwd_update_time = NOW() where last_pwd_update_time is null; + +-- 5个月没有修改密码提醒事件 +INSERT INTO `sys_quartz_job` (`id`, `create_by`, `create_time`, `del_flag`, `update_by`, `update_time`, `job_class_name`, `cron_expression`, `parameter`, `description`, `status`) VALUES ('1966781755167879169', 'admin', '2025-09-13 16:31:26', 0, NULL, NULL, 'org.ghb.modules.system.job.UserUpadtePwdJob', '0 0 0 * * ? *', NULL, '5个月未修改密码提醒', 0); + +-- ---author:wangshuai---date:20250929-----for: 【QQYUN-13676】支持拖拽调整组织机构 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1972617196420993025', '45c966826eeff4c99b8f8ebfe74511fc', '部门管理拖拽修改上下级', NULL, NULL, 0, NULL, NULL, 2, 'system:depart:updateChange', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-29 18:59:24', NULL, NULL, 0, 0, '1', 0); + +-- ---author:liusq---date:20250930-----for: [JHHB-747]【用户管理】密码重置为系统默认密码 加权限 +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1972645086223814657', '3f915b2769fc80648e92d04e84ca059d', '重置系统密码', NULL, NULL, 0, NULL, NULL, 2, 'system:user:resetPassword', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-29 20:50:13', 'admin', '2025-09-30 11:58:29', 0, 0, '1', 0); + + +-- ---author:wangshuai---date:20251011-----for: 【JHHB-765】需要能设置排序,需要能设置哪些人的联系方式隐藏不可见 +ALTER TABLE `sys_user` +ADD COLUMN `sort` int(6) NULL COMMENT '排序' AFTER `last_pwd_update_time`, +ADD COLUMN `iz_hide_contact` varchar(1) NULL COMMENT '是否隐藏联系方式(0 否 1是)' AFTER `sort`; + +update sys_user set sort = 1000 where sort is null; + + +DROP TABLE IF EXISTS `airag_mcp`; +CREATE TABLE `airag_mcp` ( + `id` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称', + `descr` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述', + `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'mcp类型(sse:sse类型;stdio:标准类型)', + `endpoint` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '服务端点(SSE类型为URL,stdio类型为命令)', + `headers` text COLLATE utf8mb4_unicode_ci COMMENT '请求头(sse类型)、环境变量(stdio类型)', + `tools` text COLLATE utf8mb4_unicode_ci COMMENT '工具列表', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '状态(enable=启用、disable=禁用)', + `synced` int DEFAULT NULL COMMENT '是否同步', + `metadata` text COLLATE utf8mb4_unicode_ci COMMENT '元数据', + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '租户id', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI MCP'; + +ALTER TABLE `airag_app` +ADD COLUMN `plugins` text NULL COMMENT '插件' AFTER `quick_command`; + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1980223355087781889', '1892553163993931777', 'MCP配置', '/super/airag/aimcp/AiragMcpList', 'super/airag/aimcp/AiragMcpList', 1, '', NULL, 1, NULL, '0', 5.00, 0, 'ant-design:tool-twotone', 1, 0, 0, 0, NULL, 'admin', '2025-10-20 18:43:33', 'admin', '2025-10-21 19:00:31', 0, 0, NULL, 0); + +UPDATE sys_permission set name = 'AI应用平台', sort_no = 0.1 where id = '1892553163993931777'; +UPDATE sys_permission set name = '零代码应用', sort_no = 0.2 where id = '1958577215150039042'; + +-- mysql + +ALTER TABLE `airag_mcp` +ADD COLUMN `category` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'mcp' COMMENT '类型(plugin=插件,mcp=MCP)' AFTER `descr`; + +-- 更新现有数据,将所有现有MCP数据的category字段设置为'mcp' +UPDATE `airag_mcp` SET `category` = 'mcp' WHERE `category` IS NULL OR `category` = ''; + +-- ---author:lvdandan---date:20251119-----for:【QQYUN-13685】大屏高级效果模板 包含导航切换、弹框、高德地图、定时轮播(默认60秒) +INSERT INTO `onl_drag_page` (`id`, `name`, `path`, `background_color`, `background_image`, `design_type`, `theme`, `style`, `cover_url`, `des_json`, `template`, `protection_code`, `type`, `iz_template`, `create_by`, `create_time`, `update_by`, `update_time`, `low_app_id`, `tenant_id`, `update_count`, `visits_num`, `del_flag`) VALUES ('1151069555267260416', '集团综合数据大屏', '/drag/page/view/1151069555267260416', '#1E0047', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/背景_1756435878126.jpg', 100, 'dark', 'bigScreen', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/封面_1763554989082.png', '{\"width\":1920,\"height\":1080,\"waterMark\":{\"show\":false,\"content\":\"\",\"fontSize\":12,\"color\":\"#ffffff\",\"angle\":45},\"sysDefColor\":[{\"color\":\"#1e90ff\",\"color1\":\"#1e90ff\"},{\"color\":\"#90ee90\",\"color1\":\"#90ee90\"},{\"color\":\"#00ced1\",\"color1\":\"#00ced1\"},{\"color\":\"#e2bd84\",\"color1\":\"#e2bd84\"},{\"color\":\"#7a90e0\",\"color1\":\"#7a90e0\"},{\"color\":\"#3ba272\",\"color1\":\"#3ba272\"},{\"color\":\"#2be7ff\",\"color1\":\"#2be7ff\"},{\"color\":\"#0a8ada\",\"color1\":\"#0a8ada\"},{\"color\":\"#ffd700\",\"color1\":\"#ffd700\"}],\"layoutMode\":\"fullScreen\"}', '[{\"component\":\"JText\",\"visible\":true,\"w\":496,\"x\":705,\"h\":60,\"i\":\"8eca7087-2a7b-40d0-9732-7784fb4dbdfc\",\"y\":5,\"orderNum\":70,\"componentName\":\"文本\",\"pageCompId\":\"1151112776819200000\"},{\"component\":\"JTabToggle\",\"visible\":true,\"w\":680,\"x\":597,\"h\":70,\"i\":\"f695ef5a-9797-4a56-8a1f-3a4db443da22\",\"y\":62,\"orderNum\":70,\"componentName\":\"导航切换\",\"pageCompId\":\"1151112776861143040\",\"key\":\"299a2a16-346c-44ca-bd9c-6b62aaf8cf98\"},{\"visible\":false,\"h\":991.8710433763201,\"i\":\"es-drager-1762494714798-7\",\"props\":{\"elements\":[{\"component\":\"JStatsSummary\",\"visible\":true,\"w\":698,\"x\":602.672919109027,\"h\":85,\"i\":\"c1789d8c-8d56-410c-9a0e-90c65a2e20e1\",\"y\":48.61195779601541,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"4.9010360893832265%\",\"left\":\"31.130365064812537%\",\"width\":\"36.05437398342474%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.56966241404334%\"},\"componentName\":\"统计概览(背景模式)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": \\\"1\\\",\\n \\\"name\\\": \\\"项目总数\\\",\\n \\\"value\\\": 600,\\n \\\"suffix\\\": \\\"个\\\"\\n },\\n {\\n \\\"id\\\": \\\"2\\\",\\n \\\"name\\\": \\\"合同总数\\\",\\n \\\"value\\\": 900,\\n \\\"suffix\\\": \\\"个\\\"\\n },\\n {\\n \\\"id\\\": \\\"3\\\",\\n \\\"name\\\": \\\"收票总数\\\",\\n \\\"value\\\": 790,\\n \\\"suffix\\\": \\\"个\\\"\\n },\\n {\\n \\\"id\\\": \\\"4\\\",\\n \\\"name\\\": \\\"总金额\\\",\\n \\\"value\\\": 17790,\\n \\\"suffix\\\": \\\"万元\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":697.9999999999999,\"height\":85},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":713,\"dataType\":1,\"h\":129,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"layout\":{\"padding\":{\"top\":9,\"left\":20,\"bottom\":0,\"right\":20},\"borderColor\":\"#0f66ff59\",\"borderRadius\":0,\"shadow\":\"none\",\"justify\":\"space-between\",\"borderWidth\":0,\"gap\":16,\"fill\":{\"image\":{\"size\":\"contain\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"drag/lib/img/bg01.png\"},\"color\":\"#0b2b63\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"image\"}},\"highlight\":[],\"fieldMap\":{\"compareValue\":\"compareValue\",\"unit\":\"suffix\",\"negativeValue\":\"0\",\"compareState\":\"compareState\",\"label\":\"name\",\"value\":\"value\",\"positiveValue\":\"1\",\"compareLabel\":\"compareLabel\"},\"card\":{\"padding\":{\"horizontal\":3,\"vertical\":15},\"borderColor\":\"#0F66FF59\",\"borderRadius\":0,\"shadow\":\"none\",\"borderWidth\":0,\"blur\":24,\"minWidth\":100,\"fill\":{\"image\":{\"size\":\"cover\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"\"},\"color\":\"#0B2B6300\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"none\"}},\"sections\":{\"middle\":{\"compare\":{\"valueStyle\":{\"positiveGradient\":{\"endColor\":\"#15f0c5\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#15f0c5\"},\"positiveColor\":\"#15F0C5\",\"fontSize\":14,\"negativeColor\":\"#D0021B\",\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"negativeGradient\":{\"endColor\":\"#D0021B\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#D0021B\"},\"fontColor\":\"#FFFFFF\"},\"alignItems\":\"center\",\"labelStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"fontColor\":\"#9ED3FF\"},\"label\":\"同比\"},\"paddingBottom\":10,\"show\":false,\"type\":\"compare\",\"align\":\"center\"},\"top\":{\"minHeight\":32,\"paddingBottom\":2,\"show\":true,\"paddingTop\":9,\"type\":\"value\",\"align\":\"center\",\"value\":{\"unit\":{\"fontSize\":18,\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"fontWeight\":500,\"fontColor\":\"#9ED3FF\"},\"unitGap\":6,\"fontSize\":20,\"fontGradient\":{\"endColor\":\"#D8F1FF\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#73E0FF\"},\"fontWeight\":600,\"fontColor\":\"#D8F1FF\"}},\"bottom\":{\"minHeight\":0,\"paddingBottom\":10,\"show\":true,\"label\":{\"fontSize\":16,\"fontColor\":\"#CFEAFF\"},\"paddingTop\":14,\"type\":\"label\",\"align\":\"center\"}}}}},{\"component\":\"JBreakRing\",\"visible\":true,\"w\":465.9999959429075,\"x\":1469.9648340688161,\"h\":188.99999576947263,\"i\":\"8400df78-833b-46c3-818b-94d611837818\",\"y\":34.6846393287222,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"3.4968899999999996%\",\"left\":\"75.929315%\",\"width\":\"24.070685%\",\"position\":\"absolute\",\"config\":{},\"height\":\"19.054895999999992%\"},\"componentName\":\"多色环形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":400,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"value\\\": 500,\\n \\\"name\\\": \\\"施工费\\\"\\n },\\n {\\n \\\"value\\\": 700,\\n \\\"name\\\": \\\"设计费\\\"\\n },\\n {\\n \\\"value\\\": 1000,\\n \\\"name\\\": \\\"土地款\\\"\\n }\\n]\",\"size\":{\"width\":465.9999959429075,\"height\":188.99999576947263},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":550,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"customColor\":[{\"color1\":\"#00ccdf\",\"color\":\"#2A6FFEC0\"},{\"color1\":\"#00c039\",\"color\":\"#28F2E699\"},{\"color1\":\"#ff7701\",\"color\":\"#FFA80099\"}],\"grid\":{\"top\":50,\"left\":50,\"show\":false},\"series\":[{\"data\":[],\"name\":\"Access From\",\"avoidLabelOverlap\":false,\"emphasis\":{\"label\":{\"show\":true,\"fontSize\":14,\"fontWeight\":\"bold\"}},\"itemStyle\":{\"shadowBlur\":20,\"borderWidth\":4},\"label\":{\"color\":\"#EEF1FA\",\"show\":true,\"fontSize\":16,\"position\":\"center\"},\"labelLine\":{\"length2\":38,\"show\":false},\"type\":\"pie\",\"radius\":[\"40%\",\"70%\"]}],\"legend\":{\"r\":1,\"orient\":\"vertical\",\"t\":22,\"show\":false},\"tooltip\":{\"trigger\":\"item\"},\"outRadius\":43,\"title\":{\"subtext\":\"\",\"top\":41,\"textAlign\":\"\",\"left\":\"center\",\"show\":true,\"customTop\":true,\"text\":\"成本分类\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontSize\":16,\"fontWeight\":\"bold\"},\"subtextStyle\":{\"color\":\"#EEF1FA\",\"fontSize\":24}},\"innerRadius\":46,\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JText\",\"visible\":true,\"w\":80.00001018811254,\"x\":1170.8546088584762,\"h\":39.00000243327086,\"i\":\"es-drager-1763379841846-17\",\"y\":607.5369335982422,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.251604999999984%\",\"left\":\"60.47912600000001%\",\"width\":\"4.132306999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.931963%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":80.00001018811254,\"height\":39.00000243327086},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C3D3E6\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":80.00001018811254,\"x\":1070.83116610898,\"h\":39.00000243327086,\"i\":\"es-drager-1763379835852-16\",\"y\":407.4900435141388,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"41.08296599999999%\",\"left\":\"55.31253199999998%\",\"width\":\"4.132306999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.931963%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":80.00001018811254,\"height\":39.00000243327086},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C3D3E6\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":80.00001018811254,\"x\":955.6142713735522,\"h\":39.00000243327086,\"i\":\"es-drager-1763379828126-15\",\"y\":504.981246741806,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.911986%\",\"left\":\"49.361138%\",\"width\":\"4.132306999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.931963%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":80.00001018811254,\"height\":39.00000243327086},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C3D3E6\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":80.00001018811254,\"x\":799.8815821142322,\"h\":39.00000243327086,\"i\":\"es-drager-1763379820679-14\",\"y\":618.9320044052293,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"62.400451000000004%\",\"left\":\"41.31694799999999%\",\"width\":\"4.132306999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.931963%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":80.00001018811254,\"height\":39.00000243327086},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C3D3E6\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":809.3657576585699,\"h\":32.000001487878116,\"i\":\"es-drager-1763379792283-13\",\"y\":543.8839267883006,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"54.834136999999984%\",\"left\":\"41.806842%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":965.0984662775381,\"h\":32.000001487878116,\"i\":\"es-drager-1763379785352-12\",\"y\":428.6670457380076,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.218022%\",\"left\":\"49.85103299999999%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1186.669389396952,\"h\":32.000001487878116,\"i\":\"es-drager-1763379778444-11\",\"y\":531.2227325944438,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"53.55764099999999%\",\"left\":\"61.29602%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1087.9120482866356,\"h\":32.000001487878116,\"i\":\"es-drager-1763379772822-10\",\"y\":314.71628807458416,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"31.729556999999993%\",\"left\":\"56.194824999999994%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":1163.1934345963189,\"h\":48.00000223181718,\"i\":\"es-drager-1763379765062-9\",\"y\":502.3681228031894,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.648532%\",\"left\":\"60.083396999999984%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"994,150\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":792.2204078520751,\"h\":48.00000223181718,\"i\":\"es-drager-1763379758530-8\",\"y\":507.432596513248,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"51.15913%\",\"left\":\"40.921219%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"994,150\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":951.7514601078782,\"h\":48.00000223181718,\"i\":\"es-drager-1763379754034-7\",\"y\":394.747952317984,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"39.79831399999999%\",\"left\":\"49.161609%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"994,150\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":1072.0328194789686,\"h\":48.00000223181718,\"i\":\"es-drager-1763379749488-6\",\"y\":284.5955548964599,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"28.692797999999996%\",\"left\":\"55.374601999999996%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"994,150\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":163.99999475010551,\"x\":1036.7666839863778,\"h\":43.00000013957801,\"i\":\"es-drager-1763379412964-5\",\"y\":754.5967136818532,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.07810699999999%\",\"left\":\"53.55297100000001%\",\"width\":\"8.471227999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.335241%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同即将到期提醒\\\"\\n}\",\"size\":{\"width\":163.99999475010551,\"height\":43.00000013957801},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":163.99999475010551,\"x\":716.5673937336227,\"h\":43.00000013957801,\"i\":\"es-drager-1763379399584-4\",\"y\":754.7256767549129,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.09110899999999%\",\"left\":\"37.013451%\",\"width\":\"8.471227999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.335241%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目审批通过提醒\\\"\\n}\",\"size\":{\"width\":163.99999475010551,\"height\":43.00000013957801},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":278.00000007233297,\"x\":998.8393905981011,\"h\":164.99999961291928,\"i\":\"es-drager-1763379229297-3\",\"y\":801.8241475579845,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.83955599999999%\",\"left\":\"51.59388100000001%\",\"width\":\"14.359765%\",\"position\":\"absolute\",\"config\":{},\"height\":\"16.635226999999997%\"},\"componentName\":\"滚动列表(多行+序号)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"JS-CG-12354653合同剩余3天\\\",\\n \\\"value\\\": 131.73\\n },\\n {\\n \\\"title\\\": \\\"JS-CG-12354653合同剩余3天\\\",\\n \\\"value\\\": 11.04\\n },\\n {\\n \\\"title\\\": \\\"JS-CG-12354653合同剩余3天\\\",\\n \\\"value\\\": 36.81\\n },\\n {\\n \\\"title\\\": \\\"JS-CG-12354653合同剩余3天\\\",\\n \\\"value\\\": 24.64\\n },\\n {\\n \\\"title\\\": \\\"JS-CG-12354653合同剩余3天\\\",\\n \\\"value\\\": 24.64\\n }\\n]\",\"size\":{\"width\":278.00000007233297,\"height\":164.99999961291928},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"showIndex\":true,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"marginRight\":18,\"compose\":{\"contentStyle\":{\"fontSize\":13,\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontStyle\":\"italic\",\"fontColor\":\"#41AAE0\",\"marginLeft\":10},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":true},\"name\":\"标题\",\"width\":250,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"title\",\"marginLeft\":0}],\"itemsPerRow\":1,\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"indexFieldStyle\":{\"width\":28,\"textStyle\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#F54100\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#D4BA28\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\"},\"marginLeft\":15},\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"\",\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"height\":33,\"marginLeft\":37}}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":278.00000007233297,\"x\":667.2450113558968,\"h\":150.99999772213383,\"i\":\"782fa728-19d6-45f1-afc4-df366e048e7f\",\"y\":818.4126650668006,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"82.512003%\",\"left\":\"34.46576099999999%\",\"width\":\"14.359765%\",\"position\":\"absolute\",\"config\":{},\"height\":\"15.223752999999999%\"},\"componentName\":\"滚动列表(多行+序号)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"XXXXX项目通过竣工结算审批\\\",\\n \\\"value\\\": 131.73\\n },\\n {\\n \\\"title\\\": \\\"XXXXXXX项目通过竣工结算审批\\\",\\n \\\"value\\\": 11.04\\n },\\n {\\n \\\"title\\\": \\\"XXXXXXX项目通过竣工结算审批\\\",\\n \\\"value\\\": 36.81\\n },\\n {\\n \\\"title\\\": \\\"XXXXX项目通过竣T结算审批\\\",\\n \\\"value\\\": 24.64\\n },\\n {\\n \\\"title\\\": \\\"XX项目通过竣工结算审批\\\",\\n \\\"value\\\": 24.64\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":278.00000007233297,\"height\":150.99999772213383},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"showIndex\":true,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"marginRight\":18,\"compose\":{\"contentStyle\":{\"fontSize\":13,\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontStyle\":\"italic\",\"fontColor\":\"#41AAE0\",\"marginLeft\":10},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":true},\"name\":\"标题\",\"width\":250,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"title\",\"marginLeft\":0}],\"itemsPerRow\":1,\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"indexFieldStyle\":{\"width\":28,\"textStyle\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#F54100\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#D4BA28\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\"},\"marginLeft\":15},\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"\",\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"height\":33,\"marginLeft\":37}}}},{\"component\":\"JImg\",\"visible\":true,\"w\":286.0000068990387,\"x\":990.8558007531537,\"h\":184.000003595944,\"i\":\"es-drager-1763378891880-2\",\"y\":797.6389179343269,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.417603%\",\"left\":\"51.181498000000005%\",\"width\":\"14.772995999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"18.550799000000005%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":286.0000068990387,\"height\":184.000003595944},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_14_1763552764377.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":297.9999880996249,\"x\":657.995300512122,\"h\":179.00000150370474,\"i\":\"es-drager-1763378858332-1\",\"y\":801.5662412492858,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.81355399999998%\",\"left\":\"33.987978%\",\"width\":\"15.392840999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"18.04670099999999%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":297.9999880996249,\"height\":179.00000150370474},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_14_1763552764377.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":87.99999765517,\"x\":1164.5720932307152,\"h\":120.00000062018773,\"i\":\"es-drager-1763118788337-7\",\"y\":499.0926174630956,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.318297%\",\"left\":\"60.154609999999984%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.098347%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":87.99999765517,\"height\":120.00000062018773},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":87.99999765517,\"x\":793.599047126823,\"h\":120.00000062018773,\"i\":\"es-drager-1763118782377-6\",\"y\":509.2215748019232,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"51.339493999999995%\",\"left\":\"40.992430999999996%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.098347%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":87.99999765517,\"height\":120.00000062018773},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":87.99999765517,\"x\":948.1946087239393,\"h\":120.00000062018773,\"i\":\"es-drager-1763118776146-5\",\"y\":394.13364690597933,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"39.73638%\",\"left\":\"48.977883999999996%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.098347%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":87.99999765517,\"height\":120.00000062018773},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":87.99999765517,\"x\":1067.2098470962017,\"h\":120.00000062018773,\"i\":\"es-drager-1763118767012-4\",\"y\":290.3118366626732,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"29.26911099999999%\",\"left\":\"55.12547699999999%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.098347%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":87.99999765517,\"height\":120.00000062018773},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":923.445466652849,\"h\":32.000001487878116,\"i\":\"es-drager-1763118663383-3\",\"y\":222.41851171132498,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"22.424135999999997%\",\"left\":\"47.699495999999996%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":902.5017538498712,\"h\":48.00000223181718,\"i\":\"es-drager-1763118651031-2\",\"y\":192.2977785332007,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"19.387376999999997%\",\"left\":\"46.617672999999996%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"994,150\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":80.00001018811254,\"x\":911.429049110856,\"h\":39.00000243327086,\"i\":\"es-drager-1763118614581-1\",\"y\":305.06330981205195,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"30.756348%\",\"left\":\"47.078802%\",\"width\":\"4.132306999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.931963%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":80.00001018811254,\"height\":39.00000243327086},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C3D3E6\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":87.99999765517,\"x\":904.0093671015944,\"h\":120.00000062018773,\"i\":\"5bf358de-c9b6-45d3-8965-ca7a70dafc6b\",\"y\":194.2157099762253,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"19.580741999999994%\",\"left\":\"46.69554699999999%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.098347%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":87.99999765517,\"height\":120.00000062018773},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JMultipleLine\",\"visible\":true,\"w\":512.0000109969051,\"x\":1344.6189907024152,\"h\":168.0000028520049,\"i\":\"32542357-d15f-4650-a311-f9249a18933e\",\"y\":257.5216908642207,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"25.963222999999996%\",\"left\":\"69.45472199999999%\",\"width\":\"26.446761999999996%\",\"position\":\"absolute\",\"config\":{},\"height\":\"16.937686000000003%\"},\"componentName\":\"对比折线图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 199,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 799,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 388,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 459,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 800,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 420,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"7月\\\",\\n \\\"value\\\": 580,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"8月\\\",\\n \\\"value\\\": 420,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"9月\\\",\\n \\\"value\\\": 700,\\n \\\"type\\\": \\\"目标成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 20,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 210,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 220,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 580,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 500,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 800,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"7月\\\",\\n \\\"value\\\": 810,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"8月\\\",\\n \\\"value\\\": 850,\\n \\\"type\\\": \\\"实际成本\\\"\\n },\\n {\\n \\\"name\\\": \\\"9月\\\",\\n \\\"value\\\": 990,\\n \\\"type\\\": \\\"实际成本\\\"\\n }\\n]\",\"size\":{\"width\":512.0000109969051,\"height\":168.0000028520049},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"name\":\"单位(万元)\",\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#e2bd84\",\"color\":\"#44F0FFB3\"},{\"color1\":\"#3ba272\",\"color\":\"#AB315CB3\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":35,\"left\":20,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"symbol\":\"circle\",\"symbolSize\":6,\"lineType\":\"area\",\"label\":{\"position\":\"top\"}}],\"legend\":{\"t\":1},\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"fontWeight\":\"normal\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JPie\",\"visible\":true,\"w\":450.00000164914417,\"x\":1373.739734956155,\"h\":299.99999659111404,\"i\":\"06345dbb-ae74-47bd-89e0-cb97aa16d150\",\"y\":503.14888401369353,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.727248%\",\"left\":\"70.95891999999999%\",\"width\":\"23.244223999999996%\",\"position\":\"absolute\",\"config\":{},\"height\":\"30.245866999999993%\"},\"componentName\":\"饼图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"value\\\": 179,\\n \\\"name\\\": \\\"前期阶段\\\"\\n },\\n {\\n \\\"value\\\": 79,\\n \\\"name\\\": \\\"施工阶段\\\"\\n },\\n {\\n \\\"value\\\": 19,\\n \\\"name\\\": \\\"审计阶段\\\"\\n },\\n {\\n \\\"value\\\": 17,\\n \\\"name\\\": \\\"竣工阶段\\\"\\n }\\n]\",\"size\":{\"width\":450.0000016491442,\"height\":299.99999659111404},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"customColor\":[{\"color1\":\"#326CF102\",\"color\":\"#326CF1\"},{\"color1\":\"#35B9FD00\",\"color\":\"#35B9FD\"},{\"color1\":\"#FFC94700\",\"color\":\"#FFC947\"},{\"color1\":\"#BD67FF00\",\"color\":\"#BD67FF\"}],\"grid\":{\"top\":49,\"left\":43,\"show\":false},\"legend\":{\"r\":1,\"orient\":\"vertical\",\"t\":19},\"series\":[{\"data\":[],\"name\":\"\",\"emphasis\":{\"itemStyle\":{\"shadowOffsetX\":0,\"shadowBlur\":10,\"shadowColor\":\"rgba(0, 0, 0, 0.5)\"}},\"label\":{\"color\":\"#EEF1FA\",\"show\":true},\"type\":\"pie\",\"radius\":\"50%\"}],\"isRadius\":true,\"tooltip\":{\"trigger\":\"item\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"outRadius\":71,\"innerRadius\":53,\"title\":{\"subtext\":\"\",\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JRingProgress\",\"visible\":true,\"w\":140.99999690293086,\"x\":1645.6975307150763,\"h\":116.99999738110213,\"i\":\"es-drager-1763019222261-6\",\"y\":848.5416208558744,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"85.54959099999999%\",\"left\":\"85.00658199999998%\",\"width\":\"7.28319%\",\"position\":\"absolute\",\"config\":{},\"height\":\"11.795887999999998%\"},\"componentName\":\"基础环形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":200,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"占比\\\",\\n \\\"value\\\": 20\\n }\\n]\",\"size\":{\"width\":140.99999690293086,\"height\":116.99999738110212},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":300,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"color\":\"#1E90FF\",\"valueFontSize\":16,\"body\":{\"gradient\":{\"type\":\"linear\"}},\"valueFontColor\":\"#FFFFFF\",\"valueFontWeight\":\"normal\",\"bgColor\":\"#9AA7B8\",\"lineHeight\":0,\"fontSize\":16,\"radius\":0.9,\"innerRadius\":0.9,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\",\"extraInfo\":{\"endColor\":\"#4B0082\",\"enabledGradient\":true,\"type\":\"linear\",\"direction\":\"to bottom\",\"startColor\":\"#FF69B4\"}}}},{\"component\":\"JRingProgress\",\"visible\":true,\"w\":140.99999690293086,\"x\":1492.6260380707386,\"h\":116.99999738110213,\"i\":\"es-drager-1763019215391-5\",\"y\":855.0011711071522,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"86.20083999999999%\",\"left\":\"77.09985299999998%\",\"width\":\"7.28319%\",\"position\":\"absolute\",\"config\":{},\"height\":\"11.795887999999998%\"},\"componentName\":\"基础环形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":200,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"占比\\\",\\n \\\"value\\\": 20\\n }\\n]\",\"size\":{\"width\":140.99999690293086,\"height\":116.99999738110212},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":300,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"valueFontWeight\":\"normal\",\"color\":\"#4FCFE3\",\"bgColor\":\"#99ABBF\",\"valueFontSize\":16,\"lineHeight\":0,\"fontSize\":16,\"radius\":0.9,\"innerRadius\":0.9,\"valueFontColor\":\"#FFFFFF\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\",\"extraInfo\":{\"endColor\":\"#00DDFF\",\"enabledGradient\":true,\"type\":\"linear\",\"direction\":\"to left\",\"startColor\":\"#00FFCC\"}}}},{\"component\":\"JRingProgress\",\"visible\":true,\"w\":140.99999690293086,\"x\":1328.1594370773037,\"h\":116.99999738110213,\"i\":\"443b689e-bd4a-4d37-9ae6-dd76589a3b09\",\"y\":851.331773938313,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"85.830893%\",\"left\":\"68.60452299999999%\",\"width\":\"7.28319%\",\"position\":\"absolute\",\"config\":{},\"height\":\"11.795887999999998%\"},\"componentName\":\"基础环形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":200,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"占比\\\",\\n \\\"value\\\": 60\\n }\\n]\",\"size\":{\"width\":140.99999690293086,\"height\":116.99999738110212},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":300,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"color\":\"#00AEFF\",\"valueFontSize\":16,\"body\":{\"gradient\":{\"type\":\"linear\"}},\"valueFontColor\":\"#FFFFFF\",\"valueFontWeight\":\"normal\",\"bgColor\":\"#6C849E\",\"lineHeight\":0,\"fontSize\":16,\"radius\":0.9,\"innerRadius\":0.9,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\",\"extraInfo\":{\"endColor\":\"#0066CC\",\"enabledGradient\":true,\"type\":\"linear\",\"direction\":\"to bottom\",\"startColor\":\"#00D4FF\"}}}},{\"component\":\"JText\",\"visible\":true,\"w\":178.00000185692846,\"x\":1592.885087740047,\"h\":35.00000472696371,\"i\":\"es-drager-1763019064412-4\",\"y\":808.8382231534828,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.54671199999999%\",\"left\":\"82.27861699999998%\",\"width\":\"9.194381999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.5286850000000007%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"未审批流程数量\\\"\\n}\",\"size\":{\"width\":178.00000185692846,\"height\":35.00000472696371},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FF5A00\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":1719.0199335729192,\"h\":48.00000223181718,\"i\":\"es-drager-1763019023655-3\",\"y\":470.7151273998363,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.457290999999984%\",\"left\":\"88.79396499999999%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"800\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1603.0937787623684,\"h\":32.000001487878116,\"i\":\"es-drager-1763019013776-2\",\"y\":481.71511613282587,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"48.56630499999999%\",\"left\":\"82.80593499999999%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":105.99999849552168,\"x\":1387.361086701876,\"h\":39.99999690049243,\"i\":\"es-drager-1763019008535-1\",\"y\":502.4490098867766,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.65668699999999%\",\"left\":\"71.662515%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"已归档资料数\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#7B959F\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1789.2133526339042,\"h\":32.000001487878116,\"i\":\"es-drager-1763018083020-24\",\"y\":813.4384516216891,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"82.010505%\",\"left\":\"92.41972399999999%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FF5A00\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":1726.616640206237,\"h\":48.00000223181718,\"i\":\"es-drager-1763018077243-23\",\"y\":802.4384529699892,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.90148999999998%\",\"left\":\"89.18636399999998%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"100\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FF5A00\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":72.00000336140681,\"x\":1464.6588384609615,\"h\":48.00000223181718,\"i\":\"es-drager-1763018057189-21\",\"y\":802.567416043049,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.914492%\",\"left\":\"75.65523999999999%\",\"width\":\"3.719076%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"1000\\\"\\n}\",\"size\":{\"width\":72.00000336140681,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1515.8604812588278,\"h\":32.000001487878116,\"i\":\"es-drager-1763018049207-20\",\"y\":809.7690544528497,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.64055799999998%\",\"left\":\"78.30000099999998%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":178.00000185692846,\"x\":1306.871006376694,\"h\":35.00000472696371,\"i\":\"es-drager-1763018038037-18\",\"y\":811.4994230815718,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.81501299999998%\",\"left\":\"67.50489399999998%\",\"width\":\"9.194381999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.5286850000000007%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"本月采购任务数量\\\"\\n}\",\"size\":{\"width\":178.00000185692846,\"height\":35.00000472696371},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":true,\"startColor\":\"#0085FF\",\"direction\":\"to top\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1776.6810972629544,\"h\":32.000001487878116,\"i\":\"es-drager-1763018002072-16\",\"y\":479.3118423508563,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"48.32400799999999%\",\"left\":\"91.77238499999999%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":1543.1582629496838,\"h\":48.00000223181718,\"i\":\"es-drager-1763017986232-14\",\"y\":473.37631740921506,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.725590999999994%\",\"left\":\"79.710036%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"800\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":105.99999849552168,\"x\":1725.543960813224,\"h\":39.99999690049243,\"i\":\"es-drager-1763017981686-13\",\"y\":502.57796304112605,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.669688%\",\"left\":\"89.13095599999998%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"需补录数量\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#7B959F\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":105.99999849552168,\"x\":1548.2872339334822,\"h\":39.99999690049243,\"i\":\"es-drager-1763017976329-12\",\"y\":503.8440765092855,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.797337%\",\"left\":\"79.97496699999999%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"未归档数据\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#7B959F\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":1.0000032732942556,\"x\":1677.4794909434468,\"h\":46.000003378663585,\"i\":\"es-drager-1763017967343-11\",\"y\":485.29425303671803,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"48.92715199999999%\",\"left\":\"86.64824199999998%\",\"width\":\"0.05165399999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.637699999999999%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1.0000032732942556,\"height\":46.000003378663585},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763552829448.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":2.0000065465885113,\"x\":1520.4806613256508,\"h\":46.99999784588518,\"i\":\"es-drager-1763017957814-10\",\"y\":487.8264898917473,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"49.18245099999999%\",\"left\":\"78.53865099999999%\",\"width\":\"0.10330799999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.738519000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":2.0000065465885113,\"height\":46.999997845885176},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763552829448.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1446.3528583792263,\"h\":32.000001487878116,\"i\":\"es-drager-1763017938561-8\",\"y\":481.97303236023504,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"48.592307999999996%\",\"left\":\"74.70966599999998%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":1377.425560317069,\"h\":48.00000223181718,\"i\":\"es-drager-1763017918861-7\",\"y\":472.23915709540506,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.610943%\",\"left\":\"71.14930699999998%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"1230\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":1402.7479415743262,\"h\":48.00000223181718,\"i\":\"es-drager-1763017900379-6\",\"y\":170.90269362302482,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"17.230333999999996%\",\"left\":\"72.457305%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"2200\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":1470.4091186376554,\"h\":32.000001487878116,\"i\":\"es-drager-1763017893992-5\",\"y\":181.90268235601428,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.339347999999998%\",\"left\":\"75.95226399999999%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":105.99999849552168,\"x\":1334.3129980877375,\"h\":39.99999690049243,\"i\":\"es-drager-1763017881010-4\",\"y\":173.38686440729214,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"17.480787%\",\"left\":\"68.92237799999998%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"总额\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#8699B0\",\"letterSpacing\":0,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":184.99998605069177,\"x\":389.9085438694022,\"h\":43.00000013957801,\"i\":\"es-drager-1763017409384-5\",\"y\":739.5322357873163,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.55931299999999%\",\"left\":\"20.140269999999997%\",\"width\":\"9.555958000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.335241%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"9000万元(不含税)\\\"\\n}\",\"size\":{\"width\":184.99998605069177,\"height\":43.00000013957801},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":152.9999974631653,\"x\":244.3048032410551,\"h\":43.00000013957801,\"i\":\"es-drager-1763017400479-4\",\"y\":740.7983591741861,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.68696299999999%\",\"left\":\"12.619278999999997%\",\"width\":\"7.903035999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.335241%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"9000万元(含税)\\\"\\n}\",\"size\":{\"width\":152.9999974631653,\"height\":43.00000013957801},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":104.00001130858149,\"x\":146.94255710654164,\"h\":43.00000013957801,\"i\":\"es-drager-1763017390547-3\",\"y\":738.3950754735062,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.444665%\",\"left\":\"7.590145999999999%\",\"width\":\"5.371999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.335241%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"金额总计:\\\"\\n}\",\"size\":{\"width\":104.00001130858149,\"height\":43.00000013957801},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":2.0000065465885113,\"x\":356.9167604994608,\"h\":46.99999784588518,\"i\":\"es-drager-1763017303728-1\",\"y\":449.8429073101763,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.35296299999999%\",\"left\":\"18.436117999999997%\",\"width\":\"0.10330799999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.738519000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":2.0000065465885113,\"height\":46.999997845885176},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763552829448.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":2.0000065465885113,\"x\":188.6517965095428,\"h\":46.99999784588518,\"i\":\"400b89c1-02d8-4517-95e5-f56241d063f2\",\"y\":447.4396236094965,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.110665000000004%\",\"left\":\"9.744587999999998%\",\"width\":\"0.10330799999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.738519000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":2.0000065465885113,\"height\":46.999997845885176},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763552829448.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":0,\"x\":197.5146628609848,\"h\":57.99999649758506,\"i\":\"es-drager-1763016996412-1\",\"y\":449.71395415582697,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.339961999999986%\",\"left\":\"10.202389%\",\"width\":\"0%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.847534%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":0,\"height\":57.99999649758506},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#F0111100\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763552829448.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":95.0000012085815,\"x\":373.7924884840798,\"h\":35.999999194185264,\"i\":\"es-drager-1763016703941-11\",\"y\":504.09847159449055,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.82298499999999%\",\"left\":\"19.307814%\",\"width\":\"4.907114%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6295039999999985%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同类型\\\"\\n}\",\"size\":{\"width\":95.0000012085815,\"height\":35.999999194185264},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":105.99999849552168,\"x\":388.65065072110207,\"h\":39.99999690049243,\"i\":\"es-drager-1763016588015-10\",\"y\":460.9249733720053,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"46.470252%\",\"left\":\"20.075294999999997%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"累计未付款\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#7B959F\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":105.99999849552168,\"x\":222.78897411151232,\"h\":39.99999690049243,\"i\":\"es-drager-1763016583354-9\",\"y\":460.9249733720053,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"46.470252%\",\"left\":\"11.507903999999998%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"累计已付款\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#7B959F\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":447.38451316379843,\"h\":32.000001487878116,\"i\":\"es-drager-1763016573207-8\",\"y\":442.7233263917942,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"44.635169999999995%\",\"left\":\"23.109123999999998%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":275.1922509197187,\"h\":32.000001487878116,\"i\":\"es-drager-1763016567041-7\",\"y\":442.7233263917942,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"44.635169999999995%\",\"left\":\"14.214733999999998%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":382.25555873847594,\"h\":48.00000223181718,\"i\":\"es-drager-1763016557285-6\",\"y\":430.45721427193484,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.39850599999999%\",\"left\":\"19.744963999999996%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"1230\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":217.66002248736228,\"h\":48.00000223181718,\"i\":\"es-drager-1763016551841-5\",\"y\":431.7233277400944,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.52615500000001%\",\"left\":\"11.242974%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"800\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":0,\"x\":187.5146301280422,\"h\":57.99999649758506,\"i\":\"es-drager-1763016537923-4\",\"y\":439.7139499713487,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"44.331765999999995%\",\"left\":\"9.685848999999997%\",\"width\":\"0%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.847534%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":0,\"height\":57.99999649758506},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763552829448.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":105.99999849552168,\"x\":53.257889122766706,\"h\":39.99999690049243,\"i\":\"es-drager-1763016385655-3\",\"y\":461.05392652635453,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"46.48325299999999%\",\"left\":\"2.7509739999999994%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同总金额\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#7B959F\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":113.25791128358735,\"h\":32.000001487878116,\"i\":\"es-drager-1763016372630-2\",\"y\":437.787805836085,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"44.137573%\",\"left\":\"5.850204999999999%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":50.66119885592028,\"h\":48.00000223181718,\"i\":\"es-drager-1763016365675-1\",\"y\":426.787807184385,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.02855799999999%\",\"left\":\"2.6168449999999996%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"1230\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JLine\",\"visible\":true,\"w\":552.0000064111372,\"x\":15.451341860937866,\"h\":233.00000029498267,\"i\":\"3929a3a9-910f-45e8-8950-5ba4570f2579\",\"y\":758.8710430813375,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.509043%\",\"left\":\"0.7981209999999999%\",\"width\":\"28.512914999999992%\",\"position\":\"absolute\",\"config\":{},\"height\":\"23.490956999999995%\"},\"componentName\":\"基础折线图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"value\\\": 150,\\n \\\"name\\\": \\\"1日\\\"\\n },\\n {\\n \\\"value\\\": 830,\\n \\\"name\\\": \\\"5日\\\"\\n },\\n {\\n \\\"value\\\": 300,\\n \\\"name\\\": \\\"10日\\\"\\n },\\n {\\n \\\"value\\\": 780,\\n \\\"name\\\": \\\"15日\\\"\\n },\\n {\\n \\\"value\\\": 900,\\n \\\"name\\\": \\\"20\\\"\\n },\\n {\\n \\\"value\\\": 430,\\n \\\"name\\\": \\\"25日\\\"\\n },\\n {\\n \\\"value\\\": 900,\\n \\\"name\\\": \\\"30\\\"\\n }\\n]\",\"size\":{\"width\":552.0000064111372,\"height\":233.00000029498267},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#89B9F063\"},\"show\":true,\"interval\":2},\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#1e90ff\",\"color\":\"#46B4F9\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":30,\"left\":0,\"bottom\":18,\"show\":false,\"right\":1,\"containLabel\":true},\"series\":[{\"symbol\":\"circle\",\"areaStyleOpacity\":0.1,\"data\":[],\"symbolSize\":6,\"lineType\":\"area\",\"itemStyle\":{\"color\":\"#64b5f6\"},\"label\":{\"color\":\"#EEF1FA\",\"position\":\"top\"},\"type\":\"line\"}],\"tooltip\":{\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"subtext\":\"\",\"textAlign\":\"left\",\"left\":10,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JRing\",\"visible\":true,\"w\":472.99999949631894,\"x\":0,\"h\":240.00000124037547,\"i\":\"d8501550-3c20-41f7-92c8-06ba3e833884\",\"y\":468.9918010555223,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.283546%\",\"left\":\"0%\",\"width\":\"24.432261999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"24.196694%\"},\"componentName\":\"饼状环形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"value\\\": 350,\\n \\\"name\\\": \\\"监理\\\"\\n },\\n {\\n \\\"value\\\": 250,\\n \\\"name\\\": \\\"施工\\\"\\n },\\n {\\n \\\"value\\\": 400,\\n \\\"name\\\": \\\"采购\\\"\\n }\\n]\",\"size\":{\"width\":472.99999949631894,\"height\":240.00000124037547},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":480,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"customColor\":[{\"color1\":\"#1E90FF38\",\"color\":\"#2A7DFB\"},{\"color1\":\"#2BE4E3\",\"color\":\"#2BE4E34C\"},{\"color1\":\"#FCA52F4F\",\"color\":\"#FCA52F\"}],\"grid\":{\"top\":50,\"left\":50,\"show\":false},\"series\":[{\"data\":[],\"name\":\"Access From\",\"avoidLabelOverlap\":false,\"emphasis\":{\"label\":{\"show\":true,\"fontSize\":14,\"fontWeight\":\"bold\"}},\"label\":{\"color\":\"#EEF1FA\",\"show\":true,\"position\":\"center\"},\"labelLine\":{\"show\":false},\"type\":\"pie\",\"radius\":[\"40%\",\"70%\"]}],\"legend\":{\"r\":1,\"orient\":\"vertical\",\"t\":31,\"show\":true},\"tooltip\":{\"trigger\":\"item\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"outRadius\":65,\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"innerRadius\":77,\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JText\",\"visible\":true,\"w\":154.00000073645958,\"x\":1341.18520860272,\"h\":35.999999194185264,\"i\":\"es-drager-1762488458149-6\",\"y\":766.0480731705519,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"77.23262799999999%\",\"left\":\"69.27735399999999%\",\"width\":\"7.95469%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6295039999999985%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"采购与审批提醒\\\"\\n}\",\"size\":{\"width\":154.00000073645958,\"height\":35.999999194185264},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":192.00000896375147,\"x\":1348.781934595686,\"h\":35.999999194185264,\"i\":\"es-drager-1762488430111-5\",\"y\":434.1688155536699,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.772707999999994%\",\"left\":\"69.669754%\",\"width\":\"9.917535999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6295039999999985%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"档案归档与资料管理\\\"\\n}\",\"size\":{\"width\":192.0000089637515,\"height\":35.999999194185264},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":180.00002776316535,\"x\":1316.2579190479719,\"h\":35.999999194185264,\"i\":\"es-drager-1762488401037-4\",\"y\":11.532246572286056,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.1626759999999992%\",\"left\":\"67.98976399999998%\",\"width\":\"9.297691%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6295039999999985%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"成本与投资控制\\\"\\n}\",\"size\":{\"width\":180.00002776316535,\"height\":35.999999194185264},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":95.0000012085815,\"x\":687.9191023369988,\"h\":35.999999194185264,\"i\":\"es-drager-1762488353166-3\",\"y\":711.8710452354521,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"71.770524%\",\"left\":\"35.53365699999999%\",\"width\":\"4.907114%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6295039999999985%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"实时动态\\\"\\n}\",\"size\":{\"width\":95.0000012085815,\"height\":35.999999194185264},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":104.00001130858149,\"x\":34.386878345744435,\"h\":43.00000013957801,\"i\":\"es-drager-1762488324502-2\",\"y\":696.7420858043853,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"70.24522899999998%\",\"left\":\"1.776214%\",\"width\":\"5.371999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.335241%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"金额统计\\\"\\n}\",\"size\":{\"width\":104.00001130858149,\"height\":43.00000013957801},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":169.99999503022275,\"x\":36.71747024670575,\"h\":60.000005269449076,\"i\":\"es-drager-1762488293431-1\",\"y\":373.32239712935564,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"37.63819899999999%\",\"left\":\"1.896598%\",\"width\":\"8.781150999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.049174%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同与资金执行\\\"\\n}\",\"size\":{\"width\":169.99999503022275,\"height\":60.000005269449076},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":559.9999938781947,\"x\":1314.9191025697774,\"h\":37.000003580117294,\"i\":\"es-drager-1762486958682-7\",\"y\":767.3141866387114,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"77.360277%\",\"left\":\"67.92060899999998%\",\"width\":\"28.926144999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7303240000000013%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146619422332997632\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":559.9999938781947,\"height\":37.000003580117294},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":559.9999938781947,\"x\":1326.5803092847598,\"h\":37.000003580117294,\"i\":\"es-drager-1762486938386-6\",\"y\":436.43494332647174,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"44.00117799999999%\",\"left\":\"68.522955%\",\"width\":\"28.926144999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7303240000000013%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146619422374940672\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":559.9999938781947,\"height\":37.000003580117294},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":559.9999938781947,\"x\":1306.0562749376322,\"h\":37.000003580117294,\"i\":\"es-drager-1762486909420-5\",\"y\":15.532234359882779,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.565952999999999%\",\"left\":\"67.46281%\",\"width\":\"28.926144999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7303240000000013%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146619422395912192\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":559.9999938781947,\"height\":37.000003580117294},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":623.9999904128958,\"x\":662.9191173028839,\"h\":37.000003580117294,\"i\":\"es-drager-1762486901803-4\",\"y\":715.8710429417594,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"72.173802%\",\"left\":\"34.24231199999999%\",\"width\":\"32.23199%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7303240000000013%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146619422416883712\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":623.9999904128958,\"height\":37.000003580117294},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":559.9999938781947,\"x\":12.653001497397423,\"h\":37.000003580117294,\"i\":\"es-drager-1762486889055-3\",\"y\":702.7420922825565,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"70.85014699999999%\",\"left\":\"0.6535759999999999%\",\"width\":\"28.926144999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7303240000000013%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146619422433660928\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":559.9999938781947,\"height\":37.000003580117294},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":559.9999938781947,\"x\":23.919097150222747,\"h\":37.000003580117294,\"i\":\"es-drager-1762486883913-2\",\"y\":386.3223946342091,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"38.94885299999999%\",\"left\":\"1.2355129999999999%\",\"width\":\"28.926144999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7303240000000013%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146619422450438144\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":559.9999938781947,\"height\":37.000003580117294},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JPictorialBar\",\"visible\":true,\"w\":450.00000164914417,\"x\":191.58031114825323,\"h\":224.00000049643637,\"i\":\"68308968-04f1-4f24-b8de-632455eb043c\",\"y\":73.06447439212201,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"7.366327999999999%\",\"left\":\"9.895856999999998%\",\"width\":\"23.244223999999996%\",\"position\":\"absolute\",\"config\":{},\"height\":\"22.583580999999995%\"},\"componentName\":\"象形柱图\",\"pageCompId\":\"1146619422463021056\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/pictogram\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"前期\\\",\\n \\\"value\\\": 3900,\\n \\\"symbol\\\": \\\"\\\",\\n \\\"symbolSize\\\": [\\n 60,\\n 60\\n ]\\n },\\n {\\n \\\"name\\\": \\\"统计\\\",\\n \\\"value\\\": 3000,\\n \\\"symbol\\\": \\\"\\\",\\n \\\"symbolSize\\\": [\\n 65,\\n 35\\n ]\\n },\\n {\\n \\\"name\\\": \\\"施工\\\",\\n \\\"value\\\": 2000,\\n \\\"symbol\\\": \\\"\\\",\\n \\\"symbolSize\\\": [\\n 50,\\n 60\\n ]\\n },\\n {\\n \\\"name\\\": \\\"竣工\\\",\\n \\\"value\\\": 2900,\\n \\\"symbol\\\": \\\"\\\",\\n \\\"symbolSize\\\": [\\n 50,\\n 30\\n ]\\n }\\n]\",\"size\":{\"width\":450.0000016491442,\"height\":224.00000049643634},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"show\":true,\"splitLine\":{\"lineStyle\":{\"color\":\"#4A90E242\"},\"show\":true},\"name\":\"单位(个)\",\"yCustomUnit\":\"个\",\"yUnit\":\"CUSTOM\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"type\":\"category\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":33,\"left\":29,\"bottom\":18,\"right\":50,\"containLabel\":true},\"series\":[{\"barCategoryGap\":\"22%\"}],\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"barOpacity\":0.8,\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"barColor\":\"#4A90E2\",\"body\":{\"gradient\":{\"type\":\"linear\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"barGradient\":{\"endColor\":\"#7357FB68\",\"enabled\":true,\"startColor\":\"#06E5FF\",\"direction\":\"to bottom\"}}}},{\"visible\":true,\"h\":39.99999690049243,\"i\":\"b0c2258f-39ba-48dc-95b1-5094042732c1\",\"orderNum\":70,\"component\":\"JText\",\"w\":105.99999849552168,\"x\":63.515831090363434,\"y\":173.773743707761,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"17.519791999999995%\",\"left\":\"3.2808360000000003%\",\"width\":\"5.475305999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.032781999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422479798272\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"在建项目数\\\"\\n}\",\"size\":{\"width\":105.9999984955217,\"height\":39.99999690049243},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#879FA9\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":49.00000551423213,\"x\":126.0480565295428,\"h\":32.000001487878116,\"i\":\"f5a20b4d-547f-403e-8164-dcfcafe4d980\",\"y\":154.30597334068014,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.55706%\",\"left\":\"6.510864999999999%\",\"width\":\"2.5310379999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.226226%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422496575488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":49.00000551423213,\"height\":32.000001487878116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#7A9AE9\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#F1F0FF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":87.99999765517,\"x\":52.05629383172334,\"h\":48.00000223181718,\"i\":\"6b21b6f5-8acb-4f3e-8b78-8c294a480972\",\"y\":142.03986122082082,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"14.320396000000002%\",\"left\":\"2.688907%\",\"width\":\"4.545537%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.839339%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422517547008\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"3531\\\"\\n}\",\"size\":{\"width\":87.99999765517,\"height\":48.00000223181718},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"fontFamily\":\"DIGITALDREAMFAT\",\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#7A9AE9\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#F1F0FF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":28,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":144.99999063645956,\"x\":39.84641532426729,\"h\":59.999995350738644,\"i\":\"291a3240-8c51-43d7-8439-01a320e34d6d\",\"y\":0,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0%\",\"left\":\"2.0582199999999995%\",\"width\":\"7.489804999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.049173%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146619422542712832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目总体概览\\\"\\n}\",\"size\":{\"width\":144.99999063645956,\"height\":59.99999535073864},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":571.9999944384291,\"x\":18.185233955287224,\"h\":37.000003580117294,\"i\":\"7d88daeb-170a-488b-83a6-a5602963ead6\",\"y\":14.266120891723347,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.438304%\",\"left\":\"0.939337%\",\"width\":\"29.545990999999994%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7303240000000013%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146619422567878656\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":571.9999944384291,\"height\":37.000003580117294},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":165.00001738304806,\"x\":31.910876126400943,\"h\":145.00000116267307,\"i\":\"3b982fd0-aee7-496b-8e5e-6c2428fdf509\",\"y\":106.8534541686754,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"10.772917999999999%\",\"left\":\"1.6483189999999999%\",\"width\":\"8.522882999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"14.618835999999996%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":165.00001738304806,\"height\":145.00000116267307},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/组 146 拷贝_1763552659762.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":1162.000009076858,\"x\":401.48886597378663,\"h\":774.9999969796257,\"i\":\"es-drager-1762944604977-2\",\"y\":26.95897560928492,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"2.717992000000001%\",\"left\":\"20.738437999999995%\",\"width\":\"60.021751999999985%\",\"position\":\"absolute\",\"config\":{},\"height\":\"78.13515699999999%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1162.000009076858,\"height\":774.9999969796257},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/建设地图背景_1763552692372.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}}]},\"component\":\"JGroup\",\"w\":1935.9648300117237,\"x\":-2,\"y\":84,\"componentName\":\"建设\",\"pageCompId\":\"1151112776882114560\",\"equalProportion\":false,\"key\":\"3eb04442-3a1d-48c3-8729-1e6ccdf2407d\",\"group\":true},{\"visible\":false,\"h\":993.0175920371283,\"i\":\"es-drager-1762481722941-1\",\"props\":{\"elements\":[{\"component\":\"JBreakRing\",\"visible\":true,\"w\":714.0000045348538,\"x\":0,\"h\":200.000004320926,\"i\":\"aca7fa88-9971-4e8c-8a35-8df42aef1ae2\",\"y\":793.0175877162023,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"79.85936946890986%\",\"left\":\"0%\",\"width\":\"35.018293000000014%\",\"position\":\"absolute\",\"config\":{},\"height\":\"20.14063053109014%\"},\"componentName\":\"多色环形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":400,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"value\\\": 109,\\n \\\"name\\\": \\\"受限空间\\\"\\n },\\n {\\n \\\"value\\\": 171,\\n \\\"name\\\": \\\"临电\\\"\\n },\\n {\\n \\\"value\\\": 73,\\n \\\"name\\\": \\\"动火\\\"\\n },\\n {\\n \\\"value\\\": 29,\\n \\\"name\\\": \\\"高处\\\"\\n },\\n {\\n \\\"value\\\": 73,\\n \\\"name\\\": \\\"动土\\\"\\n },\\n {\\n \\\"value\\\": 88,\\n \\\"name\\\": \\\"吊装\\\"\\n },\\n {\\n \\\"value\\\": 4,\\n \\\"name\\\": \\\"断路\\\"\\n }\\n]\",\"size\":{\"width\":714.0000045348538,\"height\":200.000004320926},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":550,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"customColor\":[{\"color\":\"#326CF1B3\"},{\"color\":\"#0036FFB3\"},{\"color\":\"#FC00FFB3\"},{\"color\":\"#FF5B01B3\"},{\"color\":\"#FFEB0BB3\"},{\"color\":\"#00FF96B3\"},{\"color\":\"#00FCFFB3\"}],\"grid\":{\"top\":50,\"left\":50,\"show\":false},\"series\":[{\"data\":[],\"name\":\"Access From\",\"avoidLabelOverlap\":false,\"emphasis\":{\"label\":{\"show\":true,\"fontSize\":14,\"fontWeight\":\"bold\"}},\"itemStyle\":{\"shadowBlur\":21,\"borderWidth\":5},\"label\":{\"color\":\"#EEF1FA\",\"show\":true,\"fontSize\":12,\"position\":\"center\"},\"labelLine\":{\"length2\":36,\"show\":false},\"type\":\"pie\",\"radius\":[\"40%\",\"70%\"]}],\"legend\":{\"r\":1,\"orient\":\"vertical\",\"t\":13},\"tooltip\":{\"trigger\":\"item\"},\"outRadius\":68,\"title\":{\"subtext\":\"{total}条\",\"top\":33,\"textAlign\":\"\",\"left\":\"center\",\"show\":true,\"customTop\":true,\"text\":\"总数\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontSize\":22,\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\",\"fontSize\":24}},\"innerRadius\":60,\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JListProgress\",\"visible\":true,\"w\":498.00000950250893,\"x\":878.5580325518055,\"h\":71.00000054406586,\"i\":\"es-drager-1763373026447-1\",\"y\":619.4302483462973,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"62.37857751095483%\",\"left\":\"43.089079000000005%\",\"width\":\"24.424524000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"7.14992373885469%\"},\"componentName\":\"列表进度图\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"作业投入\\\",\\n \\\"total\\\": 19,\\n \\\"date\\\": \\\"2025-12-31\\\",\\n \\\"endLabel\\\": \\\"2025-06-15\\\",\\n \\\"value\\\": 6\\n },\\n {\\n \\\"title\\\": \\\"作业进度\\\",\\n \\\"total\\\": 685,\\n \\\"date\\\": \\\"2025-11-20\\\",\\n \\\"endLabel\\\": \\\"2025-05-30\\\",\\n \\\"value\\\": 685\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":498.00000950250893,\"height\":71.00000054406586},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":1200,\"dataType\":1,\"h\":325,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"beginFields\":[{\"name\":\"名称\",\"style\":{\"letterSpacing\":0,\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\"}],\"endCurrent\":0,\"endInfo\":{\"width\":41},\"scroll\":{\"count\":1,\"interval\":3000,\"enabled\":false,\"direction\":\"down\"},\"centerTopFields\":[],\"body\":{\"gradient\":{\"type\":\"linear\"}},\"endFields\":[{\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"name\":\"value\",\"style\":{\"letterSpacing\":0,\"fontSize\":15,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#CFEAFF\",\"fontWeight\":\"normal\"},\"key\":\"value\",\"marginLeft\":0}],\"beginCurrent\":0,\"progressSection\":{\"marginRight\":8,\"marginLeft\":8},\"bar\":{\"border\":{\"padding\":8,\"color\":\"#4ECBFC5E\",\"width\":2,\"enabled\":false},\"total\":{\"field\":\"total\",\"type\":\"field\",\"value\":0},\"borderRadius\":6,\"background\":{\"color\":\"#5A97FC4F\",\"gradient\":{\"endColor\":\"#07203D\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#143B6E\"}},\"indicatorColor\":\"#DCFEFFB5\",\"exceed\":{\"indicatorColor\":\"#FEF8C9E1\",\"fill\":{\"color\":\"#FFB347\",\"gradient\":{\"endColor\":\"#FEAF24\",\"enabled\":true,\"startColor\":\"#FEF6C8\",\"direction\":\"to right\"}},\"percent\":70,\"enabled\":true},\"indicatorSize\":15,\"fill\":{\"color\":\"#33C9FF\",\"gradient\":{\"endColor\":\"#24E5F1\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#C5FDFE\"}},\"valueField\":\"value\",\"height\":4},\"centerTopInfo\":{\"layout\":\"horizontal\"},\"centerTopCurrent\":0,\"row\":{\"marginRight\":0,\"padding\":\"0 0\",\"marginBottom\":0,\"marginTop\":10,\"height\":20,\"marginLeft\":0},\"beginInfo\":{\"layout\":\"vertical\",\"width\":85}}}},{\"component\":\"JListProgress\",\"visible\":true,\"w\":498.00000950250893,\"x\":881.2192292752404,\"h\":71.00000054406586,\"i\":\"d99071e1-0ca9-4ec4-8eb5-97b53b13ff72\",\"y\":535.9953136910605,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"53.976416731096535%\",\"left\":\"43.219598000000005%\",\"width\":\"24.424524000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"7.14992373885469%\"},\"componentName\":\"列表进度图\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"作业投入\\\",\\n \\\"total\\\": 19,\\n \\\"date\\\": \\\"2025-12-31\\\",\\n \\\"endLabel\\\": \\\"2025-06-15\\\",\\n \\\"value\\\": 6\\n },\\n {\\n \\\"title\\\": \\\"作业进度\\\",\\n \\\"total\\\": 685,\\n \\\"date\\\": \\\"2025-11-20\\\",\\n \\\"endLabel\\\": \\\"2025-05-30\\\",\\n \\\"value\\\": 685\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":498.00000950250893,\"height\":71.00000054406586},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":1200,\"dataType\":1,\"h\":325,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"beginFields\":[{\"name\":\"名称\",\"style\":{\"letterSpacing\":0,\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\"}],\"endCurrent\":0,\"endInfo\":{\"width\":41},\"scroll\":{\"count\":1,\"interval\":3000,\"enabled\":false,\"direction\":\"down\"},\"centerTopFields\":[],\"body\":{\"gradient\":{\"type\":\"linear\"}},\"endFields\":[{\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"name\":\"value\",\"style\":{\"letterSpacing\":0,\"fontSize\":15,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#CFEAFF\",\"fontWeight\":\"normal\"},\"key\":\"value\",\"marginLeft\":0}],\"beginCurrent\":0,\"progressSection\":{\"marginRight\":8,\"marginLeft\":8},\"bar\":{\"border\":{\"padding\":8,\"color\":\"#4ECBFC5E\",\"width\":2,\"enabled\":false},\"total\":{\"field\":\"total\",\"type\":\"field\",\"value\":0},\"borderRadius\":6,\"background\":{\"color\":\"#5A97FC4F\",\"gradient\":{\"endColor\":\"#07203D\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#143B6E\"}},\"indicatorColor\":\"#DCFEFFB3\",\"exceed\":{\"indicatorColor\":\"#FEF8C9BD\",\"fill\":{\"color\":\"#FFB347\",\"gradient\":{\"endColor\":\"#FEAF24\",\"enabled\":true,\"startColor\":\"#FEF6C8\",\"direction\":\"to right\"}},\"percent\":70,\"enabled\":true},\"indicatorSize\":15,\"fill\":{\"color\":\"#33C9FF\",\"gradient\":{\"endColor\":\"#24E5F1\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#C5FDFE\"}},\"valueField\":\"value\",\"height\":4},\"centerTopInfo\":{\"layout\":\"horizontal\"},\"centerTopCurrent\":0,\"row\":{\"marginRight\":0,\"padding\":\"0 0\",\"marginBottom\":0,\"marginTop\":10,\"height\":20,\"marginLeft\":0},\"beginInfo\":{\"layout\":\"vertical\",\"width\":85}}}},{\"component\":\"JImg\",\"visible\":true,\"w\":323.0000073915475,\"x\":906.8179276953341,\"h\":2.99999709522538,\"i\":\"es-drager-1762941799018-34\",\"y\":242.1698934071885,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.387271217460356%\",\"left\":\"44.475092%\",\"width\":\"15.841609000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"0.3021091589194335%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":323.0000073915475,\"height\":2.99999709522538},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_26_1763552267080.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371568519-13\",\"orderNum\":70,\"component\":\"JText\",\"w\":42.00000866236812,\"x\":419.7991700328957,\"y\":217.93813036363792,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.947056337294914%\",\"left\":\"20.589146%\",\"width\":\"2.0599000000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"条\\\"\\n}\",\"size\":{\"width\":42.00000866236812,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371480202-12\",\"orderNum\":70,\"component\":\"JText\",\"w\":42.00000866236812,\"x\":636.8761097992264,\"y\":210.70737041841926,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.218896030448274%\",\"left\":\"31.235734000000004%\",\"width\":\"2.0599000000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"公里\\\"\\n}\",\"size\":{\"width\":42.00000866236812,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371473970-11\",\"orderNum\":70,\"component\":\"JText\",\"w\":42.00000866236812,\"x\":507.799168764197,\"y\":213.16890192795518,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.46678000846383%\",\"left\":\"24.905126000000003%\",\"width\":\"2.0599000000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"架次\\\"\\n}\",\"size\":{\"width\":42.00000866236812,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371446932-9\",\"orderNum\":70,\"component\":\"JText\",\"w\":52.000002957467785,\"x\":359.95302146961313,\"y\":183.01505679779882,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.430192804777217%\",\"left\":\"17.653978000000002%\",\"width\":\"2.550352000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"56\\\"\\n}\",\"size\":{\"width\":52.000002957467785,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#02DEFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371270168-8\",\"orderNum\":70,\"component\":\"JText\",\"w\":52.000002957467785,\"x\":470.4145734524033,\"y\":211.78429169116976,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.32734539543296%\",\"left\":\"23.07159%\",\"width\":\"2.550352000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"45\\\"\\n}\",\"size\":{\"width\":52.000002957467785,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#02DEFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371262441-7\",\"orderNum\":70,\"component\":\"JText\",\"w\":52.000002957467785,\"x\":599.1837985154281,\"y\":210.39967155575596,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.18790978557903%\",\"left\":\"29.387106000000006%\",\"width\":\"2.550352000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"80\\\"\\n}\",\"size\":{\"width\":52.000002957467785,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#02DEFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371256473-6\",\"orderNum\":70,\"component\":\"JText\",\"w\":52.000002957467785,\"x\":387.3376428756507,\"y\":217.3227524355682,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.88508584120256%\",\"left\":\"18.997063000000004%\",\"width\":\"2.550352000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"17\\\"\\n}\",\"size\":{\"width\":52.000002957467785,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#02DEFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371251576-5\",\"orderNum\":70,\"component\":\"JText\",\"w\":83.99999693539274,\"x\":318.41456637590863,\"y\":217.630441399603,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.91607108924873%\",\"left\":\"15.616715000000003%\",\"width\":\"4.119799%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"线上执勤\\\"\\n}\",\"size\":{\"width\":83.99999693539274,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371243864-4\",\"orderNum\":70,\"component\":\"JText\",\"w\":42.00000866236812,\"x\":394.26072212282537,\"y\":184.09197807054932,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.538642169761907%\",\"left\":\"19.336607000000004%\",\"width\":\"2.0599000000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"条\\\"\\n}\",\"size\":{\"width\":42.00000866236812,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371236206-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":107.00001235920283,\"x\":605.0299330286167,\"y\":180.2458264255996,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.15132258189242%\",\"left\":\"29.673831%\",\"width\":\"5.247840000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"日巡检里程\\\"\\n}\",\"size\":{\"width\":107.00001235920283,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371219393-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":110.9999937657679,\"x\":484.5684071400704,\"y\":178.8612062901858,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.01188697203849%\",\"left\":\"23.765768000000005%\",\"width\":\"5.444020000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"每日计划飞行\\\"\\n}\",\"size\":{\"width\":110.9999937657679,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763371210351-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":70.99999619602579,\"x\":309.7991716187691,\"y\":182.70736783376395,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.399207556731042%\",\"left\":\"15.194171%\",\"width\":\"3.4822109999999995%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"总计\\\"\\n}\",\"size\":{\"width\":70.99999619602579,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763370911329-4\",\"orderNum\":70,\"component\":\"JText\",\"w\":42.00000866236812,\"x\":197.33764932203988,\"y\":214.24583309933405,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.57523037027158%\",\"left\":\"9.67847%\",\"width\":\"2.0599000000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"版\\\"\\n}\",\"size\":{\"width\":42.00000866236812,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763370895588-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":52.000002957467785,\"x\":172.41455179860495,\"y\":212.86121296392025,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.435794760417647%\",\"left\":\"8.456111000000002%\",\"width\":\"2.550352000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"3\\\"\\n}\",\"size\":{\"width\":52.000002957467785,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#02DEFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763370887375-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":140.99999704041034,\"x\":182.10687090038692,\"y\":183.7842891065145,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.507656921715736%\",\"left\":\"8.931473000000002%\",\"width\":\"6.915377%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"无人机航线设计\\\"\\n}\",\"size\":{\"width\":140.99999704041034,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#C9E6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":311.0000020038218,\"x\":915.2682911062369,\"h\":0,\"i\":\"es-drager-1763370612716-1\",\"y\":242.05284212679635,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.375483784757172%\",\"left\":\"44.889542000000006%\",\"width\":\"15.253065999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"0%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":311.0000020038218,\"height\":0},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_26_1763552300046.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763370022040-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":323.9999823538453,\"x\":355.79916168770234,\"y\":87.47659410898373,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.80916862001706%\",\"left\":\"17.450251000000005%\",\"width\":\"15.890653000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"座 服务于岸线公园巡检和河道巡检\\\"\\n}\",\"size\":{\"width\":323.9999823538453,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763370010036-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":48.00000116155921,\"x\":319.49149072055104,\"y\":87.16890514494884,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.778183371970881%\",\"left\":\"15.669533000000001%\",\"width\":\"2.3541710000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"4\\\"\\n}\",\"size\":{\"width\":48.00000116155921,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#96F5F8\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#49ABFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1763369998907-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":94.00001161983592,\"x\":244.414543346272,\"y\":86.86121618091394,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.7471981239247%\",\"left\":\"11.987367%\",\"width\":\"4.610252000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"建设机场 \\\"\\n}\",\"size\":{\"width\":94.00001161983592,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":36.9999911254748,\"x\":197.3896625372919,\"h\":33.99999677510606,\"i\":\"83560363-065f-4d4a-9be8-fb9b83e84040\",\"y\":88.55514865541414,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.91778246080691%\",\"left\":\"9.681021%\",\"width\":\"1.8146730000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.423906791556098%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":36.9999911254748,\"height\":33.99999677510606},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_03_1763552101512.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JStatsSummary\",\"visible\":true,\"w\":595.0000071772687,\"x\":785.8511946537046,\"h\":85.00000678570775,\"i\":\"51a9c871-5835-4847-8271-f62c0698fa18\",\"y\":107.93976126080858,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"10.869874021000504%\",\"left\":\"38.542251%\",\"width\":\"29.18191100000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.559768474124843%\"},\"componentName\":\"统计概览(背景模式)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": \\\"1\\\",\\n \\\"name\\\": \\\"人员总数\\\",\\n \\\"value\\\": 681,\\n \\\"suffix\\\": \\\"人\\\"\\n },\\n {\\n \\\"id\\\": \\\"2\\\",\\n \\\"name\\\": \\\"车辆总数\\\",\\n \\\"value\\\": 155,\\n \\\"suffix\\\": \\\"辆\\\"\\n },\\n {\\n \\\"id\\\": \\\"3\\\",\\n \\\"name\\\": \\\"道路总里程\\\",\\n \\\"value\\\": 336,\\n \\\"suffix\\\": \\\"公里\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":595.0000071772687,\"height\":85.00000678570775},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":713,\"dataType\":1,\"h\":129,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"layout\":{\"padding\":{\"top\":5,\"left\":20,\"bottom\":0,\"right\":20},\"borderColor\":\"#0f66ff59\",\"borderRadius\":0,\"shadow\":\"none\",\"justify\":\"space-between\",\"borderWidth\":0,\"gap\":16,\"fill\":{\"image\":{\"size\":\"contain\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"drag/lib/img/bg01.png\"},\"color\":\"#0b2b63\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"image\"}},\"fieldMap\":{\"compareValue\":\"compareValue\",\"unit\":\"suffix\",\"negativeValue\":\"0\",\"compareState\":\"compareState\",\"label\":\"name\",\"value\":\"value\",\"positiveValue\":\"1\",\"compareLabel\":\"compareLabel\"},\"card\":{\"padding\":{\"horizontal\":3,\"vertical\":15},\"borderColor\":\"#0F66FF59\",\"borderRadius\":0,\"shadow\":\"none\",\"borderWidth\":0,\"blur\":24,\"minWidth\":100,\"fill\":{\"image\":{\"size\":\"cover\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"\"},\"color\":\"#0B2B6300\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"none\"}},\"sections\":{\"middle\":{\"compare\":{\"valueStyle\":{\"positiveGradient\":{\"endColor\":\"#15f0c5\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#15f0c5\"},\"positiveColor\":\"#15F0C5\",\"fontSize\":14,\"negativeColor\":\"#D0021B\",\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"negativeGradient\":{\"endColor\":\"#D0021B\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#D0021B\"},\"fontColor\":\"#FFFFFF\"},\"alignItems\":\"center\",\"labelStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"fontColor\":\"#9ED3FF\"},\"label\":\"同比\"},\"paddingBottom\":10,\"show\":false,\"type\":\"compare\",\"align\":\"center\"},\"top\":{\"minHeight\":40,\"paddingBottom\":10,\"show\":true,\"paddingTop\":5,\"type\":\"value\",\"align\":\"center\",\"value\":{\"unit\":{\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#96F5F8\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#49ABFF\"},\"fontWeight\":500,\"fontColor\":\"#9ED3FF\"},\"unitGap\":6,\"fontSize\":24,\"fontGradient\":{\"endColor\":\"#96F5F8\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#49ABFF\"},\"fontWeight\":600,\"fontColor\":\"#D8F1FF\"}},\"bottom\":{\"paddingBottom\":10,\"show\":true,\"label\":{\"fontSize\":14,\"fontColor\":\"#C9E6FF\"},\"type\":\"label\",\"align\":\"center\"}}}}},{\"component\":\"JListProgress\",\"visible\":true,\"w\":508.9999991492498,\"x\":175.99061836459558,\"h\":185.99999807928413,\"i\":\"96a95bd1-0e5b-4cff-8138-d5e1a8fcefff\",\"y\":324.55333636593934,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"32.68354347077916%\",\"left\":\"8.6315%\",\"width\":\"24.964021000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"18.73078579581999%\"},\"componentName\":\"列表进度图\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"违规停车\\\",\\n \\\"total\\\": 500000,\\n \\\"date\\\": \\\"2025-12-31\\\",\\n \\\"endLabel\\\": \\\"2025-06-15\\\",\\n \\\"value\\\": 262431\\n },\\n {\\n \\\"title\\\": \\\"海岸线非法闯入\\\",\\n \\\"total\\\": 30000,\\n \\\"date\\\": \\\"2025-11-20\\\",\\n \\\"endLabel\\\": \\\"2025-05-30\\\",\\n \\\"value\\\": 14305\\n },\\n {\\n \\\"title\\\": \\\"配电室巡查\\\",\\n \\\"total\\\": 6000,\\n \\\"date\\\": \\\"2026-01-15\\\",\\n \\\"endLabel\\\": \\\"2025-07-01\\\",\\n \\\"value\\\": 4270\\n },\\n {\\n \\\"title\\\": \\\"危险品车违规行驶\\\",\\n \\\"total\\\": 6000,\\n \\\"date\\\": \\\"2025-10-10\\\",\\n \\\"endLabel\\\": \\\"2025-04-28\\\",\\n \\\"value\\\": 3234\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":508.9999991492498,\"height\":185.99999807928413},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":1200,\"dataType\":1,\"h\":325,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"beginFields\":[{\"name\":\"合同名称\",\"style\":{\"letterSpacing\":0,\"fontSize\":15,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\"}],\"endCurrent\":0,\"endInfo\":{\"width\":103},\"scroll\":{\"count\":1,\"interval\":3000,\"enabled\":true,\"direction\":\"down\"},\"centerTopFields\":[],\"body\":{\"gradient\":{\"type\":\"linear\"}},\"endFields\":[{\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"name\":\"数值\",\"style\":{\"letterSpacing\":0,\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#4FB9E3\",\"fontWeight\":\"bold\"},\"key\":\"value\",\"marginLeft\":23}],\"beginCurrent\":0,\"progressSection\":{\"marginRight\":8,\"marginLeft\":8},\"bar\":{\"border\":{\"padding\":8,\"color\":\"#4ECBFC5E\",\"width\":2,\"enabled\":true},\"total\":{\"field\":\"total\",\"type\":\"field\",\"value\":0},\"borderRadius\":6,\"background\":{\"color\":\"#5A97FC4F\",\"gradient\":{\"endColor\":\"#07203D\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#143B6E\"}},\"indicatorColor\":\"#DCFEFFB5\",\"exceed\":{\"indicatorColor\":\"#FEF8C9B3\",\"fill\":{\"color\":\"#FFB347\",\"gradient\":{\"endColor\":\"#FEAF24\",\"enabled\":true,\"startColor\":\"#FEF6C8\",\"direction\":\"to right\"}},\"percent\":70,\"enabled\":true},\"indicatorSize\":15,\"fill\":{\"color\":\"#33C9FF\",\"gradient\":{\"endColor\":\"#24E5F1\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#C5FDFE\"}},\"valueField\":\"value\",\"height\":6},\"centerTopInfo\":{\"layout\":\"horizontal\"},\"centerTopCurrent\":0,\"row\":{\"marginRight\":1,\"padding\":\"0 0\",\"marginBottom\":0,\"marginTop\":4,\"height\":42,\"marginLeft\":0},\"beginInfo\":{\"layout\":\"vertical\",\"width\":131}}}},{\"component\":\"JImg\",\"visible\":true,\"w\":3.000006444267293,\"x\":464.88365818962495,\"h\":53.99999720719867,\"i\":\"es-drager-1762942619160-4\",\"y\":186.6020326563206,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.791412574425333%\",\"left\":\"22.800325000000004%\",\"width\":\"0.14713600000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.437969844665112%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":3.000006444267293,\"height\":53.99999720719867},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_22_1763552318849.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":1.9999907032825328,\"x\":866.0504140684643,\"h\":40.99999989592697,\"i\":\"es-drager-1762942529865-1\",\"y\":630.1805348963725,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.46116523510798%\",\"left\":\"42.475640000000006%\",\"width\":\"0.09809000000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.12882915919117%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1.9999907032825328,\"height\":40.99999989592697},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_22_1763552318849.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":1.9999907032825328,\"x\":869.9777278548183,\"h\":40.99999989592697,\"i\":\"es-drager-1762941823717-35\",\"y\":550.5439613083489,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"55.441511381377865%\",\"left\":\"42.66825600000001%\",\"width\":\"0.09809000000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.12882915919117%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1.9999907032825328,\"height\":40.99999989592697},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_22_1763552318849.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941725340-32\",\"orderNum\":70,\"component\":\"JText\",\"w\":34.000005070551,\"x\":1291.4712878116177,\"y\":451.7116100695211,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.48878224230209%\",\"left\":\"63.34050374010456%\",\"width\":\"1.667538%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"人\\\"\\n}\",\"size\":{\"width\":34.000005070551,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":1253.7456067176438,\"h\":38.0000028007016,\"i\":\"es-drager-1762941720287-31\",\"y\":451.96951883151854,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.51475446717158%\",\"left\":\"61.49023911015685%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"14\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941657363-30\",\"orderNum\":70,\"component\":\"JText\",\"w\":34.000005070551,\"x\":1091.4243899206565,\"y\":453.711612210325,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.690188758847384%\",\"left\":\"53.529158026387826%\",\"width\":\"1.667538%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"人\\\"\\n}\",\"size\":{\"width\":34.000005070551,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":1049.900347577761,\"h\":38.0000028007016,\"i\":\"es-drager-1762941651441-29\",\"y\":451.9695170875309,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.51475429154653%\",\"left\":\"51.49260190303707%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"88\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941632755-28\",\"orderNum\":70,\"component\":\"JText\",\"w\":34.000005070551,\"x\":878.8452499574914,\"y\":455.638918235086,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.88427454747951%\",\"left\":\"43.103165643141644%\",\"width\":\"1.667538%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"人\\\"\\n}\",\"size\":{\"width\":34.000005070551,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941620159-27\",\"orderNum\":70,\"component\":\"JText\",\"w\":75.99999334357558,\"x\":1214.366941157304,\"y\":453.10667325785766,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.629269500485975%\",\"left\":\"59.558903480209146%\",\"width\":\"3.727436999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"离线:\\\"\\n}\",\"size\":{\"width\":75.99999334357558,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941614133-26\",\"orderNum\":70,\"component\":\"JText\",\"w\":74.99999799193434,\"x\":1215.633060735264,\"y\":422.7198179040119,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.56921743317984%\",\"left\":\"59.62100060355991%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"在线:\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941608903-25\",\"orderNum\":70,\"component\":\"JText\",\"w\":34.000005070551,\"x\":1294.132494595217,\"y\":422.7198161600243,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.5692172575548%\",\"left\":\"63.47102323350761%\",\"width\":\"1.667538%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"人\\\"\\n}\",\"size\":{\"width\":34.000005070551,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941602235-24\",\"orderNum\":70,\"component\":\"JText\",\"w\":34.000005070551,\"x\":1092.819479910352,\"y\":425.2520436733287,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.824220545876166%\",\"left\":\"53.59758053298479%\",\"width\":\"1.667538%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"人\\\"\\n}\",\"size\":{\"width\":34.000005070551,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":1253.8745693152403,\"h\":38.0000028007016,\"i\":\"es-drager-1762941596165-23\",\"y\":421.7115971894209,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.46768643084153%\",\"left\":\"61.49656411015684%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"30\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":1051.2954451125793,\"h\":38.0000028007016,\"i\":\"es-drager-1762941590403-22\",\"y\":424.243844499982,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.72269171280903%\",\"left\":\"51.56102477968631%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"173\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941575296-21\",\"orderNum\":70,\"component\":\"JText\",\"w\":94.00001161983592,\"x\":997.9894510814421,\"y\":451.96951883151854,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.51475446717158%\",\"left\":\"48.94662014973859%\",\"width\":\"4.610252000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"离线:\\\"\\n}\",\"size\":{\"width\":94.00001161983592,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941569613-20\",\"orderNum\":70,\"component\":\"JText\",\"w\":94.00001161983592,\"x\":997.9894639256624,\"y\":424.11488516966904,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.70970510196275%\",\"left\":\"48.94662077968631%\",\"width\":\"4.610252000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"在线:\\\"\\n}\",\"size\":{\"width\":94.00001161983592,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":836.0551084259791,\"h\":38.0000028007016,\"i\":\"es-drager-1762941564580-19\",\"y\":453.3645878411632,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.65524231157953%\",\"left\":\"41.004513396440124%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"122\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":836.1840685085348,\"h\":38.0000028007016,\"i\":\"es-drager-1762941053477-18\",\"y\":419.3083173638135,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.22566858091834%\",\"left\":\"41.010838273089355%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"252\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762941048379-17\",\"orderNum\":70,\"component\":\"JText\",\"w\":34.000005070551,\"x\":877.7080904620868,\"y\":421.5826460137488,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.45470064119328%\",\"left\":\"43.047393396440114%\",\"width\":\"1.667538%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"人\\\"\\n}\",\"size\":{\"width\":34.000005070551,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762940580987-16\",\"orderNum\":70,\"component\":\"JText\",\"w\":94.00001161983592,\"x\":782.8780923517,\"y\":453.36459191848354,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.65524272217853%\",\"left\":\"38.39643451979088%\",\"width\":\"4.610252000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"离线:\\\"\\n}\",\"size\":{\"width\":94.00001161983592,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":38.0000028007016,\"i\":\"es-drager-1762940573639-15\",\"orderNum\":70,\"component\":\"JText\",\"w\":94.00001161983592,\"x\":782.8780898366589,\"y\":420.4454857661016,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.340185021654825%\",\"left\":\"38.39643439644011%\",\"width\":\"4.610252000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"在线:\\\"\\n}\",\"size\":{\"width\":94.00001161983592,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":122.0000038018523,\"x\":755.0234452012544,\"h\":38.0000028007016,\"i\":\"es-drager-1762940556528-14\",\"y\":629.3552169593522,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.378053118702546%\",\"left\":\"37.030297000000004%\",\"width\":\"5.983518000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"酒水作业\\\"\\n}\",\"size\":{\"width\":122.0000038018523,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":100.99999947066824,\"x\":765.1524020939509,\"h\":38.0000028007016,\"i\":\"es-drager-1762940550624-13\",\"y\":547.057446629083,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"55.09040836898173%\",\"left\":\"37.527074000000006%\",\"width\":\"4.953568000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"道路机扫\\\"\\n}\",\"size\":{\"width\":100.99999947066824,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":122.0000038018523,\"x\":1215.0281373863072,\"h\":38.0000028007016,\"i\":\"es-drager-1762940525754-12\",\"y\":221.03887569834177,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"22.25931116133512%\",\"left\":\"59.591331999999994%\",\"width\":\"5.983518000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"海港人员\\\"\\n}\",\"size\":{\"width\":122.0000038018523,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":122.0000038018523,\"x\":1002.4823825925793,\"h\":38.0000028007016,\"i\":\"es-drager-1762940521846-11\",\"y\":225.3112028888313,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"22.689547969297916%\",\"left\":\"49.166977%\",\"width\":\"5.983518000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"临港人员\\\"\\n}\",\"size\":{\"width\":122.0000038018523,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":122.0000038018523,\"x\":792.316981705006,\"h\":38.0000028007016,\"i\":\"es-drager-1762940511936-10\",\"y\":225.67715517941065,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"22.726400517884553%\",\"left\":\"38.859367000000006%\",\"width\":\"5.983518000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"空港人员\\\"\\n}\",\"size\":{\"width\":122.0000038018523,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":43.999999365650666,\"x\":1914.7889788309733,\"h\":32.000008610250845,\"i\":\"es-drager-1762938531837-5\",\"y\":636.9519390937786,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.1430669709589%\",\"left\":\"93.91126200000001%\",\"width\":\"2.1579900000000007%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.2225016824328714%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"kg\\\"\\n}\",\"size\":{\"width\":43.999999365650666,\"height\":32.000008610250845},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#88999A\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":43.999999365650666,\"x\":1776.781953911325,\"h\":32.000008610250845,\"i\":\"es-drager-1762938522652-4\",\"y\":638.2180528504308,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.27056861511959%\",\"left\":\"87.14267600000001%\",\"width\":\"2.1579900000000007%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.2225016824328714%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"kg\\\"\\n}\",\"size\":{\"width\":43.999999365650666,\"height\":32.000008610250845},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#88999A\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":43.999999365650666,\"x\":1643.839397243353,\"h\":32.000008610250845,\"i\":\"es-drager-1762938514850-3\",\"y\":636.9519390937786,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.1430669709589%\",\"left\":\"80.62247800000002%\",\"width\":\"2.1579900000000007%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.2225016824328714%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"kg\\\"\\n}\",\"size\":{\"width\":43.999999365650666,\"height\":32.000008610250845},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#88999A\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":115.99999091331775,\"x\":1494.3083357704925,\"h\":38.0000028007016,\"i\":\"es-drager-1762938341955-2\",\"y\":545.6623636434895,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"54.9499191171517%\",\"left\":\"73.28869300000001%\",\"width\":\"5.689246000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"7153\\\"\\n}\",\"size\":{\"width\":115.99999091331775,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":43.999999365650666,\"x\":1530.017579506108,\"h\":32.000008610250845,\"i\":\"es-drager-1762938204176-1\",\"y\":633.2825373568783,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.773546655676995%\",\"left\":\"75.04006100000001%\",\"width\":\"2.1579900000000007%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.2225016824328714%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"kg\\\"\\n}\",\"size\":{\"width\":43.999999365650666,\"height\":32.000008610250845},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#88999A\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":70.99999619602579,\"x\":1867.8136033479955,\"h\":38.0000028007016,\"i\":\"es-drager-1762936596210-18\",\"y\":630.4923812843198,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.49256914884002%\",\"left\":\"91.607344%\",\"width\":\"3.4822109999999995%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"9401\\\"\\n}\",\"size\":{\"width\":70.99999619602579,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#46D6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":1751.4595718742557,\"h\":38.0000028007016,\"i\":\"es-drager-1762936591363-17\",\"y\":633.1535780265655,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.760560044830726%\",\"left\":\"85.90073400000001%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"0\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#46D6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":61.00000190092615,\"x\":1618.6459574145372,\"h\":30.000000648138894,\"i\":\"es-drager-1762936587463-16\",\"y\":635.8147648701827,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.02854994399837%\",\"left\":\"79.38686000000001%\",\"width\":\"2.9917590000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.0210945796635205%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"0\\\"\\n}\",\"size\":{\"width\":61.00000190092615,\"height\":30.000000648138894},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#46D6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1866.5474862850765,\"h\":45.000005921522515,\"i\":\"es-drager-1762936579995-15\",\"y\":649.4841668231293,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"65.40510178583477%\",\"left\":\"91.54524700000002%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.5316423679068105%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"其他垃圾\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":45.000005921522515},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":13,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":101.99997443296604,\"x\":1739.9355149316998,\"h\":38.0000028007016,\"i\":\"es-drager-1762936575774-14\",\"y\":652.0164141336904,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"65.66010706780227%\",\"left\":\"85.335534%\",\"width\":\"5.002612000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"可回收垃圾\\\"\\n}\",\"size\":{\"width\":101.99997443296604,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":13,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1613.3235843570108,\"h\":38.0000028007016,\"i\":\"es-drager-1762936571795-13\",\"y\":653.2825278903425,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"65.78760871196295%\",\"left\":\"79.12582300000001%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"有害垃圾\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":13,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1490.5099641923919,\"h\":22.999997527317976,\"i\":\"es-drager-1762936567296-12\",\"y\":659.6131263694886,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"66.42511992323557%\",\"left\":\"73.10240100000001%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.316172212028447%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"厨余垃圾\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":22.999997527317976},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":13,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":133.0000138379367,\"x\":1816.9109140786636,\"h\":38.0000028007016,\"i\":\"es-drager-1762936479611-11\",\"y\":573.2590862100086,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"57.728996022517066%\",\"left\":\"89.11081%\",\"width\":\"6.523016000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"当月投放量(KG)\\\"\\n}\",\"size\":{\"width\":133.0000138379367,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1679.032810977925,\"h\":38.0000028007016,\"i\":\"es-drager-1762936475992-10\",\"y\":573.3880356416931,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"57.74198163654029%\",\"left\":\"82.34854700000001%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"设施\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":115.99999091331775,\"x\":1818.4349359474331,\"h\":38.0000028007016,\"i\":\"es-drager-1762936466314-9\",\"y\":543.1301262315568,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"54.69491483200728%\",\"left\":\"89.18555600000002%\",\"width\":\"5.689246000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"9301\\\"\\n}\",\"size\":{\"width\":115.99999091331775,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":115.99999091331775,\"x\":1659.032822387726,\"h\":38.0000028007016,\"i\":\"es-drager-1762936448667-7\",\"y\":547.0574367304547,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"55.09040737215868%\",\"left\":\"81.36764300000002%\",\"width\":\"5.689246000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"85\\\"\\n}\",\"size\":{\"width\":115.99999091331775,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":107.00001235920283,\"x\":1499.5017666197657,\"h\":38.0000028007016,\"i\":\"es-drager-1762936431998-6\",\"y\":573.645954302319,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"57.76795485823283%\",\"left\":\"73.543406%\",\"width\":\"5.247840000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"总注册人数\\\"\\n}\",\"size\":{\"width\":107.00001235920283,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":56.000004753376324,\"x\":1493.4290861110203,\"h\":38.0000028007016,\"i\":\"es-drager-1762936404011-4\",\"y\":628.3470119646975,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.27652369941136%\",\"left\":\"73.24557000000001%\",\"width\":\"2.7465330000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"123\\\"\\n}\",\"size\":{\"width\":56.000004753376324,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#46D6FF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":117.00000665430248,\"x\":1850.1641390544198,\"h\":51.000000111973286,\"i\":\"es-drager-1762936392574-3\",\"y\":637.6482977004861,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.21319247651806%\",\"left\":\"90.741722%\",\"width\":\"5.738292000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.135860685745679%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":117.00000665430248,\"height\":51.000000111973286},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_06_1763552183216.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":132.00001848629552,\"x\":1726.0844222162254,\"h\":55.99999527068225,\"i\":\"es-drager-1762936386243-2\",\"y\":633.8499465319011,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.830686547212935%\",\"left\":\"84.65620400000002%\",\"width\":\"6.473971000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.6393759506114005%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":132.00001848629552,\"height\":55.99999527068225},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_06_1763552183216.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":105.99999661821808,\"x\":1602.133647586284,\"h\":60.00000129627779,\"i\":\"es-drager-1762936160272-1\",\"y\":632.7127822069335,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.71617051707547%\",\"left\":\"78.57701000000002%\",\"width\":\"5.198794000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.042189159327041%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":105.99999661821808,\"height\":60.00000129627779},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_06_1763552183216.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":476.9999847819812,\"x\":1476.7877932958268,\"h\":28.99999171776873,\"i\":\"es-drager-1762934953780-14\",\"y\":689.6881586206188,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"69.45377042170585%\",\"left\":\"72.42939400000002%\",\"width\":\"23.394572999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9203905298673134%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":476.9999847819812,\"height\":28.99999171776873},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763552278772.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":100.00000411902698,\"x\":1479.449010408605,\"h\":61.00000032801957,\"i\":\"es-drager-1762934758071-13\",\"y\":630.3094942266853,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.47415184595424%\",\"left\":\"72.559914%\",\"width\":\"4.904523000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.142892212300184%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":100.00000411902698,\"height\":61.00000032801957},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_06_1763552183216.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":147.9999848912427,\"x\":1855.152412329461,\"h\":38.0000028007016,\"i\":\"es-drager-1762933339573-12\",\"y\":91.12543639477417,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.176618533800058%\",\"left\":\"90.98637300000001%\",\"width\":\"7.258693000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"厨余垃圾(吨/月)\\\"\\n}\",\"size\":{\"width\":147.9999848912427,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1889.466596793904,\"h\":38.0000028007016,\"i\":\"es-drager-1762933332326-11\",\"y\":58.60141419624384,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.90134703213322%\",\"left\":\"92.66932000000001%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"109.45\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#4FC5E3\",\"gradient\":{\"endColor\":\"#96F5F8\",\"enabled\":true,\"startColor\":\"#49ABFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":131.00000274531064,\"x\":1659.032822387726,\"h\":38.0000028007016,\"i\":\"es-drager-1762933079266-10\",\"y\":95.05275679230033,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.572112070774509%\",\"left\":\"81.36764300000002%\",\"width\":\"6.424924999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"生活垃圾(吨/月)\\\"\\n}\",\"size\":{\"width\":131.00000274531064,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1688.2825386004924,\"h\":38.0000028007016,\"i\":\"es-drager-1762933073725-9\",\"y\":58.7303636279284,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.914332646156435%\",\"left\":\"82.80220200000001%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"35.40\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#4FC5E3\",\"gradient\":{\"endColor\":\"#96F5F8\",\"enabled\":true,\"startColor\":\"#49ABFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":176.99999281424385,\"x\":1838.0187576475853,\"h\":92.00000000790027,\"i\":\"es-drager-1762932876846-8\",\"y\":69.67643514839494,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"7.016636533644591%\",\"left\":\"90.14604900000002%\",\"width\":\"8.681005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.26468984493685%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":176.99999281424385,\"height\":92.00000000790027},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_14_1763552212132.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":174.0000067593201,\"x\":1638.1008165170927,\"h\":103.99999828743017,\"i\":\"es-drager-1762932868681-7\",\"y\":63.474795999561906,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.392111933218262%\",\"left\":\"80.34102800000001%\",\"width\":\"8.533870000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.473127477437648%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":174.0000067593201,\"height\":103.99999828743017},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_14_1763552212132.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1488.2356348724034,\"h\":38.0000028007016,\"i\":\"es-drager-1762932864087-6\",\"y\":92.64946881205219,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.330093399653293%\",\"left\":\"72.99085600000001%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"总点位\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":74.99999799193434,\"x\":1488.3646178593435,\"h\":38.0000028007016,\"i\":\"es-drager-1762932853402-5\",\"y\":56.06096092276985,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.645515383847678%\",\"left\":\"72.99718200000001%\",\"width\":\"3.678391999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.826720000271738%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"206\\\"\\n}\",\"size\":{\"width\":74.99999799193434,\"height\":38.0000028007016},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#4FC5E3\",\"gradient\":{\"endColor\":\"#96F5F8\",\"enabled\":true,\"startColor\":\"#49ABFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":509.99999450089103,\"x\":1466.9167412089805,\"h\":73.99999763929125,\"i\":\"es-drager-1762931911166-4\",\"y\":545.608435916168,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"54.94448842511221%\",\"left\":\"71.94526600000002%\",\"width\":\"25.013066000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"7.452032897774124%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":509.99999450089103,\"height\":73.99999763929125},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/按钮21 拷贝_1763552170900.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":122.0000038018523,\"x\":1466.0375119388516,\"h\":93.99999807138387,\"i\":\"es-drager-1762930997431-1\",\"y\":68.66823015374024,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.915107114353396%\",\"left\":\"71.90214400000002%\",\"width\":\"5.983518000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.46609595088314%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":122.0000038018523,\"height\":93.99999807138387},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_14_1763552212132.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":109.00000306248539,\"x\":1408.7327115379485,\"h\":37.000003768959814,\"i\":\"es-drager-1762429130209-4\",\"y\":744.3141747813775,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.95478234725454%\",\"left\":\"69.091617%\",\"width\":\"5.345930000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812791721984\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"当月投放量\\\"\\n}\",\"size\":{\"width\":109.00000306248539,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JMultipleBar\",\"visible\":true,\"w\":627.9999965068347,\"x\":1410.9343528483823,\"h\":246.99999840730368,\"i\":\"es-drager-1762429121828-3\",\"y\":742.8628378907972,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.80862814996556%\",\"left\":\"69.199597%\",\"width\":\"30.800403%\",\"position\":\"absolute\",\"config\":{},\"height\":\"24.873678008120173%\"},\"componentName\":\"对比柱形图\",\"pageCompId\":\"1146390812808499200\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"厨余垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"厨余垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 2.5,\\n \\\"type\\\": \\\"厨余垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"厨余垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"厨余垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"厨余垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"其他垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"其他垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"其他垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"其他垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"其他垃圾\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"其他垃圾\\\"\\n }\\n]\",\"size\":{\"width\":627.9999965068347,\"height\":246.99999840730368},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"yUnit\":\"\"},\"customColor\":[{\"color1\":\"#4FF0FDA8\",\"color\":\"#4FF0FD00\"},{\"color1\":\"#4F68FDA8\",\"color\":\"#4F68FD00\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":47,\"left\":0,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"barWidth\":15,\"itemStyle\":{\"borderRadius\":0}}],\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JText\",\"visible\":true,\"w\":93.00001626819461,\"x\":1454.9343318246895,\"h\":37.000003768959814,\"i\":\"es-drager-1762429044550-2\",\"y\":310.879252361777,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"31.306520131634624%\",\"left\":\"71.35758600000001%\",\"width\":\"4.5612070000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812825276416\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"厨余垃圾\\\"\\n}\",\"size\":{\"width\":93.00001626819461,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":93.00001626819461,\"x\":1447.9343643632005,\"h\":37.000003768959814,\"i\":\"es-drager-1762429023915-1\",\"y\":149.86283794746865,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.091659921153278%\",\"left\":\"71.01427100000001%\",\"width\":\"4.5612070000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812837859328\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"生活垃圾\\\"\\n}\",\"size\":{\"width\":93.00001626819461,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4A90E2\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JMultipleLine\",\"visible\":true,\"w\":545.9999902747245,\"x\":1459.869878257456,\"h\":160.00000345674079,\"i\":\"9799edd5-f660-4e56-9305-d9e43773a75e\",\"y\":316.5486620607893,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"31.87744754968588%\",\"left\":\"71.599651%\",\"width\":\"26.778693999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"16.11250442487211%\"},\"componentName\":\"对比折线图\",\"pageCompId\":\"1146390812846247936\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 2,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 1.5,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 2.9,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 2,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 1.5,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"空港\\\"\\n }\\n]\",\"size\":{\"width\":545.9999902747245,\"height\":160.00000345674079},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#1e90ff\",\"color\":\"#2291E3A3\"},{\"color1\":\"#90ee90\",\"color\":\"#02F4FF94\"},{\"color1\":\"#00ced1\",\"color\":\"#FF029B9E\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":36,\"left\":0,\"bottom\":22,\"right\":1,\"containLabel\":true},\"series\":[{\"symbol\":\"none\",\"lineType\":\"area\",\"label\":{\"position\":\"top\"}}],\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JStackBar\",\"visible\":true,\"w\":557.9999956624503,\"x\":1455.200473925311,\"h\":152.00000130417803,\"i\":\"9857c542-9530-4c6c-ac2a-2fd20b6da0b3\",\"y\":155.8628321379194,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.695878238992144%\",\"left\":\"71.37063900000001%\",\"width\":\"27.367237000000006%\",\"position\":\"absolute\",\"config\":{},\"height\":\"15.306879004263887%\"},\"componentName\":\"生活垃圾\",\"pageCompId\":\"1146390812858830848\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"临港\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"海港\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"空港\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"空港\\\"\\n }\\n]\",\"size\":{\"width\":557.9999956624503,\"height\":152.00000130417803},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#F4DC42B3\",\"color\":\"#F4DC4200\"},{\"color1\":\"#00D8FFB3\",\"color\":\"#00D8FF00\"},{\"color1\":\"#006CFFB3\",\"color\":\"#006CFF00\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":43,\"left\":0,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"barWidth\":21,\"itemStyle\":{\"borderRadius\":1},\"label\":{\"color\":\"#EEF1FA\",\"show\":false}}],\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JMultipleBar\",\"visible\":true,\"w\":627.9999965068347,\"x\":743.9343337341033,\"h\":246.99999840730368,\"i\":\"ddbe13ab-bd12-4d18-9551-5773cc07cb6d\",\"y\":741.8628388590553,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.70792509699238%\",\"left\":\"36.48642900000001%\",\"width\":\"30.800403%\",\"position\":\"absolute\",\"config\":{},\"height\":\"24.873678008120173%\"},\"componentName\":\"对比柱形图\",\"pageCompId\":\"1146390812879802368\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"修剪\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"修剪\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 2.5,\\n \\\"type\\\": \\\"修剪\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"修剪\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"修剪\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"修剪\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"破绿\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"破绿\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"破绿\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"破绿\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"破绿\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"破绿\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 13,\\n \\\"type\\\": \\\"基础养护\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 14,\\n \\\"type\\\": \\\"基础养护\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 13.5,\\n \\\"type\\\": \\\"基础养护\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 15,\\n \\\"type\\\": \\\"基础养护\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 14.9,\\n \\\"type\\\": \\\"基础养护\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 16,\\n \\\"type\\\": \\\"基础养护\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 3,\\n \\\"type\\\": \\\"提升改造\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 4,\\n \\\"type\\\": \\\"提升改造\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 3.5,\\n \\\"type\\\": \\\"提升改造\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 5,\\n \\\"type\\\": \\\"提升改造\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 4.9,\\n \\\"type\\\": \\\"提升改造\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 6,\\n \\\"type\\\": \\\"提升改造\\\"\\n }\\n]\",\"size\":{\"width\":627.9999965068347,\"height\":246.99999840730368},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"yUnit\":\"\"},\"customColor\":[{\"color1\":\"#3FECFBB3\",\"color\":\"#3FECFB00\"},{\"color1\":\"#006CFFB5\",\"color\":\"#006CFF00\"},{\"color1\":\"#C000FFB3\",\"color\":\"#109EE05C\"},{\"color1\":\"#3F8FFBB5\",\"color\":\"#3F8FFB00\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":47,\"left\":0,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"barWidth\":15,\"itemStyle\":{\"borderRadius\":0}}],\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JMultipleLine\",\"visible\":true,\"w\":549.0000171083353,\"x\":153.2004704533646,\"h\":244.00000131207827,\"i\":\"4c6e2a64-9c5b-4433-a253-e3246a922a38\",\"y\":519.0562760062809,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"52.27060227014323%\",\"left\":\"7.513752%\",\"width\":\"26.925831%\",\"position\":\"absolute\",\"config\":{},\"height\":\"24.571568849200737%\"},\"componentName\":\"对比折线图\",\"pageCompId\":\"1146390812892385280\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"08-23\\\",\\n \\\"value\\\": 620,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-24\\\",\\n \\\"value\\\": 768,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-25\\\",\\n \\\"value\\\": 600,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-26\\\",\\n \\\"value\\\": 810,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-27\\\",\\n \\\"value\\\": 700,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-28\\\",\\n \\\"value\\\": 900,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-29\\\",\\n \\\"value\\\": 820,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-30\\\",\\n \\\"value\\\": 830,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-31\\\",\\n \\\"value\\\": 790,\\n \\\"type\\\": \\\"总量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-23\\\",\\n \\\"value\\\": 196,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-24\\\",\\n \\\"value\\\": 600,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-25\\\",\\n \\\"value\\\": 300,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-26\\\",\\n \\\"value\\\": 550,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-27\\\",\\n \\\"value\\\": 610,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-28\\\",\\n \\\"value\\\": 390,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-29\\\",\\n \\\"value\\\": 420,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-30\\\",\\n \\\"value\\\": 380,\\n \\\"type\\\": \\\"完成量\\\"\\n },\\n {\\n \\\"name\\\": \\\"08-31\\\",\\n \\\"value\\\": 600,\\n \\\"type\\\": \\\"完成量\\\"\\n }\\n]\",\"size\":{\"width\":549.0000171083353,\"height\":244.00000131207827},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"yUnit\":\"\"},\"customColor\":[{\"color1\":\"#1e90ff\",\"color\":\"#AB315C\"},{\"color1\":\"#90ee90\",\"color\":\"#4FCAE3\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"name\":\"单位(个)\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":39,\"left\":0,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"symbol\":\"circle\",\"symbolSize\":6,\"lineType\":\"line\",\"label\":{\"color\":\"#EEF1FA\",\"show\":false,\"position\":\"top\"},\"lineWidth\":2}],\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"left\":\"center\",\"show\":true,\"text\":\"无人机巡检完成情况\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JText\",\"visible\":true,\"w\":88.99999408294256,\"x\":1463.7409174557679,\"h\":40.99999989592697,\"i\":\"es-drager-1762423810391-12\",\"y\":495.8229777070758,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"49.93093593537638%\",\"left\":\"71.789507%\",\"width\":\"4.365025%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.12882915919117%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812909162496\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"垃圾分类\\\"\\n}\",\"size\":{\"width\":88.99999408294256,\"height\":40.99999989592697},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":93.00001626819461,\"x\":1479.9343583411257,\"h\":37.000003768959814,\"i\":\"es-drager-1762423782353-11\",\"y\":9.862844821448832,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0.9932195462132423%\",\"left\":\"72.58371800000002%\",\"width\":\"4.5612070000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812921745408\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"垃圾收运\\\"\\n}\",\"size\":{\"width\":93.00001626819461,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":148.99998024288396,\"x\":761.7409183086403,\"h\":38.99999193381503,\"i\":\"es-drager-1762423748544-10\",\"y\":705.7092569027857,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"71.06714549286653%\",\"left\":\"37.35975700000001%\",\"width\":\"7.307738000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.9274220564218205%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812934328320\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"园林作业用工\\\"\\n}\",\"size\":{\"width\":148.99998024288396,\"height\":38.99999193381503},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":107.00001235920283,\"x\":782.2004827011841,\"h\":30.000000648138894,\"i\":\"es-drager-1762423716800-9\",\"y\":61.00000032801958,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.142892212300185%\",\"left\":\"38.363201000000004%\",\"width\":\"5.247840000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.0210945796635205%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812951105536\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"环卫作业\\\"\\n}\",\"size\":{\"width\":107.00001235920283,\"height\":30.000000648138894},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":100.00000411902698,\"x\":186.26495127640098,\"h\":42.99999795941056,\"i\":\"es-drager-1762423687040-8\",\"y\":756.1371663778658,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.14539484911707%\",\"left\":\"9.135407000000002%\",\"width\":\"4.904523000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.33023526513746%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812963688448\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"危险作业\\\"\\n}\",\"size\":{\"width\":100.00000411902698,\"height\":42.99999795941056},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":103.00001056329427,\"x\":174.8698773207855,\"h\":48.0000030167479,\"i\":\"es-drager-1762423656832-7\",\"y\":276.8393938102532,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"27.878599133609544%\",\"left\":\"8.576533000000001%\",\"width\":\"5.051659000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.833751526826244%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390812980465664\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"巡检信息\\\"\\n}\",\"size\":{\"width\":103.00001056329427,\"height\":48.0000030167479},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":569.0000056985347,\"x\":1438.1359798965655,\"h\":37.000003768959814,\"i\":\"es-drager-1762423639047-6\",\"y\":500.29073351733564,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.38085302104397%\",\"left\":\"70.53370700000002%\",\"width\":\"27.906735000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146390812993048576\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":569.0000056985347,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":573.0000074944433,\"x\":1453.934356862392,\"h\":37.000003768959814,\"i\":\"es-drager-1762423624157-5\",\"y\":13.862831049787644,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.396030761282759%\",\"left\":\"71.30854200000002%\",\"width\":\"28.102916000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146390813005631488\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":573.0000074944433,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":614.0000004158265,\"x\":740.8053850815827,\"h\":37.000003768959814,\"i\":\"es-drager-1762423610295-4\",\"y\":708.1770245481904,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"71.31565746941088%\",\"left\":\"36.332969000000006%\",\"width\":\"30.113770000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146390813018214400\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":614.0000004158265,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":660.0000108741033,\"x\":759.2004468880306,\"h\":37.000003768959814,\"i\":\"es-drager-1762423600686-3\",\"y\":61.999999359761375,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.24359526527333%\",\"left\":\"37.23515900000001%\",\"width\":\"32.369851000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146390813034991616\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":660.0000108741033,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":538.0000070722509,\"x\":158.9343362111958,\"h\":37.000003768959814,\"i\":\"es-drager-1762423595387-2\",\"y\":761.871045843406,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.72281457576837%\",\"left\":\"7.794970753298481%\",\"width\":\"26.386333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146390813051768832\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":538.0000070722509,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":538.0000070722509,\"x\":156.6682288267175,\"h\":37.000003768959814,\"i\":\"es-drager-1762423584121-1\",\"y\":283.573262408907,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"28.55672091641095%\",\"left\":\"7.683829000000002%\",\"width\":\"26.386333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146390813064351744\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":538.0000070722509,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":170.00000496341158,\"x\":177.20046083947247,\"h\":60.00000129627779,\"i\":\"72ffb6d2-c59b-407e-83d4-146e19df5a1a\",\"y\":0,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0%\",\"left\":\"8.690837000000002%\",\"width\":\"8.337689000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.042189159327041%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146390813076934656\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"无人机业务信息\\\"\\n}\",\"size\":{\"width\":170.00000496341158,\"height\":60.00000129627779},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":558.9999910140915,\"x\":162.200469396823,\"h\":37.000003768959814,\"i\":\"f4665840-e58b-49bf-8ded-2ea4758037bb\",\"y\":13.733881618103084,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.3830451472595444%\",\"left\":\"7.955159%\",\"width\":\"27.416282000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.726016947298594%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146390813097906176\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":558.9999910140915,\"height\":37.000003768959814},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":617.0000068600939,\"x\":765.6154848633763,\"h\":71.99999957580766,\"i\":\"es-drager-1762931502489-3\",\"y\":533.076198288189,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"53.68245261341327%\",\"left\":\"37.549786000000005%\",\"width\":\"30.260906000000006%\",\"position\":\"absolute\",\"config\":{},\"height\":\"7.250626791827836%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":617.0000068600939,\"height\":71.99999957580766},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_18_1763552143794.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":540.9999931271748,\"x\":178.64135535481833,\"h\":65.99999548672854,\"i\":\"709d5eeb-9748-4a8f-893a-dec888551a01\",\"y\":75.12777806457078,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"7.565603939649218%\",\"left\":\"8.761506000000002%\",\"width\":\"26.533468000000006%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.646407477165907%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":540.9999931271748,\"height\":65.99999548672854},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_21_1763552566127.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":543.999999571442,\"x\":177.00929035487692,\"h\":98.00000409697937,\"i\":\"es-drager-1762942596197-2\",\"y\":158.32571975954107,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.943898781767137%\",\"left\":\"8.681461000000002%\",\"width\":\"26.680604000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.868909159598777%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":543.999999571442,\"height\":98.00000409697937},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_07_1763552117126.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":614.9999957674679,\"x\":765.4865222657797,\"h\":71.99999957580766,\"i\":\"es-drager-1762941743379-33\",\"y\":617.7772565987063,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"62.21211603425532%\",\"left\":\"37.54346100000001%\",\"width\":\"30.16281500000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"7.250626791827836%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":614.9999957674679,\"height\":71.99999957580766},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_18_1763552143794.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JSemiGauge\",\"visible\":true,\"w\":221,\"x\":754.6072684642438,\"h\":208,\"i\":\"50f215a7-2178-488b-b3de-6ade82567e05\",\"y\":260.17467686082085,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.20040963494764%\",\"left\":\"37.00988551705342%\",\"width\":\"10.838995383538858%\",\"position\":\"absolute\",\"config\":{},\"height\":\"20.94625529979765%\"},\"componentName\":\"半圆仪表盘\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataType\":1,\"h\":430,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"total\\\": 800,\\n \\\"used\\\": 500\\n }\\n]\",\"size\":{\"width\":221,\"height\":208},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":500,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"titleSuffix\":\"人\",\"customAttr\":{\"innerCircle\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":0,\"y2\":1,\"x2\":1,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#42A8FF66\"},{\"offset\":1,\"color\":\"#42A8FF66\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":15}},\"name\":\"内部小圆\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"outerScale\":{\"axisLabel\":{\"color\":\"#FFFFFF\",\"distance\":-52,\"show\":true,\"fontSize\":12},\"min\":0,\"max\":100,\"axisLine\":{\"show\":false},\"name\":\"外部刻度\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"splitNumber\":2,\"detail\":{\"show\":false},\"type\":\"gauge\",\"radius\":66},\"innerProgress\":{\"axisLabel\":{\"show\":false},\"animationDuration\":2000,\"pointer\":{\"show\":false,\"length\":74,\"width\":1,\"itemStyle\":{\"color\":\"#FFFFFF\"}},\"data\":[{\"name\":\"去年优良率\",\"value\":44}],\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#2E76B9\"],[1,\"#2E76B9\"]],\"width\":1}},\"name\":\"内部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"detail\":{\"offsetCenter\":[0,50],\"show\":false,\"textStyle\":{\"padding\":[0,0,0,0],\"color\":\"#FFFFFF\",\"fontSize\":18,\"fontWeight\":\"normal\"}},\"type\":\"gauge\",\"radius\":\"30%\",\"title\":{\"offsetCenter\":[0,26],\"show\":true,\"textStyle\":{\"color\":\"#FFFFFF\",\"fontSize\":16,\"fontWeight\":\"normal\"}}},\"outerProgress\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#2E76B9\"],[1,\"#2E76B9\"]],\"width\":2}},\"name\":\"外部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"basic\":{\"startAngle\":180,\"endAngle\":0},\"innerShadow\":{\"axisLabel\":{\"show\":false},\"customGradient\":{\"endColor\":\"#42A8FFCC\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#2E76B900\"},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":1,\"y2\":0,\"x2\":0,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#2de69600\"},{\"offset\":1,\"color\":\"#2de696\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":100}},\"name\":\"内部阴影\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":79}},\"valuePrefix\":\"已使用:\",\"titlePrefix\":\"总人数:\",\"valueMapping\":\"used\",\"titleMapping\":\"total\",\"valueSuffix\":\"辆\"}}},{\"component\":\"JSemiGauge\",\"visible\":true,\"w\":222,\"x\":969.7186400937863,\"h\":210,\"i\":\"es-drager-1763539498104-1\",\"y\":263.8440789710201,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.569929987821922%\",\"left\":\"47.56007177967479%\",\"width\":\"10.88804061151867%\",\"position\":\"absolute\",\"config\":{},\"height\":\"21.147661600757242%\"},\"componentName\":\"半圆仪表盘\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataType\":1,\"h\":430,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"total\\\": 800,\\n \\\"used\\\": 500\\n }\\n]\",\"size\":{\"width\":222,\"height\":210},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":500,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"titleSuffix\":\"人\",\"customAttr\":{\"innerCircle\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":0,\"y2\":1,\"x2\":1,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#32B9BA66\"},{\"offset\":1,\"color\":\"#32B9BA66\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":15}},\"name\":\"内部小圆\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"outerScale\":{\"axisLabel\":{\"color\":\"#FFFFFF\",\"distance\":-49,\"show\":true,\"fontSize\":12},\"min\":0,\"max\":100,\"axisLine\":{\"show\":false},\"name\":\"外部刻度\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"splitNumber\":2,\"detail\":{\"show\":false},\"type\":\"gauge\",\"radius\":66},\"innerProgress\":{\"axisLabel\":{\"show\":false},\"animationDuration\":2000,\"pointer\":{\"show\":false,\"length\":81,\"width\":1,\"itemStyle\":{\"color\":\"#FFFFFF\"}},\"data\":[{\"name\":\"去年优良率\",\"value\":44}],\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#32B9BA\"],[1,\"#32B9BA\"]],\"width\":1}},\"name\":\"内部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"detail\":{\"offsetCenter\":[0,50],\"show\":false,\"textStyle\":{\"padding\":[0,0,0,0],\"color\":\"#FFFFFF\",\"fontSize\":18,\"fontWeight\":\"normal\"}},\"type\":\"gauge\",\"radius\":\"30%\",\"title\":{\"offsetCenter\":[0,26],\"show\":true,\"textStyle\":{\"color\":\"#FFFFFF\",\"fontSize\":16,\"fontWeight\":\"normal\"}}},\"outerProgress\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#32B9BA\"],[1,\"#32B9BA\"]],\"width\":2}},\"name\":\"外部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"basic\":{\"startAngle\":180,\"endAngle\":0},\"innerShadow\":{\"axisLabel\":{\"show\":false},\"customGradient\":{\"endColor\":\"#32B9BACC\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#32B9BA00\"},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":1,\"y2\":0,\"x2\":0,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#2de69600\"},{\"offset\":1,\"color\":\"#2de696\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":100}},\"name\":\"内部阴影\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":79}},\"valuePrefix\":\"已使用:\",\"titlePrefix\":\"总人数:\",\"valueMapping\":\"used\",\"titleMapping\":\"total\",\"valueSuffix\":\"辆\"}}},{\"component\":\"JSemiGauge\",\"visible\":true,\"w\":222,\"x\":1181.0316529894487,\"h\":210,\"i\":\"es-drager-1763539599125-2\",\"y\":261.1828831914188,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.301939188772533%\",\"left\":\"57.9239666722439%\",\"width\":\"10.88804061151867%\",\"position\":\"absolute\",\"config\":{},\"height\":\"21.147661600757242%\"},\"componentName\":\"半圆仪表盘\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataType\":1,\"h\":430,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"total\\\": 800,\\n \\\"used\\\": 500\\n }\\n]\",\"size\":{\"width\":222,\"height\":210},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":500,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"titleSuffix\":\"人\",\"customAttr\":{\"innerCircle\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":0,\"y2\":1,\"x2\":1,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#4277FF66\"},{\"offset\":1,\"color\":\"#4277FF66\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":15}},\"name\":\"内部小圆\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"outerScale\":{\"axisLabel\":{\"color\":\"#FFFFFF\",\"distance\":-52,\"show\":true,\"fontSize\":12},\"min\":0,\"max\":100,\"axisLine\":{\"show\":false},\"name\":\"外部刻度\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"splitNumber\":2,\"detail\":{\"show\":false},\"type\":\"gauge\",\"radius\":66},\"innerProgress\":{\"axisLabel\":{\"show\":false},\"animationDuration\":2000,\"pointer\":{\"show\":false,\"length\":81,\"width\":1,\"itemStyle\":{\"color\":\"#FFFFFF\"}},\"data\":[{\"name\":\"去年优良率\",\"value\":44}],\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#2E76B9\"],[1,\"#2E76B9\"]],\"width\":1}},\"name\":\"内部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"detail\":{\"offsetCenter\":[0,50],\"show\":false,\"textStyle\":{\"padding\":[0,0,0,0],\"color\":\"#FFFFFF\",\"fontSize\":18,\"fontWeight\":\"normal\"}},\"type\":\"gauge\",\"radius\":\"30%\",\"title\":{\"offsetCenter\":[0,26],\"show\":true,\"textStyle\":{\"color\":\"#FFFFFF\",\"fontSize\":16,\"fontWeight\":\"normal\"}}},\"outerProgress\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#4277FF\"],[1,\"#4277FF\"]],\"width\":2}},\"name\":\"外部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"basic\":{\"startAngle\":180,\"endAngle\":0},\"innerShadow\":{\"axisLabel\":{\"show\":false},\"customGradient\":{\"endColor\":\"#4277FFCC\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#4277FF00\"},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":1,\"y2\":0,\"x2\":0,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#2de69600\"},{\"offset\":1,\"color\":\"#2de696\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":100}},\"name\":\"内部阴影\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":79}},\"valuePrefix\":\"已使用:\",\"titlePrefix\":\"总人数:\",\"valueMapping\":\"used\",\"titleMapping\":\"total\",\"valueSuffix\":\"辆\"}}}]},\"component\":\"JGroup\",\"w\":2038.934349355217,\"x\":-127,\"y\":76,\"componentName\":\"投资\",\"pageCompId\":\"1151112776903086080\",\"equalProportion\":false,\"key\":\"ad56ff31-2be7-4967-8ad8-22fb9249deac\",\"group\":true},{\"visible\":false,\"h\":988.6694020273344,\"i\":\"es-drager-1762421939532-37\",\"props\":{\"elements\":[{\"component\":\"JStatsSummary\",\"visible\":true,\"w\":713.0000000000001,\"x\":571.0199296600234,\"h\":129,\"i\":\"6ba699ad-92ee-4f14-b66c-7d2cbce6057e\",\"y\":68.20046885031215,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.898207703248671%\",\"left\":\"30.823751360096747%\",\"width\":\"38.48785931663358%\",\"position\":\"absolute\",\"config\":{},\"height\":\"13.047839827497103%\"},\"componentName\":\"统计概览(背景模式)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": \\\"1\\\",\\n \\\"name\\\": \\\"成本已支付金额\\\",\\n \\\"value\\\": 96790,\\n \\\"suffix\\\": \\\"万元\\\",\\n \\\"compareLabel\\\": \\\"同比\\\",\\n \\\"compareValue\\\": \\\"20%\\\",\\n \\\"compareState\\\": \\\"0\\\"\\n },\\n {\\n \\\"id\\\": \\\"2\\\",\\n \\\"name\\\": \\\"成本未付款金额\\\",\\n \\\"value\\\": 96.79,\\n \\\"suffix\\\": \\\"%\\\",\\n \\\"compareLabel\\\": \\\"同比\\\",\\n \\\"compareValue\\\": \\\"20%\\\",\\n \\\"compareState\\\": \\\"0\\\"\\n },\\n {\\n \\\"id\\\": \\\"3\\\",\\n \\\"name\\\": \\\"租赁应收未回金额\\\",\\n \\\"value\\\": 10790,\\n \\\"suffix\\\": \\\"元\\\",\\n \\\"compareLabel\\\": \\\"同比\\\",\\n \\\"compareValue\\\": \\\"20%\\\",\\n \\\"compareState\\\": \\\"1\\\"\\n },\\n {\\n \\\"id\\\": \\\"4\\\",\\n \\\"name\\\": \\\"租赁项目欠款金额\\\",\\n \\\"value\\\": 86790,\\n \\\"suffix\\\": \\\"元\\\",\\n \\\"compareLabel\\\": \\\"同比\\\",\\n \\\"compareValue\\\": \\\"20%\\\",\\n \\\"compareState\\\": \\\"1\\\"\\n }\\n]\",\"size\":{\"width\":713.0000000000001,\"height\":129},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":713,\"dataType\":1,\"h\":129,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"layout\":{\"padding\":{\"top\":5,\"left\":20,\"bottom\":0,\"right\":20},\"borderColor\":\"#0f66ff59\",\"borderRadius\":0,\"shadow\":\"none\",\"justify\":\"space-between\",\"borderWidth\":0,\"gap\":16,\"fill\":{\"image\":{\"size\":\"contain\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"drag/lib/img/bg01.png\"},\"color\":\"#0b2b63\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"image\"}},\"fieldMap\":{\"compareValue\":\"compareValue\",\"unit\":\"suffix\",\"negativeValue\":\"0\",\"compareState\":\"compareState\",\"label\":\"name\",\"value\":\"value\",\"positiveValue\":\"1\",\"compareLabel\":\"compareLabel\"},\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"card\":{\"padding\":{\"horizontal\":3,\"vertical\":15},\"borderColor\":\"#0F66FF59\",\"borderRadius\":0,\"shadow\":\"none\",\"borderWidth\":0,\"blur\":24,\"minWidth\":100,\"fill\":{\"image\":{\"size\":\"cover\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"\"},\"color\":\"#0B2B6300\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"none\"}},\"sections\":{\"middle\":{\"compare\":{\"valueStyle\":{\"positiveGradient\":{\"endColor\":\"#15f0c5\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#15f0c5\"},\"positiveColor\":\"#00FFAE\",\"fontSize\":14,\"negativeColor\":\"#FF0000\",\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"negativeGradient\":{\"endColor\":\"#D0021B\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#D0021B\"},\"fontColor\":\"#FFFFFF\"},\"alignItems\":\"center\",\"labelStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#CFEAFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ED3FF\"},\"fontColor\":\"#DADADA\"},\"label\":\"同比\"},\"paddingBottom\":10,\"show\":true,\"type\":\"compare\",\"align\":\"center\"},\"top\":{\"minHeight\":40,\"paddingBottom\":10,\"show\":true,\"paddingTop\":5,\"type\":\"value\",\"align\":\"center\",\"value\":{\"unit\":{\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#96F5F8\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#9ED3FF\"},\"fontWeight\":500,\"fontColor\":\"#9ED3FF\"},\"unitGap\":6,\"fontSize\":24,\"fontGradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#96F5F8\"},\"fontWeight\":600,\"fontColor\":\"#D8F1FF\"}},\"bottom\":{\"paddingBottom\":10,\"show\":true,\"label\":{\"fontSize\":14,\"fontColor\":\"#C9E6FF\"},\"type\":\"label\",\"align\":\"center\"}}}}},{\"component\":\"JImg\",\"visible\":true,\"w\":83.00000204530508,\"x\":1683.8100819494582,\"h\":51.999996548865546,\"i\":\"es-drager-1762851269890-2\",\"y\":81.32122205883473,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.22532%\",\"left\":\"90.89235%\",\"width\":\"4.480354000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.259593999999999%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":83.00000204530508,\"height\":51.999996548865546},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_29_1763551901494.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":83.00000204530508,\"x\":1530.6096120950147,\"h\":51.999996548865546,\"i\":\"es-drager-1762851253605-1\",\"y\":80.0551023625165,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.097257000000003%\",\"left\":\"82.62256300000001%\",\"width\":\"4.480354000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.259593999999999%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":83.00000204530508,\"height\":51.999996548865546},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_27_1763551895529.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"visible\":true,\"h\":43,\"i\":\"es-drager-1762849064798-20\",\"orderNum\":70,\"component\":\"JText\",\"w\":51,\"x\":1169.6975226315708,\"y\":432.5854492285337,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.754307389455725%\",\"left\":\"63.14046801410759%\",\"width\":\"2.752988534569863%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492799424990345%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":51.99998699681006,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2d91eb63-bada-4811-8c1b-65189dcf7599\"},{\"visible\":true,\"h\":43.000010455188466,\"i\":\"es-drager-1762849058217-19\",\"orderNum\":70,\"component\":\"JText\",\"w\":51.99998699681006,\"x\":1038.021084598434,\"y\":487.0285938030566,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"49.2610161500266%\",\"left\":\"56.03255185375025%\",\"width\":\"2.806968000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":51.99998699681005,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2d91eb63-bada-4811-8c1b-65189dcf7599\"},{\"component\":\"JText\",\"visible\":true,\"w\":51.99998699681006,\"x\":683.5075917950732,\"h\":43.000010455188466,\"i\":\"es-drager-1762849043828-18\",\"y\":489.56082449258037,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"49.51714126974116%\",\"left\":\"36.89585418633911%\",\"width\":\"2.806968000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":51.99998699681005,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2d91eb63-bada-4811-8c1b-65189dcf7599\"},{\"component\":\"JText\",\"visible\":true,\"w\":51.99998699681006,\"x\":937.9976478556124,\"h\":43.000010455188466,\"i\":\"es-drager-1762849029696-16\",\"y\":292.0461788601647,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"29.539315999999992%\",\"left\":\"50.63327000000002%\",\"width\":\"2.806968000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":51.99998699681005,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2d91eb63-bada-4811-8c1b-65189dcf7599\"},{\"component\":\"JText\",\"visible\":true,\"w\":101.00000181927524,\"x\":1150.971855353579,\"h\":46.99999878199271,\"i\":\"es-drager-1762849006795-15\",\"y\":409.787098507104,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"41.44834437748427%\",\"left\":\"62.129653360804035%\",\"width\":\"5.451997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"794,150\\\"\\n}\",\"size\":{\"width\":101.00000181927523,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to top\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":101.00000181927524,\"x\":1019.4243913572378,\"h\":46.99999878199271,\"i\":\"es-drager-1762849000354-14\",\"y\":465.62531457297723,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.096159102140774%\",\"left\":\"55.028699240536014%\",\"width\":\"5.451997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"994,150\\\"\\n}\",\"size\":{\"width\":101.00000181927523,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to top\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":101.00000181927524,\"x\":909.4009228742274,\"h\":46.99999878199271,\"i\":\"es-drager-1762848992281-13\",\"y\":268.2396227476817,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"27.131377%\",\"left\":\"49.089614000000005%\",\"width\":\"5.451997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"94,000\\\"\\n}\",\"size\":{\"width\":101.00000181927523,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to top\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":101.00000181927524,\"x\":659.9753814335057,\"h\":46.99999878199271,\"i\":\"es-drager-1762848979407-12\",\"y\":468.2865173043536,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.3653292338268%\",\"left\":\"35.62558153303556%\",\"width\":\"5.451997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"994,150\\\"\\n}\",\"size\":{\"width\":101.00000181927523,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to top\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":76.00000522075926,\"x\":1146.1007905389508,\"h\":43.00000056849445,\"i\":\"es-drager-1762848746940-11\",\"y\":490.1176839485176,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"49.57346540142718%\",\"left\":\"61.86671246696447%\",\"width\":\"4.102492999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.34928%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":76.00000522075925,\"height\":43.00000056849446},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"54cf0d91-5cd0-40b5-8034-95d55014f90e\"},{\"component\":\"JText\",\"visible\":true,\"w\":76.00000522075926,\"x\":1034.8112462126353,\"h\":43.00000056849445,\"i\":\"es-drager-1762848738842-10\",\"y\":540.8914319443187,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"54.70902920988389%\",\"left\":\"55.85928424053601%\",\"width\":\"4.102492999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.34928%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":76.00000522075925,\"height\":43.00000056849446},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"54cf0d91-5cd0-40b5-8034-95d55014f90e\"},{\"component\":\"JText\",\"visible\":true,\"w\":76.00000522075926,\"x\":926.053909410244,\"h\":43.00000056849445,\"i\":\"es-drager-1762848724480-9\",\"y\":354.9008064339704,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"35.896812999999995%\",\"left\":\"49.98854500000001%\",\"width\":\"4.102492999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.34928%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":76.00000522075925,\"height\":43.00000056849446},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"54cf0d91-5cd0-40b5-8034-95d55014f90e\"},{\"component\":\"JText\",\"visible\":true,\"w\":76.00000522075926,\"x\":675.3622355462353,\"h\":43.00000056849445,\"i\":\"es-drager-1762848716458-8\",\"y\":554.9477012273585,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"56.130765257769696%\",\"left\":\"36.45616649294621%\",\"width\":\"4.102492999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.34928%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":76.00000522075925,\"height\":43.00000056849446},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"54cf0d91-5cd0-40b5-8034-95d55014f90e\"},{\"component\":\"JImg\",\"visible\":true,\"w\":59.99999924656725,\"x\":932.8722283986059,\"h\":84.99999169692029,\"i\":\"es-drager-1762848356232-7\",\"y\":280.17468158185136,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"28.338560999999995%\",\"left\":\"50.35659900000001%\",\"width\":\"3.238810000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.597412999999998%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":59.99999924656725,\"height\":84.99999169692029},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":101.00000181927524,\"x\":789.2485271715286,\"h\":46.99999878199271,\"i\":\"es-drager-1762848336840-6\",\"y\":322.81171698679043,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"32.65112850917029%\",\"left\":\"42.603767573124884%\",\"width\":\"5.451997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"1,994,150\\\"\\n}\",\"size\":{\"width\":101.00000181927523,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to top\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":51.99998699681006,\"x\":811.5146364804583,\"h\":43.000010455188466,\"i\":\"es-drager-1762848322084-5\",\"y\":345.3521537580295,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"34.93100454508466%\",\"left\":\"43.805695879732%\",\"width\":\"2.806968000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":51.99998699681005,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2d91eb63-bada-4811-8c1b-65189dcf7599\"},{\"component\":\"JText\",\"visible\":true,\"w\":76.00000522075926,\"x\":803.3692688716295,\"h\":43.00000056849445,\"i\":\"es-drager-1762848304881-4\",\"y\":430.9969452788255,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.59363649719883%\",\"left\":\"43.36600757312489%\",\"width\":\"4.102492999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.34928%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目名称\\\"\\n}\",\"size\":{\"width\":76.00000522075925,\"height\":43.00000056849446},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"54cf0d91-5cd0-40b5-8034-95d55014f90e\"},{\"component\":\"JImg\",\"visible\":true,\"w\":64.00000537144585,\"x\":1033.0246263328966,\"h\":88.9999997971126,\"i\":\"es-drager-1762848210868-3\",\"y\":465.157090512513,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.048800090169316%\",\"left\":\"55.76284220044669%\",\"width\":\"3.454731000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.001997999999999%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":64.00000537144585,\"height\":88.9999997971126},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":61.99999304634536,\"x\":1164.7010515207064,\"h\":84.99999169692029,\"i\":\"es-drager-1762848205032-2\",\"y\":414.5123157421458,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"41.92628141339864%\",\"left\":\"62.870757667411134%\",\"width\":\"3.3467700000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.597412999999998%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":61.99999304634537,\"height\":84.99999169692029},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":70.00000529610256,\"x\":675.9789007962789,\"h\":91.9999984572363,\"i\":\"es-drager-1762848199690-1\",\"y\":474.01993004375464,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"47.94524125776971%\",\"left\":\"36.48945418633911%\",\"width\":\"3.7786120000000007%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.305436%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":70.00000529610256,\"height\":91.9999984572363},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":11.000002949424383,\"x\":247.82881673228633,\"h\":17.000002294061673,\"i\":\"es-drager-1762776290330-1\",\"y\":237.77959838698735,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.050465999999997%\",\"left\":\"13.377841000000002%\",\"width\":\"0.593782%\",\"position\":\"absolute\",\"config\":{},\"height\":\"1.719483%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":11.000002949424381,\"height\":17.000002294061673},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_17_1763552004773.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":131.9999983424479,\"x\":379.57794515961206,\"h\":35.99999380817846,\"i\":\"es-drager-1762775755103-5\",\"y\":100.65181473429544,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"10.180533%\",\"left\":\"20.489681000000004%\",\"width\":\"7.125381999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6412570000000004%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"超支合同总数\\\"\\n}\",\"size\":{\"width\":131.9999983424479,\"height\":35.99999380817846},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFD3D3\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":41.99999947259708,\"x\":394.9003504185903,\"h\":30.00000637462507,\"i\":\"es-drager-1762775743083-4\",\"y\":85.321220272333,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.629903999999998%\",\"left\":\"21.316787000000005%\",\"width\":\"2.267167000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.034382%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":41.99999947259708,\"height\":30.00000637462507},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":51.00000862224333,\"x\":373.37631562661164,\"h\":37.00000324824705,\"i\":\"es-drager-1762775732063-3\",\"y\":82.78898087969645,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.373777999999996%\",\"left\":\"20.154916000000004%\",\"width\":\"2.752989000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7424040000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"51\\\"\\n}\",\"size\":{\"width\":51.00000862224333,\"height\":37.00000324824705},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":0,\"fontSize\":26,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":131.9999983424479,\"x\":175.8616630329137,\"h\":35.99999380817846,\"i\":\"es-drager-1762775491330-2\",\"y\":102.04689646741414,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"10.321639999999999%\",\"left\":\"9.493042000000003%\",\"width\":\"7.125381999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6412570000000004%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同到期总数\\\"\\n}\",\"size\":{\"width\":131.9999983424479,\"height\":35.99999380817846},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#EFF1CA\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":113.99999856847775,\"x\":1682.1316877454951,\"h\":32.99999514805475,\"i\":\"es-drager-1762421807094-36\",\"y\":163.459559605057,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"16.533287999999995%\",\"left\":\"90.80175000000001%\",\"width\":\"6.153739000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.3378189999999996%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"总应收金额\\\"\\n}\",\"size\":{\"width\":113.99999856847775,\"height\":32.99999514805475},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#ACCDF2\",\"gradient\":{\"endColor\":\"#D7EDFF\",\"enabled\":true,\"startColor\":\"#4D699D\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2811e8ee-093c-4be8-8ae8-e06b1d2b3b0c\"},{\"component\":\"JText\",\"visible\":true,\"w\":51.00000862224333,\"x\":1736.1289403823714,\"h\":39.00000235499617,\"i\":\"es-drager-1762421803113-35\",\"y\":132.5240243353311,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"13.404280952114176%\",\"left\":\"93.71653073348223%\",\"width\":\"2.752989000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.944695999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":51.00000862224333,\"height\":39.00000235499617},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#A5BECF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"58cdf874-8a47-4b55-8c44-8bfc3eb806d9\"},{\"component\":\"JText\",\"visible\":true,\"w\":69.00000839621347,\"x\":1683.131684645384,\"h\":46.99999878199271,\"i\":\"es-drager-1762421795606-34\",\"y\":125.45955680343542,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.689738%\",\"left\":\"90.85573%\",\"width\":\"3.7246319999999993%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"3898\\\"\\n}\",\"size\":{\"width\":69.00000839621347,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":113.99999856847775,\"x\":1522.6872394711825,\"h\":32.99999514805475,\"i\":\"es-drager-1762421729992-33\",\"y\":163.459559605057,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"16.533287999999995%\",\"left\":\"82.194912%\",\"width\":\"6.153739000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.3378189999999996%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目累计欠款\\\"\\n}\",\"size\":{\"width\":113.99999856847775,\"height\":32.99999514805475},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#ACCDF2\",\"gradient\":{\"endColor\":\"#D7EDFF\",\"enabled\":true,\"startColor\":\"#4D699D\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2811e8ee-093c-4be8-8ae8-e06b1d2b3b0c\"},{\"component\":\"JText\",\"visible\":true,\"w\":51.00000862224333,\"x\":1567.9533708468402,\"h\":39.00000235499617,\"i\":\"es-drager-1762421726217-32\",\"y\":133.2579137508141,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"13.478510964085629%\",\"left\":\"84.63838534669647%\",\"width\":\"2.752989000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.944695999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":51.00000862224333,\"height\":39.00000235499617},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#A5BECF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"58cdf874-8a47-4b55-8c44-8bfc3eb806d9\"},{\"component\":\"JText\",\"visible\":true,\"w\":64.00000537144585,\"x\":1524.798348154353,\"h\":46.99999878199271,\"i\":\"es-drager-1762421719453-31\",\"y\":126.45955635680997,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.790883999999997%\",\"left\":\"82.30887%\",\"width\":\"3.454731000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"898\\\"\\n}\",\"size\":{\"width\":64.00000537144585,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FFC5AB\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#E86B6B\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":64.00000537144585,\"x\":1383.5322244068514,\"h\":46.99999878199271,\"i\":\"es-drager-1762421654488-30\",\"y\":125.45955680343542,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.689738%\",\"left\":\"74.68330100000001%\",\"width\":\"3.454731000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"198\\\"\\n}\",\"size\":{\"width\":64.00000537144585,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#6BE5E8\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"a04f824f-4454-4452-8bbf-651d4d5477c3\"},{\"component\":\"JText\",\"visible\":true,\"w\":51.00000862224333,\"x\":1420.8772050872835,\"h\":39.00000235499617,\"i\":\"es-drager-1762421637698-29\",\"y\":131.7462480034502,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"13.325611952114174%\",\"left\":\"76.69918930660712%\",\"width\":\"2.752989000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.944695999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":51.00000862224333,\"height\":39.00000235499617},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#A5BECF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"58cdf874-8a47-4b55-8c44-8bfc3eb806d9\"},{\"component\":\"JText\",\"visible\":true,\"w\":77.99999902053742,\"x\":1392.4094662453533,\"h\":32.99999514805475,\"i\":\"es-drager-1762421586808-28\",\"y\":164.68177249323125,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"16.656909999999996%\",\"left\":\"75.162496%\",\"width\":\"4.210453000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.3378189999999996%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"欠款数量\\\"\\n}\",\"size\":{\"width\":77.99999902053742,\"height\":32.99999514805475},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#ACCDF2\",\"gradient\":{\"endColor\":\"#D7EDFF\",\"enabled\":true,\"startColor\":\"#4D699D\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2811e8ee-093c-4be8-8ae8-e06b1d2b3b0c\"},{\"visible\":true,\"h\":43.000010455188466,\"i\":\"es-drager-1762421434219-27\",\"orderNum\":70,\"component\":\"JText\",\"w\":133.99999214222606,\"x\":1711.798361243923,\"y\":203.45956151342773,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"20.579129999999996%\",\"left\":\"92.40316200000001%\",\"width\":\"7.233342000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970187673600\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目/合同名称\\\"\\n}\",\"size\":{\"width\":133.99999214222606,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#A2C8F2\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"40d1d9a6-fa9d-4e2d-b25f-aa85c9c09609\"},{\"visible\":true,\"h\":43.000010455188466,\"i\":\"es-drager-1762420435770-26\",\"orderNum\":70,\"component\":\"JText\",\"w\":99.0000080194971,\"x\":1312.7983569915898,\"y\":198.45955385986088,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"20.073398999999995%\",\"left\":\"70.865075%\",\"width\":\"5.344037%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970187673600\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"费项明细\\\"\\n}\",\"size\":{\"width\":99.0000080194971,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"40d1d9a6-fa9d-4e2d-b25f-aa85c9c09609\"},{\"component\":\"JMultipleBar\",\"visible\":true,\"w\":514.9999966205892,\"x\":1324.7983383155806,\"h\":202.99999831528342,\"i\":\"0e6f2873-f014-44d9-92dc-fb5faf81406f\",\"y\":242.4595638684239,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.523825999999996%\",\"left\":\"71.512836%\",\"width\":\"27.799786000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"20.532647%\"},\"componentName\":\"对比柱形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"租金\\\",\\n \\\"value\\\": 910,\\n \\\"type\\\": \\\"应收\\\"\\n },\\n {\\n \\\"name\\\": \\\"水电\\\",\\n \\\"value\\\": 910,\\n \\\"type\\\": \\\"应收\\\"\\n },\\n {\\n \\\"name\\\": \\\"物业\\\",\\n \\\"value\\\": 960,\\n \\\"type\\\": \\\"应收\\\"\\n },\\n {\\n \\\"name\\\": \\\"租金\\\",\\n \\\"value\\\": 800,\\n \\\"type\\\": \\\"实收\\\"\\n },\\n {\\n \\\"name\\\": \\\"水电\\\",\\n \\\"value\\\": 700,\\n \\\"type\\\": \\\"实收\\\"\\n },\\n {\\n \\\"name\\\": \\\"物业\\\",\\n \\\"value\\\": 700,\\n \\\"type\\\": \\\"实收\\\"\\n },\\n {\\n \\\"name\\\": \\\"租金\\\",\\n \\\"value\\\": 480,\\n \\\"type\\\": \\\"欠款\\\"\\n },\\n {\\n \\\"name\\\": \\\"水电\\\",\\n \\\"value\\\": 230,\\n \\\"type\\\": \\\"欠款\\\"\\n },\\n {\\n \\\"name\\\": \\\"物业\\\",\\n \\\"value\\\": 400,\\n \\\"type\\\": \\\"欠款\\\"\\n }\\n]\",\"size\":{\"width\":514.9999966205892,\"height\":202.99999831528342},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"name\":\"单位(万元)\",\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#006CFF00\",\"color\":\"#006CFFB3\"},{\"color1\":\"#00D8FF00\",\"color\":\"#00D8FFB5\"},{\"color1\":\"#FFBB3800\",\"color\":\"#FFBB38B5\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":50,\"left\":24,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"barWidth\":15,\"barGap\":\"100%\",\"itemStyle\":{\"borderRadius\":7},\"label\":{\"color\":\"#4A90E2\",\"show\":true,\"position\":\"top\"}}],\"legend\":{},\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"visible\":true,\"h\":43.000010455188466,\"i\":\"es-drager-1762420262561-25\",\"orderNum\":70,\"component\":\"JText\",\"w\":99.0000080194971,\"x\":1318.7983569162466,\"y\":718.4595490085985,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"72.66934199999999%\",\"left\":\"71.18895600000002%\",\"width\":\"5.344037%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970187673600\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"欠款分析\\\"\\n}\",\"size\":{\"width\":99.0000080194971,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"40d1d9a6-fa9d-4e2d-b25f-aa85c9c09609\"},{\"component\":\"JScrollList\",\"visible\":true,\"w\":514.9999966205892,\"x\":1329.7983413403485,\"h\":220.00000060934508,\"i\":\"c01aa5d0-70b0-4813-b062-a2ee7a8589dd\",\"y\":764.4595383505226,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"77.32205899999998%\",\"left\":\"71.78273700000001%\",\"width\":\"27.799786000000005%\",\"position\":\"absolute\",\"config\":{},\"height\":\"22.25213%\"},\"componentName\":\"滚动列表(单行)\",\"config\":{\"chartData\":\"[{\\\"id\\\":1,\\\"projectName\\\":\\\"项目A\\\",\\\"officeFee\\\":1200,\\\"travelFee\\\":5600,\\\"arrearsAmount\\\":3000},{\\\"id\\\":2,\\\"projectName\\\":\\\"项目B\\\",\\\"officeFee\\\":800,\\\"travelFee\\\":4200,\\\"arrearsAmount\\\":0},{\\\"id\\\":3,\\\"projectName\\\":\\\"项目C\\\",\\\"officeFee\\\":1500,\\\"travelFee\\\":1800,\\\"arrearsAmount\\\":1200},{\\\"id\\\":4,\\\"projectName\\\":\\\"项目D\\\",\\\"officeFee\\\":600,\\\"travelFee\\\":2300,\\\"arrearsAmount\\\":900},{\\\"id\\\":5,\\\"projectName\\\":\\\"项目E\\\",\\\"officeFee\\\":950,\\\"travelFee\\\":3000,\\\"arrearsAmount\\\":150}]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":514.9999966205892,\"height\":220.00000060934508},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"timeOut\":0,\"option\":{\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"marginRight\":15,\"name\":\"项目名称\",\"width\":116,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"projectName\",\"marginLeft\":10},{\"marginRight\":1,\"textAlign\":\"left\",\"compose\":{\"contentStyle\":{\"marginRight\":4,\"fontSize\":15,\"fontGradient\":{\"endColor\":\"#038F8A\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#06CFC8\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\",\"marginLeft\":3},\"suffixStyle\":{\"fontColor\":\"#FFFFFF\"},\"prefix\":\"办公费\",\"prefixStyle\":{\"fontColor\":\"#DEDEDE\"},\"suffix\":\"元\",\"enabled\":true},\"name\":\"办公费\",\"width\":108,\"textStyle\":{\"fontColor\":\"#FFFFFF\"},\"key\":\"officeFee\"},{\"marginRight\":13,\"textAlign\":\"left\",\"compose\":{\"contentStyle\":{\"marginRight\":1,\"fontGradient\":{\"endColor\":\"#038F8A\",\"enabled\":true,\"startColor\":\"#06CFC8\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\",\"marginLeft\":1},\"suffixStyle\":{\"fontColor\":\"#FFFFFF\"},\"prefix\":\"差旅费\",\"prefixStyle\":{\"fontColor\":\"#DEDEDE\"},\"suffix\":\"元\",\"enabled\":true},\"name\":\"差旅费\",\"width\":111,\"textStyle\":{\"fontColor\":\"#FFFFFF\"},\"key\":\"travelFee\"},{\"textAlign\":\"left\",\"compose\":{\"contentStyle\":{\"marginRight\":2,\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#0DA183\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#12E3B9\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\",\"marginLeft\":3},\"suffixStyle\":{\"fontColor\":\"#FFFFFF\"},\"prefix\":\"欠款金额\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"suffix\":\"元\",\"enabled\":true},\"name\":\"欠款金额\",\"width\":122,\"textStyle\":{\"fontColor\":\"#DC3545\"},\"key\":\"arrearsAmount\"}],\"itemsPerRow\":1,\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"drag/lib/img/scrollList-bg-01.png\",\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"height\":44}}}},{\"visible\":true,\"h\":43.000010455188466,\"i\":\"es-drager-1762420151147-24\",\"orderNum\":70,\"component\":\"JText\",\"w\":99.0000080194971,\"x\":1319.128959818718,\"y\":444.6612017548762,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"44.97572199999999%\",\"left\":\"71.206802%\",\"width\":\"5.344037%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.3492809999999995%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970187673600\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"账期监控\\\"\\n}\",\"size\":{\"width\":99.0000080194971,\"height\":43.000010455188466},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"40d1d9a6-fa9d-4e2d-b25f-aa85c9c09609\"},{\"component\":\"JPermanentCalendar\",\"visible\":true,\"w\":477.0000032728707,\"x\":1347.6529914352425,\"h\":219.0000010559705,\"i\":\"da53da9d-fbc3-45f8-8f66-da05f0b73aad\",\"y\":493.1934304894369,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"49.884564999999995%\",\"left\":\"72.74653400000001%\",\"width\":\"25.748540000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"22.150983999999998%\"},\"componentName\":\"万日历\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[],\"dataType\":1,\"h\":480,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"date\\\": \\\"2025-11-05\\\",\\n \\\"value\\\": 620000\\n },\\n {\\n \\\"date\\\": \\\"2025-11-08\\\",\\n \\\"value\\\": 265000\\n },\\n {\\n \\\"date\\\": \\\"2025-11-10\\\",\\n \\\"value\\\": 564000\\n },\\n {\\n \\\"date\\\": \\\"2025-11-14\\\",\\n \\\"value\\\": 120000\\n },\\n {\\n \\\"date\\\": \\\"2025-11-15\\\",\\n \\\"value\\\": 565000\\n },\\n {\\n \\\"date\\\": \\\"2025-11-20\\\",\\n \\\"value\\\": 120000\\n },\\n {\\n \\\"date\\\": \\\"2025-11-24\\\",\\n \\\"value\\\": 102000\\n },\\n {\\n \\\"date\\\": \\\"2025-11-25\\\",\\n \\\"value\\\": 120\\n },\\n {\\n \\\"date\\\": \\\"2025-11-28\\\",\\n \\\"value\\\": 103\\n }\\n]\",\"size\":{\"width\":477.0000032728707,\"height\":219.0000010559705},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":1000,\"linkageConfig\":[],\"option\":{\"container\":{\"border\":{\"color\":\"#2B6CB0\",\"width\":0,\"style\":\"solid\",\"enabled\":true},\"padding\":{\"top\":5,\"left\":4,\"bottom\":8,\"right\":0},\"margin\":{\"bottom\":0},\"background\":{\"color\":\"#00000000\",\"gradient\":{\"endColor\":\"#0A1E3A\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to bottom\",\"startColor\":\"#0B2B58\"}}},\"dataVal\":{\"offsetX\":-2,\"offsetY\":-15,\"color\":{\"gradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"value\":\"#D0021B\"},\"fontSize\":14,\"position\":\"top\"},\"week\":{\"color\":{\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"value\":\"#FFFFFF\"},\"prefix\":\"周\",\"start\":\"sun\",\"fontSize\":21,\"marginBottom\":21,\"showEn\":false,\"fontWeight\":\"bold\",\"height\":22},\"month\":{\"offsetTop\":-11,\"color\":{\"gradient\":{\"endColor\":\"#ffffff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom\",\"startColor\":\"#ffffff\"},\"value\":\"#ffffff\"},\"show\":true,\"en\":{\"color\":{\"value\":\"#FFFFFF\"},\"fontSize\":29,\"opacity\":1},\"offsetLeft\":0,\"cn\":{\"color\":{\"gradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"value\":\"#FFFFFF\"},\"fontSize\":70,\"opacity\":0.11,\"fontStyle\":\"italic\"},\"position\":\"center\",\"showEn\":false},\"field\":{\"unit\":\"万\",\"appendUnit\":true,\"dateField\":\"date\",\"valueField\":\"value\"},\"title\":{\"color\":{\"gradient\":{\"endColor\":\"#3bc6ff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#9fe5ff\"},\"value\":\"#9fe5ff\"},\"show\":true},\"cell\":{\"width\":120,\"day\":{\"color\":{\"value\":\"#FFFFFF\"},\"fontSize\":14},\"height\":25},\"circle\":{\"dashed\":false,\"strokeWidth\":4,\"glowStrength\":0.45,\"fillOpacity\":0.16,\"size\":37,\"minIntensity\":0.55,\"pulse\":true,\"doubleRing\":true,\"glowEnabled\":true,\"enabled\":true}}}},{\"component\":\"JMultipleBar\",\"visible\":true,\"w\":756.0000090320697,\"x\":546.7983604353072,\"h\":283.000002132025,\"i\":\"c55fc027-14ba-4db5-81ea-a7ec69526940\",\"y\":705.6693998953094,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"71.375669%\",\"left\":\"29.516267000000003%\",\"width\":\"40.809007000000015%\",\"position\":\"absolute\",\"config\":{},\"height\":\"28.62433100000001%\"},\"componentName\":\"对比柱形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 325,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 465,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 305,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 105,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 256,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 600,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"7月\\\",\\n \\\"value\\\": 500,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"8月\\\",\\n \\\"value\\\": 132,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"9月\\\",\\n \\\"value\\\": 156,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"10月\\\",\\n \\\"value\\\": 213,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"11月\\\",\\n \\\"value\\\": 356,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"12月\\\",\\n \\\"value\\\": 113,\\n \\\"type\\\": \\\"成本支出金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 50,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 450,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 365,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 355,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 49,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 60,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"7月\\\",\\n \\\"value\\\": 117,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"8月\\\",\\n \\\"value\\\": 229,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"9月\\\",\\n \\\"value\\\": 119,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"10月\\\",\\n \\\"value\\\": 103,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"11月\\\",\\n \\\"value\\\": 90,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"12月\\\",\\n \\\"value\\\": 143,\\n \\\"type\\\": \\\"租赁收入金额\\\"\\n }\\n]\",\"size\":{\"width\":756.0000090320697,\"height\":283.000002132025},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"name\":\"单位(万元)\",\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#00A8FF\",\"color\":\"#0F3352\"},{\"color1\":\"#15DBCB\",\"color\":\"#0F3352\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":35,\"left\":1,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"barWidth\":12,\"itemStyle\":{\"borderRadius\":0},\"label\":{\"color\":\"#EEF1FA\"}}],\"legend\":{\"r\":1,\"t\":1},\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"visible\":true,\"h\":59.9999929758621,\"i\":\"es-drager-1762419089506-23\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.00001021548871,\"x\":572.3306080699609,\"y\":650.5322268067889,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"65.798762%\",\"left\":\"30.894502000000006%\",\"width\":\"9.176629%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.0687619999999995%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970187673600\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"资金流动趋势\\\"\\n}\",\"size\":{\"width\":170.00001021548871,\"height\":59.9999929758621},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"40d1d9a6-fa9d-4e2d-b25f-aa85c9c09609\"},{\"visible\":true,\"h\":35.99999380817846,\"i\":\"es-drager-1762419081801-22\",\"orderNum\":70,\"component\":\"JImg\",\"w\":725.9999908834636,\"x\":555.7983510596312,\"y\":664.266120184477,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"67.18789099999998%\",\"left\":\"30.002088000000004%\",\"width\":\"39.18960100000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6412570000000004%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146300970204450816\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":725.9999908834637,\"height\":35.99999380817846},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"d05d8636-fe01-4048-8a21-99851c7012b1\"},{\"visible\":true,\"h\":37.00000324824705,\"i\":\"es-drager-1762419024578-19\",\"orderNum\":70,\"component\":\"JText\",\"w\":134.99998904211512,\"x\":551.209851019992,\"y\":210.79835561773643,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"21.321419999999993%\",\"left\":\"29.754400000000004%\",\"width\":\"7.287322000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7424040000000005%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970175090688\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目欠款分布\\\"\\n}\",\"size\":{\"width\":134.99998904211512,\"height\":37.00000324824705},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"e190480f-f18e-453c-88fe-e109292f6fdf\"},{\"visible\":true,\"h\":346.9999933213852,\"i\":\"f8220f2d-63b9-471c-a776-57a0d72f73c2\",\"orderNum\":70,\"component\":\"JListProgress\",\"w\":533.9999932944485,\"x\":13.72565215056514,\"y\":623.9917724037379,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.11429999999999%\",\"left\":\"0.7409130000000002%\",\"width\":\"28.825409000000008%\",\"position\":\"absolute\",\"config\":{},\"height\":\"35.09767699999999%\"},\"componentName\":\"列表进度图\",\"pageCompId\":\"1146300970011512832\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"2025年度战略合作框架协议\\\",\\n \\\"total\\\": 1200000,\\n \\\"date\\\": \\\"2025-12-31\\\",\\n \\\"endLabel\\\": \\\"2025-06-15\\\",\\n \\\"value\\\": 800000\\n },\\n {\\n \\\"title\\\": \\\"智能制造设备采购合同\\\",\\n \\\"total\\\": 850000,\\n \\\"date\\\": \\\"2025-11-20\\\",\\n \\\"endLabel\\\": \\\"2025-05-30\\\",\\n \\\"value\\\": 500000\\n },\\n {\\n \\\"title\\\": \\\"信息化系统集成服务合同\\\",\\n \\\"total\\\": 2000000,\\n \\\"date\\\": \\\"2026-01-15\\\",\\n \\\"endLabel\\\": \\\"2025-07-01\\\",\\n \\\"value\\\": 1500000\\n },\\n {\\n \\\"title\\\": \\\"2025年技术支持与维护协议\\\",\\n \\\"total\\\": 600000,\\n \\\"date\\\": \\\"2025-10-10\\\",\\n \\\"endLabel\\\": \\\"2025-04-28\\\",\\n \\\"value\\\": 300000\\n },\\n {\\n \\\"title\\\": \\\"大数据平台建设合同\\\",\\n \\\"total\\\": 1750000,\\n \\\"date\\\": \\\"2025-09-25\\\",\\n \\\"endLabel\\\": \\\"2025-05-10\\\",\\n \\\"value\\\": 1200000\\n },\\n {\\n \\\"title\\\": \\\"云服务采购框架协议\\\",\\n \\\"total\\\": 950000,\\n \\\"date\\\": \\\"2025-08-31\\\",\\n \\\"endLabel\\\": \\\"2025-06-01\\\",\\n \\\"value\\\": 700000\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":533.9999932944485,\"height\":346.99999332138515},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":1200,\"dataType\":1,\"h\":325,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"beginFields\":[{\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"name\":\"合同名称\",\"style\":{\"letterSpacing\":0,\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\"},{\"compose\":{\"contentStyle\":{\"marginRight\":1,\"fontSize\":12,\"fontGradient\":{\"endColor\":\"#78F7FA\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#78F7FA\",\"fontWeight\":\"bold\",\"marginLeft\":0},\"suffixStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#78F7FA\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#78F7FA\",\"fontWeight\":\"bold\"},\"prefix\":\"\",\"prefixStyle\":{\"fontSize\":14,\"fontColor\":\"#666666\"},\"suffix\":\"万元\",\"enabled\":true},\"name\":\"总额\",\"width\":100,\"style\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#FF4500\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFD700\",\"direction\":\"to bottom\"},\"fontColor\":\"#FFFFFF\"},\"key\":\"total\"}],\"endCurrent\":0,\"endInfo\":{\"width\":103},\"scroll\":{\"count\":1,\"interval\":3000,\"enabled\":true,\"direction\":\"down\"},\"centerTopFields\":[{\"marginRight\":0,\"name\":\"最近日期\",\"style\":{\"letterSpacing\":0,\"fontSize\":12,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"italic\",\"fontColor\":\"#888888\",\"fontWeight\":\"normal\"},\"key\":\"endLabel\",\"marginLeft\":0},{\"marginRight\":0,\"isUseExceedFillColor\":true,\"compose\":{\"contentStyle\":{\"marginRight\":0,\"fontColor\":\"#6EEDF3\",\"marginLeft\":0},\"suffixStyle\":{\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontColor\":\"#6EEDF3\"},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"suffix\":\"\",\"enabled\":true},\"showPercentage\":true,\"name\":\"进度值\",\"width\":100,\"style\":{\"fontSize\":12,\"fontGradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#00D4FF\",\"direction\":\"to bottom\"},\"fontColor\":\"#6EEDF3\"},\"key\":\"value\",\"marginLeft\":0}],\"body\":{\"gradient\":{\"type\":\"linear\"}},\"endFields\":[{\"name\":\"到期日期\",\"style\":{\"letterSpacing\":0,\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"date\"}],\"beginCurrent\":1,\"progressSection\":{\"marginRight\":0,\"marginLeft\":0},\"bar\":{\"border\":{\"padding\":8,\"color\":\"#4ECBFC21\",\"width\":2,\"enabled\":true},\"total\":{\"field\":\"total\",\"type\":\"field\",\"value\":0},\"borderRadius\":8,\"background\":{\"color\":\"#5A97FC4F\",\"gradient\":{\"endColor\":\"#0066CC\",\"enabled\":false,\"direction\":\"to bottom\",\"startColor\":\"#FFFFFF\"}},\"indicatorColor\":\"#02F7FFA3\",\"exceed\":{\"indicatorColor\":\"#FEF8C9A1\",\"fill\":{\"color\":\"#FFB347\",\"gradient\":{\"endColor\":\"#FEAF24\",\"enabled\":true,\"startColor\":\"#FEF6C8\",\"direction\":\"to right\"}},\"percent\":70,\"enabled\":true},\"indicatorSize\":10,\"fill\":{\"color\":\"#33C9FF\",\"gradient\":{\"endColor\":\"#24E5F1\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#C5FDFE\"}},\"valueField\":\"value\",\"height\":3},\"centerTopInfo\":{\"layout\":\"horizontal\"},\"centerTopCurrent\":1,\"row\":{\"marginRight\":0,\"padding\":\"0 0\",\"marginBottom\":0,\"marginTop\":12,\"height\":31,\"marginLeft\":9},\"beginInfo\":{\"layout\":\"vertical\",\"width\":176}}},\"key\":\"a436dea0-cd2d-4a65-aa6b-426cde282aec\"},{\"visible\":true,\"h\":304.99999230626537,\"i\":\"8cc8a3c3-0948-4171-b8d4-209129a39128\",\"orderNum\":70,\"component\":\"JCardScroll\",\"w\":531.9999994946703,\"x\":4.733886769359984,\"y\":279.3786642995031,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"28.258046999999998%\",\"left\":\"0.2555360000000001%\",\"width\":\"28.717449000000006%\",\"position\":\"absolute\",\"config\":{},\"height\":\"30.849542999999997%\"},\"componentName\":\"卡片滚动(高亮)\",\"pageCompId\":\"1146300970061844480\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[],\"dataType\":1,\"h\":304,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"projectName\\\": \\\"苏州地铁5号线工程\\\",\\n \\\"status\\\": \\\"一期\\\",\\n \\\"paymentMethod\\\": \\\"分期\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786720503390209\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"南京软件园研发楼\\\",\\n \\\"status\\\": \\\"二期\\\",\\n \\\"paymentMethod\\\": \\\"一次性付款\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786777713696769\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"重庆智慧交通枢纽\\\",\\n \\\"status\\\": \\\"三期\\\",\\n \\\"paymentMethod\\\": \\\"分期\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786804406247425\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"武汉光谷科技园\\\",\\n \\\"status\\\": \\\"一期\\\",\\n \\\"paymentMethod\\\": \\\"分期\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786837256036353\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"上海浦东新区道路改造\\\",\\n \\\"status\\\": \\\"二期\\\",\\n \\\"paymentMethod\\\": \\\"一次性付款\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\",\\n \\\"道路工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786864602898433\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"深圳南山创新中心\\\",\\n \\\"status\\\": \\\"三期\\\",\\n \\\"paymentMethod\\\": \\\"分期\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\",\\n \\\"道路工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786897117143041\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"成都高新产业园\\\",\\n \\\"status\\\": \\\"一期\\\",\\n \\\"paymentMethod\\\": \\\"分期\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786931179085826\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"杭州滨江智慧社区\\\",\\n \\\"status\\\": \\\"二期\\\",\\n \\\"paymentMethod\\\": \\\"一次性付款\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966786962128855042\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"广州南沙港区扩建\\\",\\n \\\"status\\\": \\\"三期\\\",\\n \\\"paymentMethod\\\": \\\"分期\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966787000406073346\\\"\\n },\\n {\\n \\\"projectName\\\": \\\"天津生态城住宅项目\\\",\\n \\\"status\\\": \\\"一期\\\",\\n \\\"paymentMethod\\\": \\\"分期\\\",\\n \\\"type\\\": [\\n \\\"建筑工程\\\",\\n \\\"市政工程\\\"\\n ],\\n \\\"id\\\": \\\"1966787029329993729\\\"\\n }\\n]\",\"size\":{\"width\":531.9999994946703,\"height\":304.99999230626537},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":540,\"linkageConfig\":[],\"option\":{\"showIndex\":true,\"autoScrollEnabled\":true,\"rowGap\":16,\"indexFieldStyle\":{},\"contentFieldMapping\":[{\"valueStyle\":{\"fontSize\":14,\"fontColor\":\"#A6D8FF\",\"fontWeight\":\"normal\"},\"itemConfig\":{\"alignItems\":\"flex-start\",\"marginBottom\":24,\"marginTop\":13,\"height\":40},\"nameStyle\":{\"fontColor\":\"#FFFFFF\"},\"omitConfig\":{\"show\":true,\"lines\":2},\"name\":\"项目名称\",\"width\":100,\"key\":\"projectName\",\"showLabel\":false},{\"valueStyle\":{\"fontSize\":16,\"marginBottom\":10,\"fontColor\":\"#FFFFFF\",\"marginTop\":-5},\"itemConfig\":{\"alignItems\":\"flex-start\",\"justifyContent\":\"center\"},\"nameStyle\":{\"fontColor\":\"#FFFFFF\"},\"name\":\"进度\",\"width\":100,\"key\":\"status\",\"showLabel\":false},{\"valueStyle\":{\"fontSize\":12,\"marginBottom\":0,\"fontColor\":\"#BFBFBF\",\"marginTop\":-10},\"itemConfig\":{\"alignItems\":\"center\",\"layoutDirection\":\"row\",\"justifyContent\":\"center\"},\"nameStyle\":{\"fontColor\":\"#FFFFFF\"},\"name\":\"付款方式\",\"width\":100,\"key\":\"paymentMethod\",\"showLabel\":false},{\"valueStyle\":{\"fontSize\":14,\"fontColor\":\"#FFFFFF\"},\"itemConfig\":{\"alignItems\":\"center\",\"layoutDirection\":\"column\",\"justifyContent\":\"flex-start\"},\"nameStyle\":{\"fontSize\":14,\"marginBottom\":10,\"fontColor\":\"#FFFFFF\",\"marginTop\":26},\"valueType\":\"array\",\"name\":\"合同类型\",\"width\":100,\"key\":\"type\"}],\"autoScrollSpeed\":100,\"scrollDirection\":\"left\",\"animationDuration\":800,\"columnGap\":16,\"stayDuration\":5000,\"contentCurrent\":1,\"cardStyle\":{\"backgroundColor\":\"#0648786E\",\"borderColor\":\"#1890FF\",\"backgroundImage\":\"https://static.ghb.com/jimureport/images/组-1121_05_1757733370432.png\",\"paddingRight\":5,\"borderEnabled\":false,\"paddingBottom\":5,\"borderRadius\":0,\"borderWidth\":1,\"width\":122,\"paddingTop\":5,\"bgHighlightImage\":\"https://static.ghb.com/jimureport/images/组-1121_03_1757735281389.png\",\"borderStyle\":\"dashed\",\"paddingLeft\":5,\"height\":300},\"currentValue\":0,\"direction\":\"horizontal\"}},\"key\":\"cb43a6dd-2191-428e-8d28-101517716e82\"},{\"visible\":true,\"h\":52.99999610224011,\"i\":\"874a6164-d6b8-400c-8d6c-7224b8d8fa73\",\"orderNum\":70,\"component\":\"JText\",\"w\":95.0000018946185,\"x\":269.38688254140277,\"y\":179.11254927232653,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.116525999999997%\",\"left\":\"14.541549000000003%\",\"width\":\"5.128116%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.360739999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970103787520\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"详情>\\\"\\n}\",\"size\":{\"width\":95.00000189461852,\"height\":52.99999610224012},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"setModalCited\":true,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#5A868B\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#36363600\",\"backgroundImage\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/产城详情_032_1763553721746.png\",\"backgroundSize\":\"100% 100%\",\"targetCompId\":\"es-drager-1762409324858-6\",\"backgroundPosition\":\"center center\",\"title\":\"\",\"sizeMode\":\"fit\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F00\"},\"openType\":\"_blank\"}},\"key\":\"42c54784-6b0f-4d44-84f0-2c8a63af9b79\"},{\"visible\":true,\"h\":46.99999878199271,\"i\":\"es-drager-1762399836630-5\",\"orderNum\":70,\"component\":\"JText\",\"w\":67.99999297100209,\"x\":251.18523463376653,\"y\":182.11254793245024,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.419963999999997%\",\"left\":\"13.559021000000001%\",\"width\":\"3.6706510000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970124759040\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":67.99999297100209,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#5A868B\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"fe69c43c-3bfb-4983-8a75-a3d98c2b240d\"},{\"visible\":true,\"h\":52.99999610224011,\"i\":\"39150764-32e5-464a-8d07-abca92992b3e\",\"orderNum\":70,\"component\":\"JText\",\"w\":94.00000499472947,\"x\":198.65299660081877,\"y\":176.58030987969,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"17.860399999999995%\",\"left\":\"10.723322%\",\"width\":\"5.074136000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.360739999999999%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970141536256\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"6,790\\\"\\n}\",\"size\":{\"width\":94.00000499472947,\"height\":52.99999610224012},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":24,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"1b524244-da11-4aa4-a0af-a750ed3fe210\"},{\"visible\":true,\"h\":46.99999878199271,\"i\":\"8f386c60-44f0-4bf0-85e1-dfc3ebbc975f\",\"orderNum\":70,\"component\":\"JText\",\"w\":109.99999244359917,\"x\":125.18521769065302,\"y\":179.5803085398137,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.163837999999995%\",\"left\":\"6.757519000000002%\",\"width\":\"5.937818000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.753863999999998%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970154119168\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目总数\\\"\\n}\",\"size\":{\"width\":109.99999244359917,\"height\":46.99999878199271},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"546303e1-9281-41dd-88d7-56a2175baf6a\"},{\"visible\":true,\"h\":37.00000324824705,\"i\":\"es-drager-1762399377808-4\",\"orderNum\":70,\"component\":\"JText\",\"w\":114.99999546836682,\"x\":12.20164798297948,\"y\":93.395080190355,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.446542999999998%\",\"left\":\"0.6586470000000001%\",\"width\":\"6.207719000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7424040000000005%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970175090688\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同预警\\\"\\n}\",\"size\":{\"width\":114.99999546836682,\"height\":37.00000324824705},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4a9caef5-fe02-44c4-9191-9e069c016e1f\"},{\"visible\":true,\"h\":59.9999929758621,\"i\":\"es-drager-1762398785719-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.00001021548871,\"x\":1319.7983538161357,\"y\":1.9999991067491294,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0.20229199999999942%\",\"left\":\"71.24293600000001%\",\"width\":\"9.176629%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.0687619999999995%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970187673600\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"租赁系统数据\\\"\\n}\",\"size\":{\"width\":170.00001021548871,\"height\":59.9999929758621},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"40d1d9a6-fa9d-4e2d-b25f-aa85c9c09609\"},{\"visible\":true,\"h\":35.99999380817846,\"i\":\"es-drager-1762398759885-2\",\"orderNum\":70,\"component\":\"JImg\",\"w\":537.0000025194379,\"x\":1315.532231435849,\"y\":17.266122437005365,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.7463999999999997%\",\"left\":\"71.01265%\",\"width\":\"28.98735%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.6412570000000004%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146300970204450816\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":537.0000025194379,\"height\":35.99999380817846},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"d05d8636-fe01-4048-8a21-99851c7012b1\"},{\"visible\":true,\"h\":59.9999929758621,\"i\":\"68f75009-8c50-4375-8985-6f36fcbcbace\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.00001021548871,\"x\":4.20163573322229,\"y\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0%\",\"left\":\"0.22680500000000006%\",\"width\":\"9.176629%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.0687619999999995%\"},\"componentName\":\"文本\",\"pageCompId\":\"1146300970217033728\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"成本系统数据\\\"\\n}\",\"size\":{\"width\":170.00001021548871,\"height\":59.9999929758621},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2859886c-0e76-46eb-b86b-64326ad27bbf\"},{\"visible\":true,\"h\":35.000004141497904,\"i\":\"bf455d73-9694-488e-897d-ab5c552a2e95\",\"orderNum\":70,\"component\":\"JImg\",\"w\":543.0000024440947,\"x\":0,\"y\":15.733882597743403,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.5914199999999996%\",\"left\":\"0%\",\"width\":\"29.311231000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.5401119999999993%\"},\"componentName\":\"图片\",\"pageCompId\":\"1146300970233810944\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":543.0000024440947,\"height\":35.000004141497904},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"c77e6615-da1a-4c2e-8e3b-2bb9002c9184\"},{\"component\":\"JText\",\"visible\":true,\"w\":41.000002572708,\"x\":194.9824077577821,\"h\":29.99999648793105,\"i\":\"a3e3e41c-44ed-4d59-bb93-372d8d63fb1f\",\"y\":89.24855128478228,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.027138%\",\"left\":\"10.525183%\",\"width\":\"2.213187%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.0343809999999998%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":41.000002572708,\"height\":29.99999648793105},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":149.9999981164181,\"x\":321.2637687712664,\"h\":99.99999488423282,\"i\":\"es-drager-1762774351674-1\",\"y\":67.59554452387727,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.837021999999997%\",\"left\":\"17.341872%\",\"width\":\"8.097025000000002%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.114603999999998%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":149.9999981164181,\"height\":99.99999488423282},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_41_1763551881751.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":51.00000862224333,\"x\":170.92614665521,\"h\":37.00000324824705,\"i\":\"f1f7a03d-01ce-4ee5-bcc4-06a169d1d2bd\",\"y\":84.18405272612117,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.514883999999999%\",\"left\":\"9.226622%\",\"width\":\"2.752989000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7424040000000005%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"76\\\"\\n}\",\"size\":{\"width\":51.00000862224333,\"height\":37.00000324824705},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":0,\"fontSize\":26,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":167.99999789038833,\"x\":104.88628099031132,\"h\":104.00000298442514,\"i\":\"2ea9f177-88fc-49ce-a2e7-0a9089f41e0d\",\"y\":66.45838686435945,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.722002999999997%\",\"left\":\"5.661779000000001%\",\"width\":\"9.068668000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.519188999999999%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":167.99999789038833,\"height\":104.00000298442514},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_39_1763551870530.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":538.9999963192163,\"x\":12.66118712893443,\"h\":35.000004141497904,\"i\":\"3f286895-ab97-45e7-9384-527089c0afd6\",\"y\":186.73973824120662,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.887985999999998%\",\"left\":\"0.6834530000000001%\",\"width\":\"29.095310000000012%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.5401119999999993%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":538.9999963192163,\"height\":35.000004141497904},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_13_1763551964951.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":11.999999849313447,\"x\":248.95778692630336,\"h\":18.000001847436234,\"i\":\"cea81bd3-6d33-41da-8393-fec1e1ba11f9\",\"y\":594.9542752387625,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"60.177272%\",\"left\":\"13.438783%\",\"width\":\"0.6477620000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"1.8206289999999996%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":11.999999849313447,\"height\":18.000001847436234},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_17_1763552004773.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":89.99999886985086,\"x\":796.3892117165844,\"h\":124.99999360529105,\"i\":\"82435ec8-26f7-4c2b-8a28-dd8d034db75f\",\"y\":315.88394276806093,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"31.950411545084663%\",\"left\":\"42.98922291982134%\",\"width\":\"4.858215%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.643255%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":89.99999886985086,\"height\":124.99999360529107},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":894.0000072991742,\"x\":487.45604718243874,\"h\":603.9999971789773,\"i\":\"1b53a65a-6907-45bd-83d7-7d55d7048d91\",\"y\":152.55450644061344,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.430284999999996%\",\"left\":\"26.312959000000003%\",\"width\":\"48.25827%\",\"position\":\"absolute\",\"config\":{},\"height\":\"61.092210999999985%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":894.0000072991742,\"height\":603.9999971789773},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/产城背景地图_1763551428302.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":83.00000204530508,\"x\":1376.2719838540795,\"h\":51.999996548865546,\"i\":\"00c0b354-a82e-40dd-b1c2-ef2e1d6d0a09\",\"y\":80.18405451262291,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.1103%\",\"left\":\"74.29139200000002%\",\"width\":\"4.480354000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.259593999999999%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":83.00000204530508,\"height\":51.999996548865546},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_25_1763551889963.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}}]},\"component\":\"JGroup\",\"w\":1852.532233955287,\"x\":22,\"y\":74,\"componentName\":\"新成\",\"pageCompId\":\"1151112776924057600\",\"equalProportion\":false,\"key\":\"f9b7b1c7-f98e-4b8a-b271-bab6cb7ffb0d\",\"group\":true},{\"visible\":false,\"h\":982.5087924970692,\"i\":\"es-drager-1762409324858-6\",\"props\":{\"elements\":[{\"component\":\"JText\",\"visible\":true,\"w\":170,\"x\":763.4701055099648,\"h\":60,\"i\":\"86652dd5-ea38-431d-9a83-83fa934fcf0a\",\"y\":0,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0%\",\"left\":\"44.883604086417684%\",\"width\":\"9.994121105232216%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.106815578465063%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目列表\\\"\\n}\",\"size\":{\"width\":170,\"height\":60},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":2,\"fontSize\":26,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JScrollTable\",\"visible\":true,\"w\":1701,\"x\":0,\"h\":892,\"i\":\"ebb593ac-a3cb-430c-855a-9024de4329ad\",\"y\":90.50879249706912,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.212008400152726%\",\"left\":\"0%\",\"width\":\"100%\",\"position\":\"absolute\",\"config\":{},\"height\":\"90.78799159984727%\"},\"componentName\":\"详情弹框表格\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"施工合同\\\",\\n \\\"htdate\\\": \\\"2024-10-11\\\",\\n \\\"htmoney\\\": \\\"1200000\\\",\\n \\\"ljbgje\\\": \\\"50000\\\",\\n \\\"dthtje\\\": \\\"1250000\\\",\\n \\\"ljsfje\\\": \\\"100000\\\",\\n \\\"ljwfje\\\": \\\"2500000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"二期\\\",\\n \\\"htname\\\": \\\"装修合同\\\",\\n \\\"htdate\\\": \\\"2024-11-05\\\",\\n \\\"htmoney\\\": \\\"980000\\\",\\n \\\"ljbgje\\\": \\\"30000\\\",\\n \\\"dthtje\\\": \\\"1010000\\\",\\n \\\"ljsfje\\\": \\\"80000\\\",\\n \\\"ljwfje\\\": \\\"2100000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"三期\\\",\\n \\\"htname\\\": \\\"消防合同\\\",\\n \\\"htdate\\\": \\\"2024-12-01\\\",\\n \\\"htmoney\\\": \\\"760000\\\",\\n \\\"ljbgje\\\": \\\"20000\\\",\\n \\\"dthtje\\\": \\\"780000\\\",\\n \\\"ljsfje\\\": \\\"60000\\\",\\n \\\"ljwfje\\\": \\\"1800000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"智能化合同\\\",\\n \\\"htdate\\\": \\\"2025-01-10\\\",\\n \\\"htmoney\\\": \\\"540000\\\",\\n \\\"ljbgje\\\": \\\"15000\\\",\\n \\\"dthtje\\\": \\\"555000\\\",\\n \\\"ljsfje\\\": \\\"45000\\\",\\n \\\"ljwfje\\\": \\\"1200000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"二期\\\",\\n \\\"htname\\\": \\\"景观合同\\\",\\n \\\"htdate\\\": \\\"2025-02-15\\\",\\n \\\"htmoney\\\": \\\"420000\\\",\\n \\\"ljbgje\\\": \\\"10000\\\",\\n \\\"dthtje\\\": \\\"430000\\\",\\n \\\"ljsfje\\\": \\\"35000\\\",\\n \\\"ljwfje\\\": \\\"950000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"三期\\\",\\n \\\"htname\\\": \\\"幕墙合同\\\",\\n \\\"htdate\\\": \\\"2025-03-20\\\",\\n \\\"htmoney\\\": \\\"880000\\\",\\n \\\"ljbgje\\\": \\\"25000\\\",\\n \\\"dthtje\\\": \\\"905000\\\",\\n \\\"ljsfje\\\": \\\"70000\\\",\\n \\\"ljwfje\\\": \\\"1600000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"暖通合同\\\",\\n \\\"htdate\\\": \\\"2025-04-08\\\",\\n \\\"htmoney\\\": \\\"650000\\\",\\n \\\"ljbgje\\\": \\\"18000\\\",\\n \\\"dthtje\\\": \\\"668000\\\",\\n \\\"ljsfje\\\": \\\"52000\\\",\\n \\\"ljwfje\\\": \\\"1350000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"二期\\\",\\n \\\"htname\\\": \\\"电梯合同\\\",\\n \\\"htdate\\\": \\\"2025-05-12\\\",\\n \\\"htmoney\\\": \\\"720000\\\",\\n \\\"ljbgje\\\": \\\"22000\\\",\\n \\\"dthtje\\\": \\\"742000\\\",\\n \\\"ljsfje\\\": \\\"58000\\\",\\n \\\"ljwfje\\\": \\\"1500000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"三期\\\",\\n \\\"htname\\\": \\\"厨房设备合同\\\",\\n \\\"htdate\\\": \\\"2025-06-05\\\",\\n \\\"htmoney\\\": \\\"380000\\\",\\n \\\"ljbgje\\\": \\\"8000\\\",\\n \\\"dthtje\\\": \\\"388000\\\",\\n \\\"ljsfje\\\": \\\"30000\\\",\\n \\\"ljwfje\\\": \\\"800000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"软装合同\\\",\\n \\\"htdate\\\": \\\"2025-07-01\\\",\\n \\\"htmoney\\\": \\\"560000\\\",\\n \\\"ljbgje\\\": \\\"12000\\\",\\n \\\"dthtje\\\": \\\"572000\\\",\\n \\\"ljsfje\\\": \\\"48000\\\",\\n \\\"ljwfje\\\": \\\"1150000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"施工合同\\\",\\n \\\"htdate\\\": \\\"2024-10-11\\\",\\n \\\"htmoney\\\": \\\"1200000\\\",\\n \\\"ljbgje\\\": \\\"50000\\\",\\n \\\"dthtje\\\": \\\"1250000\\\",\\n \\\"ljsfje\\\": \\\"100000\\\",\\n \\\"ljwfje\\\": \\\"2500000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"二期\\\",\\n \\\"htname\\\": \\\"装修合同\\\",\\n \\\"htdate\\\": \\\"2024-11-05\\\",\\n \\\"htmoney\\\": \\\"980000\\\",\\n \\\"ljbgje\\\": \\\"30000\\\",\\n \\\"dthtje\\\": \\\"1010000\\\",\\n \\\"ljsfje\\\": \\\"80000\\\",\\n \\\"ljwfje\\\": \\\"2100000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"三期\\\",\\n \\\"htname\\\": \\\"消防合同\\\",\\n \\\"htdate\\\": \\\"2024-12-01\\\",\\n \\\"htmoney\\\": \\\"760000\\\",\\n \\\"ljbgje\\\": \\\"20000\\\",\\n \\\"dthtje\\\": \\\"780000\\\",\\n \\\"ljsfje\\\": \\\"60000\\\",\\n \\\"ljwfje\\\": \\\"1800000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"智能化合同\\\",\\n \\\"htdate\\\": \\\"2025-01-10\\\",\\n \\\"htmoney\\\": \\\"540000\\\",\\n \\\"ljbgje\\\": \\\"15000\\\",\\n \\\"dthtje\\\": \\\"555000\\\",\\n \\\"ljsfje\\\": \\\"45000\\\",\\n \\\"ljwfje\\\": \\\"1200000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"二期\\\",\\n \\\"htname\\\": \\\"景观合同\\\",\\n \\\"htdate\\\": \\\"2025-02-15\\\",\\n \\\"htmoney\\\": \\\"420000\\\",\\n \\\"ljbgje\\\": \\\"10000\\\",\\n \\\"dthtje\\\": \\\"430000\\\",\\n \\\"ljsfje\\\": \\\"35000\\\",\\n \\\"ljwfje\\\": \\\"950000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"三期\\\",\\n \\\"htname\\\": \\\"幕墙合同\\\",\\n \\\"htdate\\\": \\\"2025-03-20\\\",\\n \\\"htmoney\\\": \\\"880000\\\",\\n \\\"ljbgje\\\": \\\"25000\\\",\\n \\\"dthtje\\\": \\\"905000\\\",\\n \\\"ljsfje\\\": \\\"70000\\\",\\n \\\"ljwfje\\\": \\\"1600000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"暖通合同\\\",\\n \\\"htdate\\\": \\\"2025-04-08\\\",\\n \\\"htmoney\\\": \\\"650000\\\",\\n \\\"ljbgje\\\": \\\"18000\\\",\\n \\\"dthtje\\\": \\\"668000\\\",\\n \\\"ljsfje\\\": \\\"52000\\\",\\n \\\"ljwfje\\\": \\\"1350000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"二期\\\",\\n \\\"htname\\\": \\\"电梯合同\\\",\\n \\\"htdate\\\": \\\"2025-05-12\\\",\\n \\\"htmoney\\\": \\\"720000\\\",\\n \\\"ljbgje\\\": \\\"22000\\\",\\n \\\"dthtje\\\": \\\"742000\\\",\\n \\\"ljsfje\\\": \\\"58000\\\",\\n \\\"ljwfje\\\": \\\"1500000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"三期\\\",\\n \\\"htname\\\": \\\"厨房设备合同\\\",\\n \\\"htdate\\\": \\\"2025-06-05\\\",\\n \\\"htmoney\\\": \\\"380000\\\",\\n \\\"ljbgje\\\": \\\"8000\\\",\\n \\\"dthtje\\\": \\\"388000\\\",\\n \\\"ljsfje\\\": \\\"30000\\\",\\n \\\"ljwfje\\\": \\\"800000\\\"\\n },\\n {\\n \\\"name\\\": \\\"怡悦湾酒店\\\",\\n \\\"fqname\\\": \\\"一期\\\",\\n \\\"htname\\\": \\\"软装合同\\\",\\n \\\"htdate\\\": \\\"2025-07-01\\\",\\n \\\"htmoney\\\": \\\"560000\\\",\\n \\\"ljbgje\\\": \\\"12000\\\",\\n \\\"dthtje\\\": \\\"572000\\\",\\n \\\"ljsfje\\\": \\\"48000\\\",\\n \\\"ljwfje\\\": \\\"1150000\\\"\\n }\\n]\",\"size\":{\"width\":1701,\"height\":892},\"syncColumn\":false,\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"headerBgColor\":\"#003B6F\",\"borderColor\":\"#FFFFFF\",\"textPosition\":\"center\",\"scroll\":true,\"scrollTime\":50,\"bodyFontSize\":20,\"bodyFontColor\":\"#FFFFFF\",\"rankingTitle\":\"#\",\"showBorder\":false,\"oddColor\":\"#001E3C\",\"evenColor\":\"#00284E\",\"fieldMapping\":[{\"name\":\"项目名称\",\"width\":200,\"key\":\"name\"},{\"name\":\"分期名称\",\"width\":200,\"key\":\"fqname\"},{\"name\":\"合同日期\",\"width\":200,\"key\":\"htdate\"},{\"name\":\"合同金额(元)\",\"width\":200,\"key\":\"htmoney\"},{\"name\":\"累计变更金额\",\"width\":200,\"key\":\"ljbgje\"},{\"name\":\"动态合同金额\",\"width\":200,\"key\":\"dthtje\"},{\"name\":\"累计实付金额\",\"width\":200,\"key\":\"ljsfje\"},{\"name\":\"累计未付金额\",\"width\":200,\"key\":\"ljwfje\"}],\"showHead\":true,\"borderWidth\":1,\"ranking\":false,\"lineHeight\":50,\"fontSize\":24,\"borderStyle\":\"solid\",\"headerFontColor\":\"#FFFFFF\"}}}]},\"modalCited\":\"874a6164-d6b8-400c-8d6c-7224b8d8fa73\",\"component\":\"JGroup\",\"w\":1701,\"x\":99,\"y\":34.49120750293087,\"componentName\":\"详情弹框\",\"pageCompId\":\"1151112776945029120\",\"equalProportion\":false,\"key\":\"3e57537d-5911-41a4-8cfa-db9b2dc711c6\",\"group\":true},{\"visible\":false,\"h\":950.9395927008758,\"i\":\"es-drager-1756456982092-28\",\"props\":{\"elements\":[{\"component\":\"JBar\",\"visible\":true,\"w\":541,\"x\":1308.9728600943263,\"h\":190,\"i\":\"84e41e0f-f14b-44d6-8e0a-fdf0b1390398\",\"y\":279.80457035966094,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"29.424011000000007%\",\"left\":\"69.27569900000002%\",\"width\":\"28.631726677892527%\",\"position\":\"absolute\",\"config\":{},\"height\":\"19.980238645901636%\"},\"componentName\":\"基础柱形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 90\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 79\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 70\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 55\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 78\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 98\\n }\\n]\",\"size\":{\"width\":541.058706079854,\"height\":190.48343252800288},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"lineStyle\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#4A90E235\"},\"show\":true,\"interval\":2},\"name\":\"单位(个)\",\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":30,\"left\":16,\"bottom\":18,\"show\":false,\"right\":9,\"containLabel\":true},\"series\":[{\"barWidth\":17,\"data\":[],\"showBackground\":false,\"backgroundStyle\":{\"color\":\"#51626E\"},\"itemStyle\":{\"color\":\"#00A8FFA6\",\"borderRadius\":3},\"label\":{\"position\":\"top\"},\"type\":\"bar\"}],\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#475580\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JStatsSummary\",\"visible\":true,\"w\":681,\"x\":580.0384926431407,\"h\":129,\"i\":\"be3108cf-9334-449a-8795-ef8fb1e69164\",\"y\":54.525238831863746,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.733827810975898%\",\"left\":\"30.697788510194417%\",\"width\":\"36.041045966071735%\",\"position\":\"absolute\",\"config\":{},\"height\":\"13.565530449059532%\"},\"componentName\":\"统计概览(背景模式)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": \\\"1\\\",\\n \\\"name\\\": \\\"在管项目总数\\\",\\n \\\"value\\\": 960,\\n \\\"suffix\\\": \\\"个\\\"\\n },\\n {\\n \\\"id\\\": \\\"2\\\",\\n \\\"name\\\": \\\"累计放款金额\\\",\\n \\\"value\\\": 790,\\n \\\"suffix\\\": \\\"万元\\\"\\n },\\n {\\n \\\"id\\\": \\\"3\\\",\\n \\\"name\\\": \\\"当前借款余额\\\",\\n \\\"value\\\": 900,\\n \\\"suffix\\\": \\\"万元\\\"\\n },\\n {\\n \\\"id\\\": \\\"4\\\",\\n \\\"name\\\": \\\"总应收租金\\\",\\n \\\"value\\\": 6790,\\n \\\"suffix\\\": \\\"万元\\\"\\n },\\n {\\n \\\"id\\\": \\\"5\\\",\\n \\\"name\\\": \\\"租金实收率\\\",\\n \\\"value\\\": 90,\\n \\\"suffix\\\": \\\"%\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":680.9999999999999,\"height\":129},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":713,\"dataType\":1,\"h\":129,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"layout\":{\"padding\":{\"top\":5,\"left\":20,\"bottom\":0,\"right\":20},\"borderColor\":\"#0f66ff59\",\"borderRadius\":0,\"shadow\":\"none\",\"justify\":\"space-between\",\"borderWidth\":0,\"gap\":16,\"fill\":{\"image\":{\"size\":\"100% 100%\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"drag/lib/img/bg01.png\"},\"color\":\"#0b2b63\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"image\"}},\"fieldMap\":{\"compareValue\":\"compareValue\",\"unit\":\"suffix\",\"negativeValue\":\"0\",\"compareState\":\"compareState\",\"label\":\"name\",\"value\":\"value\",\"positiveValue\":\"1\",\"compareLabel\":\"compareLabel\"},\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"card\":{\"padding\":{\"horizontal\":3,\"vertical\":15},\"borderColor\":\"#0F66FF59\",\"borderRadius\":0,\"shadow\":\"none\",\"borderWidth\":0,\"blur\":24,\"minWidth\":100,\"fill\":{\"image\":{\"size\":\"cover\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"\"},\"color\":\"#0B2B6300\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"none\"}},\"sections\":{\"middle\":{\"compare\":{\"valueStyle\":{\"positiveGradient\":{\"endColor\":\"#15f0c5\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#15f0c5\"},\"positiveColor\":\"#15F0C5\",\"fontSize\":14,\"negativeColor\":\"#D0021B\",\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"negativeGradient\":{\"endColor\":\"#D0021B\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#D0021B\"},\"fontColor\":\"#FFFFFF\"},\"alignItems\":\"center\",\"labelStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"fontColor\":\"#9ED3FF\"},\"label\":\"同比\"},\"paddingBottom\":10,\"show\":false,\"type\":\"compare\",\"align\":\"center\"},\"top\":{\"minHeight\":40,\"paddingBottom\":10,\"show\":true,\"paddingTop\":5,\"type\":\"value\",\"align\":\"center\",\"value\":{\"unit\":{\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#96F5F8\"},\"fontWeight\":500,\"fontColor\":\"#9ED3FF\"},\"unitGap\":6,\"fontSize\":24,\"fontGradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#96F5F8\"},\"fontWeight\":600,\"fontColor\":\"#D8F1FF\"}},\"bottom\":{\"paddingBottom\":10,\"show\":true,\"label\":{\"fontSize\":14,\"fontColor\":\"#C9E6FF\"},\"type\":\"label\",\"align\":\"center\"}}}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":1092.016430295897,\"h\":27.97398018640021,\"i\":\"es-drager-1763118212824-19\",\"y\":506.6564308568187,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"53.27955999999999%\",\"left\":\"57.79356%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"客户名称\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":1205.967192459297,\"h\":27.97398018640021,\"i\":\"es-drager-1763118197556-18\",\"y\":430.68925214089586,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.29091600000001%\",\"left\":\"63.824257%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"客户名称\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":985.6623767923331,\"h\":27.97398018640021,\"i\":\"es-drager-1763118191675-17\",\"y\":323.06908863293563,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"33.97367100000001%\",\"left\":\"52.164909%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"客户名称\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1095.814765730692,\"h\":29.97211347737514,\"i\":\"es-drager-1763118021837-16\",\"y\":430.68924263149984,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.290915%\",\"left\":\"57.99458200000001%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1216.0961310407038,\"h\":29.97211347737514,\"i\":\"es-drager-1763118010715-15\",\"y\":378.7783346397803,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"39.83200800000001%\",\"left\":\"64.360318%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":995.79131537374,\"h\":29.97211347737514,\"i\":\"es-drager-1763118003690-14\",\"y\":267.3598140979031,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"28.115331000000005%\",\"left\":\"52.700970000000005%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1088.346997390856,\"h\":39.00000114161981,\"i\":\"es-drager-1763117996097-13\",\"y\":405.4958098141571,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.64159500000001%\",\"left\":\"57.59936%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"96340\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1211.1606115175618,\"h\":39.00000114161981,\"i\":\"es-drager-1763117990004-12\",\"y\":349.78654478852053,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"36.78325600000001%\",\"left\":\"64.099112%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"11960\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":990.855795850598,\"h\":39.00000114161981,\"i\":\"es-drager-1763117985668-11\",\"y\":243.43250029186595,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"25.599155000000007%\",\"left\":\"52.439764%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"91260\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":691.9226477632122,\"h\":27.97398018640021,\"i\":\"es-drager-1763117979572-10\",\"y\":496.5274692569776,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"52.21440700000001%\",\"left\":\"36.619113%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"客户名称\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":700.7854619362721,\"h\":29.97211347737514,\"i\":\"es-drager-1763117971569-9\",\"y\":416.76192399774175,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"43.82633%\",\"left\":\"37.088166%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":695.8499424131302,\"h\":39.00000114161981,\"i\":\"es-drager-1763117964448-8\",\"y\":391.56849118039895,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"41.17701000000001%\",\"left\":\"36.82696%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"9160\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":83.00000257528725,\"x\":688.6400925035406,\"h\":115.9999953491752,\"i\":\"es-drager-1763117932887-7\",\"y\":383.5890619640866,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"40.337900000000005%\",\"left\":\"36.445388%\",\"width\":\"4.392668000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.198461000000002%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":83.00000257528727,\"height\":115.9999953491752},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":70.0000067249707,\"x\":1091.0082054226732,\"h\":95.99999549522116,\"i\":\"es-drager-1763117918967-6\",\"y\":408.65354737265926,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.97366000000001%\",\"left\":\"57.740201%\",\"width\":\"3.70466%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.095278000000004%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":70.0000067249707,\"height\":95.99999549522116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":62.00000055779603,\"x\":1213.9507598693788,\"h\":83.00000462407712,\"i\":\"es-drager-1763117907587-5\",\"y\":351.80712025388317,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"36.995738%\",\"left\":\"64.246777%\",\"width\":\"3.28127%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.72821%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":62.00000055779603,\"height\":83.0000046240771},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":62.00000055779603,\"x\":993.7749034175382,\"h\":83.00000462407712,\"i\":\"es-drager-1763117887178-4\",\"y\":245.58203267539477,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"25.825198000000004%\",\"left\":\"52.59425399999999%\",\"width\":\"3.28127%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.72821%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":62.00000055779603,\"height\":83.0000046240771},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":835.1230982678547,\"h\":29.97211347737514,\"i\":\"es-drager-1763117551885-3\",\"y\":278.8838421178201,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"29.327188%\",\"left\":\"44.197812%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":831.4536842579369,\"h\":39.00000114161981,\"i\":\"es-drager-1763117542406-2\",\"y\":249.8920522665603,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.278436%\",\"left\":\"44.003613%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"199,445\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":830.058638424713,\"h\":27.97398018640021,\"i\":\"es-drager-1763117524939-1\",\"y\":363.71387293167453,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"38.247842000000006%\",\"left\":\"43.929782%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"客户名称\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":98.99999601451351,\"x\":817.9132500968582,\"h\":131.00000237168769,\"i\":\"01adf6f2-8586-42bf-8ca5-e68125185724\",\"y\":236.8481470050254,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.906750000000002%\",\"left\":\"43.287003%\",\"width\":\"5.239447%\",\"position\":\"absolute\",\"config\":{},\"height\":\"13.775849000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":98.9999960145135,\"height\":131.00000237168769},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_44_1763551574907.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":173.99999242262612,\"x\":592.259300662204,\"h\":37.00000400904318,\"i\":\"es-drager-1763014748783-1\",\"y\":163.07150518786855,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"17.148461000000005%\",\"left\":\"31.344559%\",\"width\":\"9.208725000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.890889%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"采购合同金额分布\\\"\\n}\",\"size\":{\"width\":173.9999924226261,\"height\":37.00000400904318},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"79b612d5-4915-4201-84ce-a34c4f9b4615\"},{\"component\":\"JText\",\"visible\":true,\"w\":128.9999907987339,\"x\":1694.4314250338105,\"h\":26.99999932736819,\"i\":\"es-drager-1762929346632-5\",\"y\":724.1710725496224,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.15321500000002%\",\"left\":\"89.675596%\",\"width\":\"6.827158%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"36个月期项目数量\\\"\\n}\",\"size\":{\"width\":128.9999907987339,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#8D8D8D\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":46.00000711856976,\"x\":1732.8018837686052,\"h\":28.999996459944818,\"i\":\"es-drager-1762929340450-4\",\"y\":695.437187025299,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"73.13158400000002%\",\"left\":\"91.706303%\",\"width\":\"2.434491%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.0496150000000006%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":46.00000711856976,\"height\":28.999996459944818},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#505050\",\"letterSpacing\":0,\"fontSize\":13,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1685.8264915007508,\"h\":39.00000114161981,\"i\":\"es-drager-1762929333594-3\",\"y\":688.9776350506045,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"72.452303%\",\"left\":\"89.220191%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"400\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#6BE5E8\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":166.999991750129,\"x\":1684.6625708132242,\"h\":37.00000400904318,\"i\":\"es-drager-1762929318489-2\",\"y\":659.1324815182508,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"69.313812%\",\"left\":\"89.158592%\",\"width\":\"8.838259%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.890889%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"36个月期项目占比\\\"\\n}\",\"size\":{\"width\":166.999991750129,\"height\":37.00000400904318},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"79b612d5-4915-4201-84ce-a34c4f9b4615\"},{\"component\":\"JRingProgress\",\"visible\":true,\"w\":112.99999735950763,\"x\":1590.1172323342091,\"h\":99.000000703482,\"i\":\"es-drager-1762929307141-1\",\"y\":657.0708920282549,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"69.097017%\",\"left\":\"84.154902%\",\"width\":\"5.980378999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.410756000000001%\"},\"componentName\":\"基础环形图\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":200,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"占比\\\",\\n \\\"value\\\": 40\\n }\\n]\",\"size\":{\"width\":112.99999735950763,\"height\":99.000000703482},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":300,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"valueFontWeight\":\"normal\",\"color\":\"#1E90FF\",\"bgColor\":\"#E8EDF3B8\",\"valueFontSize\":16,\"lineHeight\":0,\"fontSize\":16,\"radius\":0.9,\"innerRadius\":0.9,\"valueFontColor\":\"#FFFFFF\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\",\"extraInfo\":{\"endColor\":\"#FF4500\",\"enabledGradient\":false,\"type\":\"linear\",\"direction\":\"to left\",\"startColor\":\"#FFD700\"}}}},{\"component\":\"JText\",\"visible\":true,\"w\":128.9999907987339,\"x\":1426.143030016014,\"h\":26.99999932736819,\"i\":\"es-drager-1762928981087-4\",\"y\":721.7677914451772,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"75.90048800000001%\",\"left\":\"75.476779%\",\"width\":\"6.827158%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"高收益项目数量\\\"\\n}\",\"size\":{\"width\":128.9999907987339,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#8D8D8D\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":46.00000711856976,\"x\":1472.110197410645,\"h\":28.999996459944818,\"i\":\"es-drager-1762928898594-3\",\"y\":693.0339059208537,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"72.87885700000001%\",\"left\":\"77.909532%\",\"width\":\"2.434491%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.0496150000000006%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":46.00000711856976,\"height\":28.999996459944818},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#505050\",\"letterSpacing\":0,\"fontSize\":13,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1425.1348240379136,\"h\":39.00000114161981,\"i\":\"es-drager-1762928768350-2\",\"y\":686.5743539461594,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"72.19957600000001%\",\"left\":\"75.423421%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"600\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#6BE5E8\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#49ABFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":22,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":149.0000062166706,\"x\":1423.970903350387,\"h\":37.00000400904318,\"i\":\"es-drager-1762928758538-1\",\"y\":656.7291909044097,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"69.06108400000001%\",\"left\":\"75.361822%\",\"width\":\"7.885633%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.890889%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"高收益项目占比\\\"\\n}\",\"size\":{\"width\":149.0000062166706,\"height\":37.00000400904318},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"79b612d5-4915-4201-84ce-a34c4f9b4615\"},{\"component\":\"JRingProgress\",\"visible\":true,\"w\":101.99999360342322,\"x\":1325.6271916463306,\"h\":103.00000447803119,\"i\":\"627c8b9b-8365-4edd-ba69-abfd0086e195\",\"y\":649.6031348785871,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"68.311714%\",\"left\":\"70.157108%\",\"width\":\"5.398218%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.831393000000002%\"},\"componentName\":\"基础环形图\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":200,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"占比\\\",\\n \\\"value\\\": 60\\n }\\n]\",\"size\":{\"width\":101.99999360342322,\"height\":103.00000447803119},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":300,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"valueFontWeight\":\"normal\",\"color\":\"#1E90FF\",\"bgColor\":\"#E8EDF3C0\",\"valueFontSize\":16,\"lineHeight\":0,\"fontSize\":16,\"radius\":0.9,\"innerRadius\":0.9,\"valueFontColor\":\"#FFFFFF\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\",\"extraInfo\":{\"endColor\":\"#FF4500\",\"enabledGradient\":false,\"type\":\"linear\",\"direction\":\"to left\",\"startColor\":\"#FFD700\"}}}},{\"component\":\"JText\",\"visible\":true,\"w\":115.99999494841742,\"x\":1684.1734894470342,\"h\":26.000000761079882,\"i\":\"es-drager-1762926696188-10\",\"y\":882.3070586104344,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"92.782661%\",\"left\":\"89.132708%\",\"width\":\"6.1391500000000025%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.734138000000001%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"零保证金项目数\\\"\\n}\",\"size\":{\"width\":115.99999494841741,\"height\":26.000000761079882},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":98.00000941495898,\"x\":1548.6987068173507,\"h\":26.99999932736819,\"i\":\"es-drager-1762926691124-9\",\"y\":883.5731776217401,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"92.91580500000002%\",\"left\":\"81.96288%\",\"width\":\"5.186524%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"保证金覆盖率\\\"\\n}\",\"size\":{\"width\":98.000009414959,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1732.5439653383592,\"h\":29.97211347737514,\"i\":\"es-drager-1762926686158-8\",\"y\":845.847502100111,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"88.94860500000001%\",\"left\":\"91.692653%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#BA3232\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FDAE93\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1598.4642474370226,\"h\":29.97211347737514,\"i\":\"es-drager-1762926681277-7\",\"y\":848.5086970408884,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"89.228454%\",\"left\":\"84.596657%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"%\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#BA3232\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FDAE93\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1693.5521782708327,\"h\":39.00000114161981,\"i\":\"es-drager-1762926674612-6\",\"y\":839.774820879915,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"88.310007%\",\"left\":\"89.629063%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"790\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FDAE93\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#BA3232\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1546.6822948611493,\"h\":39.00000114161981,\"i\":\"es-drager-1762926667152-5\",\"y\":842.3070589025264,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"88.57629500000002%\",\"left\":\"81.856164%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"96.79\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FDAE93\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#BA3232\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1425.1348240379136,\"h\":29.97211347737514,\"i\":\"es-drager-1762926619855-4\",\"y\":849.9037729703601,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"89.37515900000001%\",\"left\":\"75.423421%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#BA3232\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FDAE93\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1358.1594352472453,\"h\":39.00000114161981,\"i\":\"es-drager-1762926610924-3\",\"y\":843.7021348319981,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"88.72300000000001%\",\"left\":\"71.878835%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"10,000\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#BA3232\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FDAE93\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":98.00000941495898,\"x\":1362.7080771250178,\"h\":26.99999932736819,\"i\":\"es-drager-1762926602282-2\",\"y\":882.4360155286006,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"92.796222%\",\"left\":\"72.119566%\",\"width\":\"5.186524%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"最大还款差额\\\"\\n}\",\"size\":{\"width\":98.000009414959,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":531.0000078248536,\"x\":1315.369293849801,\"h\":95.99999549522116,\"i\":\"es-drager-1762926555663-1\",\"y\":822.9325585844631,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"86.53888900000001%\",\"left\":\"69.614222%\",\"width\":\"28.102490000000003%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.095278000000004%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":531.0000078248536,\"height\":95.99999549522116},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_35_1763551508136.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":149.0000062166706,\"x\":1494.8735867909734,\"h\":37.00000400904318,\"i\":\"es-drager-1762926336471-2\",\"y\":785.873387113962,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"82.64177800000002%\",\"left\":\"79.114255%\",\"width\":\"7.885633%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.890889%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"风险预警\\\"\\n}\",\"size\":{\"width\":149.0000062166706,\"height\":37.00000400904318},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"79b612d5-4915-4201-84ce-a34c4f9b4615\"},{\"component\":\"JText\",\"visible\":true,\"w\":149.0000062166706,\"x\":1486.1397129379136,\"h\":37.00000400904318,\"i\":\"es-drager-1762926323283-1\",\"y\":487.1981242324508,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"51.233340999999996%\",\"left\":\"78.652026%\",\"width\":\"7.885633%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.890889%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项目级关键指标\\\"\\n}\",\"size\":{\"width\":149.0000062166706,\"height\":37.00000400904318},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"79b612d5-4915-4201-84ce-a34c4f9b4615\"},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1794.7127938210083,\"h\":29.97211347737514,\"i\":\"es-drager-1762925789762-11\",\"y\":236.97293880777556,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.919873000000006%\",\"left\":\"94.982858%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"天\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1758.12427745993,\"h\":39.00000114161981,\"i\":\"es-drager-1762925775562-10\",\"y\":230.77130066941345,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.267714000000005%\",\"left\":\"93.046458%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"70\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#3584DE\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":104.00000459277844,\"x\":1670.3751368557098,\"h\":26.000000761079882,\"i\":\"es-drager-1762925764016-9\",\"y\":237.98115349214476,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"25.025895999999996%\",\"left\":\"88.402448%\",\"width\":\"5.504066%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.734138000000001%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"平均预期天数\\\"\\n}\",\"size\":{\"width\":104.00000459277844,\"height\":26.000000761079882},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1613.6576836518175,\"h\":29.97211347737514,\"i\":\"es-drager-1762925757252-8\",\"y\":235.70681979646992,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.786729000000005%\",\"left\":\"85.40075%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"当年\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":277.99999701540446,\"x\":1611.5123124804927,\"h\":49.000005823294806,\"i\":\"es-drager-1762925746092-7\",\"y\":227.7274000895535,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"23.947620000000004%\",\"left\":\"85.28720899999999%\",\"width\":\"14.712790999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.152799000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":277.99999701540446,\"height\":49.000005823294806},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_31_1763551480635.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":46.99999371812427,\"x\":1312.4501862828608,\"h\":28.999996459944818,\"i\":\"es-drager-1762925685447-6\",\"y\":233.30353869202474,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.534002%\",\"left\":\"69.459732%\",\"width\":\"2.487414%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.0496150000000006%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"本月\\\"\\n}\",\"size\":{\"width\":46.99999371812427,\"height\":28.999996459944818},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":98.00000941495898,\"x\":1369.1676394867527,\"h\":26.99999932736819,\"i\":\"es-drager-1762925676767-5\",\"y\":234.31174386699809,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.640024000000004%\",\"left\":\"72.46143%\",\"width\":\"5.186524%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"最大单笔欠款\\\"\\n}\",\"size\":{\"width\":98.000009414959,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1459.4490100125442,\"h\":39.00000114161981,\"i\":\"es-drager-1762925670446-4\",\"y\":227.10190055366263,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"23.881843000000003%\",\"left\":\"77.239455%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"450\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#4EABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1511.2309625884175,\"h\":29.97211347737514,\"i\":\"es-drager-1762925663957-3\",\"y\":233.30353869202474,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.534002%\",\"left\":\"79.979948%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#708489\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":149.0000062166706,\"x\":1307.7458108005394,\"h\":37.00000400904318,\"i\":\"es-drager-1762925615604-2\",\"y\":179.6600187624041,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"18.892894999999996%\",\"left\":\"69.210759%\",\"width\":\"7.885633%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.890889%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"还款与预期监控\\\"\\n}\",\"size\":{\"width\":149.0000062166706,\"height\":37.00000400904318},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"79b612d5-4915-4201-84ce-a34c4f9b4615\"},{\"component\":\"JImg\",\"visible\":true,\"w\":277.99999701540446,\"x\":1307.772566294842,\"h\":49.000005823294806,\"i\":\"es-drager-1762925573195-1\",\"y\":222.79187145310112,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"23.428604000000004%\",\"left\":\"69.212175%\",\"width\":\"14.712790999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.152799000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":277.99999701540446,\"height\":49.000005823294806},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_31_1763551480635.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":98.00000941495898,\"x\":1684.3024486621575,\"h\":26.99999932736819,\"i\":\"es-drager-1762920907802-4\",\"y\":150.61890367447094,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.838956000000001%\",\"left\":\"89.139533%\",\"width\":\"5.186524%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"罚息收入\\\"\\n}\",\"size\":{\"width\":98.000009414959,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#D7EDFF\",\"enabled\":true,\"startColor\":\"#4D699D\"},\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1728.8745702235644,\"h\":29.97211347737514,\"i\":\"es-drager-1762920898709-3\",\"y\":126.82054678659989,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"13.336341%\",\"left\":\"91.498455%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#A5BECF\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1679.6248475692616,\"h\":39.00000114161981,\"i\":\"es-drager-1762920889380-2\",\"y\":120.61889913884187,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.684181%\",\"left\":\"88.891977%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"898\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#FFC5AB\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#E86B6B\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1517.6905060550296,\"h\":39.00000114161981,\"i\":\"es-drager-1762920868707-1\",\"y\":118.21561803439675,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.431454000000002%\",\"left\":\"80.321811%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"3,898\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#6BE5E8\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":98.00000941495898,\"x\":1523.6342315562724,\"h\":26.99999932736819,\"i\":\"es-drager-1762920200057-8\",\"y\":146.94949404932422,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.453084%\",\"left\":\"80.636375%\",\"width\":\"5.186524%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"手续费收入\\\"\\n}\",\"size\":{\"width\":98.000009414959,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#D7EDFF\",\"enabled\":true,\"startColor\":\"#4D699D\"},\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1578.335291699086,\"h\":29.97211347737514,\"i\":\"es-drager-1762920184548-6\",\"y\":124.41725617275884,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"13.083613%\",\"left\":\"83.531358%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#A5BECF\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":98.00000941495898,\"x\":1340.1758506806332,\"h\":26.99999932736819,\"i\":\"es-drager-1762920042120-5\",\"y\":147.0784604768863,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.466646%\",\"left\":\"70.927077%\",\"width\":\"5.186524%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.8392970000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"租金收入\\\"\\n}\",\"size\":{\"width\":98.000009414959,\"height\":26.99999932736819},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#D7EDFF\",\"enabled\":true,\"startColor\":\"#4D699D\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"normal\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":1387.280183268488,\"h\":29.97211347737514,\"i\":\"es-drager-1762920024296-4\",\"y\":120.74785605700805,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.697742000000002%\",\"left\":\"73.420013%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#A5BECF\",\"letterSpacing\":0,\"fontSize\":12,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.00000431388042,\"x\":1336.7643551009614,\"h\":39.00000114161981,\"i\":\"es-drager-1762920012633-3\",\"y\":114.54621791864594,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.045583%\",\"left\":\"70.746528%\",\"width\":\"3.863430999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.1012070000000005%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"960\\\"\\n}\",\"size\":{\"width\":73.00000431388042,\"height\":39.00000114161981},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#038BFE\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#6BBBE8\",\"direction\":\"to bottom\"},\"letterSpacing\":2,\"fontSize\":24,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":97.00000392028137,\"x\":1681.1488904078549,\"h\":66.000000468988,\"i\":\"es-drager-1762919932439-2\",\"y\":61.86573353334538,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.5057480000000005%\",\"left\":\"88.972635%\",\"width\":\"5.1336%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.940504000000002%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":97.00000392028139,\"height\":66.000000468988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_17_1763551465441.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":98.00000941495898,\"x\":1516.6823000769286,\"h\":68.99999616785293,\"i\":\"es-drager-1762919907257-1\",\"y\":56.93021440628894,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.986733000000002%\",\"left\":\"80.268453%\",\"width\":\"5.186524%\",\"position\":\"absolute\",\"config\":{},\"height\":\"7.2559809999999985%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":98.000009414959,\"height\":68.99999616785293},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_15_1763551456718.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":104.00000459277844,\"x\":1323.09496172476,\"h\":64.00000333641137,\"i\":\"95a344c5-64da-4b5a-8c42-564baeb66174\",\"y\":55.79305231314942,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.867149999999998%\",\"left\":\"70.023093%\",\"width\":\"5.504066%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.730186000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":104.00000459277844,\"height\":64.00000333641137},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_13_1763551449127.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":902.9999922765066,\"x\":508.64661984462634,\"h\":604.9999979594594,\"i\":\"f14ca628-b80a-4dd7-8df3-67597e00b63c\",\"y\":88.35669360434433,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.291514864092687%\",\"left\":\"26.919465794871066%\",\"width\":\"47.790109%\",\"position\":\"absolute\",\"config\":{},\"height\":\"63.62128600000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":902.9999922765066,\"height\":604.9999979594594},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/产城背景地图_1763551428302.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JCardCarousel\",\"visible\":true,\"w\":313.40683127324746,\"x\":934.3368548099414,\"h\":160.8503385248215,\"i\":\"es-drager-1762858057998-9\",\"y\":787.428049725881,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"82.80526500000002%\",\"left\":\"49.448572%\",\"width\":\"16.586652%\",\"position\":\"absolute\",\"config\":{},\"height\":\"16.914884999999995%\"},\"componentName\":\"卡片轮播\",\"config\":{\"chartData\":\"[{\\\"title\\\":\\\"销售物料汇总数据\\\",\\\"orderNum\\\":1247,\\\"orderAmount\\\":28475000,\\\"deliveryNum\\\":1189,\\\"signNum\\\":1156,\\\"outAmount\\\":26789000},{\\\"title\\\":\\\"采购物料汇总数据\\\",\\\"orderNum\\\":892,\\\"orderAmount\\\":15680000,\\\"deliveryNum\\\":856,\\\"signNum\\\":823,\\\"outAmount\\\":14875000},{\\\"title\\\":\\\"库存物料汇总数据\\\",\\\"orderNum\\\":2156,\\\"orderAmount\\\":3440,\\\"deliveryNum\\\":2340,\\\"signNum\\\":2340,\\\"outAmount\\\":7100000},{\\\"title\\\":\\\"质量物料汇总数据\\\",\\\"orderNum\\\":110,\\\"orderAmount\\\":33330000,\\\"deliveryNum\\\":1100,\\\"signNum\\\":110,\\\"outAmount\\\":111110000}]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":313.40683127324746,\"height\":160.85033852482147},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"dataMapping\":[],\"background\":\"#FFFFFF00\",\"w\":1000,\"dataType\":1,\"h\":230,\"linkageConfig\":[],\"timeOut\":0,\"option\":{\"titleFieldMapping\":{\"offset\":{\"x\":22,\"y\":0},\"show\":true,\"position\":\"left\",\"textStyle\":{\"letterSpacing\":0,\"fontSize\":12,\"fontGradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to bottom\",\"startColor\":\"#FFFFFF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"key\":\"title\",\"direction\":\"vertical\"},\"autoScrollEnabled\":true,\"contentLineHeight\":24,\"autoScrollDirection\":\"to-left\",\"contentFieldMapping\":[{\"marginRight\":0,\"valueStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"个\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"订单数量\",\"width\":120,\"key\":\"orderNum\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"元\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"订单金额\",\"width\":150,\"key\":\"orderAmount\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"个\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"发货数量\",\"width\":120,\"key\":\"deliveryNum\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"个\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"签收数量\",\"width\":120,\"key\":\"signNum\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"元\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"出库金额\",\"width\":150,\"key\":\"outAmount\",\"marginLeft\":0}],\"contentCurrent\":0,\"autoScrollSpeed\":100,\"contentLineAlign\":\"start\",\"contentLineTextGap\":7,\"cardStyle\":{\"backgroundColor\":\"#1890FF1A\",\"borderColor\":\"#1890FF\",\"backgroundImagePosition\":\"center\",\"backgroundImage\":\"drag/lib/img/cardCarousel-bg-01.png\",\"paddingRight\":7,\"minWidth\":300,\"backgroundImageRepeat\":\"no-repeat\",\"backgroundImageSize\":\"100% 100%\",\"marginRight\":25,\"borderEnabled\":true,\"paddingBottom\":16,\"borderRadius\":2,\"borderWidth\":1,\"paddingTop\":11,\"borderStyle\":\"dashed\",\"paddingLeft\":60},\"currentValue\":0}}},{\"component\":\"JText\",\"visible\":true,\"w\":166.999991750129,\"x\":928.9449046933179,\"h\":46.00000061503394,\"i\":\"es-drager-1762858012980-8\",\"y\":742.7945873431419,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"78.11164800000002%\",\"left\":\"49.16321%\",\"width\":\"8.838259%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.837321000000001%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"采购物料汇总数据\\\"\\n}\",\"size\":{\"width\":166.999991750129,\"height\":46.00000061503395},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":164.99999965589683,\"x\":604.9472435546074,\"h\":46.00000061503394,\"i\":\"es-drager-1762858006638-7\",\"y\":745.4557822839193,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"78.39149700000002%\",\"left\":\"32.01605200000001%\",\"width\":\"8.732411999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.837321000000001%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"销售物料汇总数据\\\"\\n}\",\"size\":{\"width\":164.99999965589683,\"height\":46.00000061503395},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":464.15006375641275,\"h\":27.97398018640021,\"i\":\"es-drager-1762857953002-6\",\"y\":603.0104611624024,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.412068%\",\"left\":\"24.564543%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同总金额\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBF4FC\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":498.46424973104354,\"h\":29.97211347737514,\"i\":\"es-drager-1762857945733-5\",\"y\":577.8170283450596,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"60.76274800000001%\",\"left\":\"26.380577000000006%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.85083281172334,\"x\":444.15006723359915,\"h\":42.93802267351676,\"i\":\"es-drager-1762857940021-4\",\"y\":566.550904652079,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"59.578012000000015%\",\"left\":\"23.506069%\",\"width\":\"3.90846%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.515326000000002%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"5451\\\"\\n}\",\"size\":{\"width\":73.85083281172334,\"height\":42.93802267351676},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":2,\"fontSize\":26,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":254.103164781102,\"h\":27.97398018640021,\"i\":\"es-drager-1762857932522-3\",\"y\":603.1394180805687,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.425629000000015%\",\"left\":\"13.448081999999998%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同总数\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#CAF1F1\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":273.2239145409379,\"h\":29.97211347737514,\"i\":\"es-drager-1762857921854-2\",\"y\":574.1476187199129,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"60.376876%\",\"left\":\"14.460023%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.85083281172334,\"x\":232.83704384994144,\"h\":42.93802267351676,\"i\":\"es-drager-1762857915642-1\",\"y\":564.1476235476339,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"59.325285000000015%\",\"left\":\"12.3226%\",\"width\":\"3.90846%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.515326000000002%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"700\\\"\\n}\",\"size\":{\"width\":73.85083281172334,\"height\":42.93802267351676},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":2,\"fontSize\":26,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":431.3599206203753,\"h\":27.97398018640021,\"i\":\"es-drager-1762857685097-3\",\"y\":92.89323780828366,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.768574000000001%\",\"left\":\"22.829167%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同总金额\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBF4FC\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.85083281172334,\"x\":415.15827842747956,\"h\":42.93802267351676,\"i\":\"es-drager-1762857678154-2\",\"y\":62.76426684509252,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.600237000000002%\",\"left\":\"21.971716%\",\"width\":\"3.90846%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.515326000000002%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"5451\\\"\\n}\",\"size\":{\"width\":73.85083281172334,\"height\":42.93802267351676},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":2,\"fontSize\":26,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":470.7385853332709,\"h\":29.97211347737514,\"i\":\"es-drager-1762857671067-1\",\"y\":74.03038102867723,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"7.784972%\",\"left\":\"24.913232%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万元\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":79.61138898968348,\"x\":236.50643896473628,\"h\":27.97398018640021,\"i\":\"es-drager-1762857250911-4\",\"y\":91.75606620574825,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.648990000000001%\",\"left\":\"12.516798000000001%\",\"width\":\"4.21333%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.9417200000000006%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"合同总数\\\"\\n}\",\"size\":{\"width\":79.61138898968348,\"height\":27.97398018640021},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#CAF1F1\",\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":55.4256503232591,\"x\":264.4900406878781,\"h\":29.97211347737514,\"i\":\"es-drager-1762857246742-3\",\"y\":66.5626238790095,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.999669000000003%\",\"left\":\"13.997793999999999%\",\"width\":\"2.933331%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.151842%\"},\"componentName\":\"文本\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"个\\\"\\n}\",\"size\":{\"width\":55.42565032325909,\"height\":29.972113477375135},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"fontSize\":15,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":73.85083281172334,\"x\":220.3048156669637,\"h\":42.93802267351676,\"i\":\"c9cf304e-4c81-4f49-86ba-4bd605c5a2ee\",\"y\":60.36098574064735,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.347509999999999%\",\"left\":\"11.659348%\",\"width\":\"3.90846%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.515326000000002%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"700\\\"\\n}\",\"size\":{\"width\":73.85083281172334,\"height\":42.93802267351676},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#DBE5F5\",\"letterSpacing\":2,\"fontSize\":26,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":159.22275908424388,\"x\":378.3118436361548,\"h\":100.90612107946721,\"i\":\"es-drager-1762856453053-5\",\"y\":566.1992281719063,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"59.541030000000006%\",\"left\":\"20.021666%\",\"width\":\"8.426659%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.611202000000002%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":159.22275908424388,\"height\":100.90612107946721},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_08_1763551371680.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":158.21502548422043,\"x\":175.86167221580308,\"h\":103.90332101592959,\"i\":\"es-drager-1762856443052-4\",\"y\":565.062056569371,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"59.42144600000001%\",\"left\":\"9.307252%\",\"width\":\"8.373326%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.926385000000002%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":158.21502548422043,\"height\":103.90332101592959},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_06_1763551351533.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":179.4107809769754,\"x\":9.999998261406802,\"h\":59.87246905308507,\"i\":\"es-drager-1762856433841-3\",\"y\":582.2553106684887,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.229474%\",\"left\":\"0.5292370000000001%\",\"width\":\"9.495083999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.296138%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"销售合同执行情况\\\"\\n}\",\"size\":{\"width\":179.4107809769754,\"height\":59.87246905308506},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":1.0000054946776085,\"x\":265.756146201102,\"h\":489.0000026102841,\"i\":\"es-drager-1762856395529-2\",\"y\":261.1933607244709,\"orderNum\":70,\"angle\":89.5176726903602,\"groupStyle\":{\"transform\":\"rotate(89.5176726903602deg)\",\"top\":\"27.466872%\",\"left\":\"14.064801%\",\"width\":\"0.05292399999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"51.422825%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1.0000054946776085,\"height\":489.0000026102842},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":23,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_23_1763551793351.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":168.29241816982415,\"x\":345.52170050011733,\"h\":100.90612107946721,\"i\":\"es-drager-1762856376359-1\",\"y\":61.146461844218294,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.430110000000002%\",\"left\":\"18.28629%\",\"width\":\"8.906659%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.611202000000002%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":168.29241816982417,\"height\":100.90612107946721},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_08_1763551371680.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":170.3079042649941,\"x\":154.4665731743963,\"h\":110.89681606252965,\"i\":\"d3cf63e9-7b37-4afe-b90d-c6c1bdf48102\",\"y\":52.412585683244885,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.511663000000001%\",\"left\":\"8.174944000000002%\",\"width\":\"9.013325999999996%\",\"position\":\"absolute\",\"config\":{},\"height\":\"11.661815000000002%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":170.30790426499408,\"height\":110.89681606252967},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_06_1763551351533.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":179.4107809769754,\"x\":6.330603146611956,\"h\":59.87246905308507,\"i\":\"39a18ddf-1cad-406d-aeba-22acef8b3394\",\"y\":83.53314890672488,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.784275000000003%\",\"left\":\"0.33503899999999986%\",\"width\":\"9.495083999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.296138%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"销售合同执行情况\\\"\\n}\",\"size\":{\"width\":179.4107809769754,\"height\":59.87246905308506},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":275.6824912726846,\"x\":608.4677262042908,\"h\":56.760195916509936,\"i\":\"es-drager-1762509913951-2\",\"y\":686.8419103099617,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"72.22771200000001%\",\"left\":\"32.202369%\",\"width\":\"14.590139999999996%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.968854%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"销售/采购物料汇总数据\\\"\\n}\",\"size\":{\"width\":275.6824912726846,\"height\":56.760195916509936},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"55be9102-a729-42da-a3af-8e09bb26acad\"},{\"component\":\"JImg\",\"visible\":true,\"w\":652.010970522931,\"x\":602.0879012620165,\"h\":39.44354789584329,\"i\":\"es-drager-1762509378719-1\",\"y\":698.3655484446457,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"73.43952800000001%\",\"left\":\"31.864725%\",\"width\":\"34.506839%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.147850000000002%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":652.010970522931,\"height\":39.44354789584329},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"165d8b3e-f0cd-4f0c-803d-1ba28cbe4255\"},{\"component\":\"JText\",\"visible\":true,\"w\":197.1030783259555,\"x\":1276.2217920728488,\"h\":57.678851110038615,\"i\":\"es-drager-1756456857944-26\",\"y\":0,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0%\",\"left\":\"67.542391%\",\"width\":\"10.431426%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.065459%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"租赁业务数据\\\"\\n}\",\"size\":{\"width\":197.1030783259555,\"height\":57.67885111003861},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"79b612d5-4915-4201-84ce-a34c4f9b4615\"},{\"component\":\"JImg\",\"visible\":true,\"w\":545.6198636829778,\"x\":1285.7391522050414,\"h\":40.91284466052541,\"i\":\"es-drager-1756456883802-27\",\"y\":10.89156760620763,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.1453480000000005%\",\"left\":\"68.046085%\",\"width\":\"28.876227%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.302360000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":545.6198636829778,\"height\":40.91284466052541},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"165d8b3e-f0cd-4f0c-803d-1ba28cbe4255\"},{\"component\":\"JText\",\"visible\":true,\"w\":180.5063957945135,\"x\":20.990441085205163,\"h\":44.25372367518584,\"i\":\"aeb580ea-2f80-4d05-8e54-5077a7907722\",\"y\":2.771618046281901,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0.29146100000000014%\",\"left\":\"1.110892%\",\"width\":\"9.553068%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.653684000000002%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"供应链业务数据\\\"\\n}\",\"size\":{\"width\":180.5063957945135,\"height\":44.25372367518584},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"55be9102-a729-42da-a3af-8e09bb26acad\"},{\"component\":\"JImg\",\"visible\":true,\"w\":532.0000133195311,\"x\":8.069766961641264,\"h\":39.99999970790813,\"i\":\"cb3770f4-03fa-4ad8-a636-c53fca6ac8f1\",\"y\":6.48828936918585,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0.6823029999999993%\",\"left\":\"0.4270819999999998%\",\"width\":\"28.155414%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.206366000000001%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":532.0000133195311,\"height\":39.99999970790813},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"165d8b3e-f0cd-4f0c-803d-1ba28cbe4255\"},{\"component\":\"JCardScroll\",\"visible\":true,\"w\":571.0000008704807,\"x\":5.169592308042205,\"h\":252.99999862798865,\"i\":\"e0b9a809-d8a1-4996-bafa-73202d5a4b71\",\"y\":197.15144948020918,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"20.732279%\",\"left\":\"0.273594%\",\"width\":\"30.219437999999993%\",\"position\":\"absolute\",\"config\":{},\"height\":\"26.605265%\"},\"componentName\":\"卡片滚动(横向)\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[],\"dataType\":1,\"h\":255,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"rank\\\": 1,\\n \\\"customerName\\\": \\\"北京华信科技有限公司\\\",\\n \\\"contractAmount\\\": 8000\\n },\\n {\\n \\\"rank\\\": 2,\\n \\\"customerName\\\": \\\"上海(中国)智远信息技术股份有限公司\\\",\\n \\\"contractAmount\\\": 7800\\n },\\n {\\n \\\"rank\\\": 3,\\n \\\"customerName\\\": \\\"深圳市鼎盛软件有限公司\\\",\\n \\\"contractAmount\\\": 6880\\n },\\n {\\n \\\"rank\\\": 4,\\n \\\"customerName\\\": \\\"广州恒信数据服务有限公司\\\",\\n \\\"contractAmount\\\": 5600\\n },\\n {\\n \\\"rank\\\": 5,\\n \\\"customerName\\\": \\\"杭州云帆科技发展有限公司\\\",\\n \\\"contractAmount\\\": 4900\\n },\\n {\\n \\\"rank\\\": 6,\\n \\\"customerName\\\": \\\"成都睿智科技有限公司\\\",\\n \\\"contractAmount\\\": 4700\\n },\\n {\\n \\\"rank\\\": 7,\\n \\\"customerName\\\": \\\"南京博思信息技术有限公司\\\",\\n \\\"contractAmount\\\": 4500\\n },\\n {\\n \\\"rank\\\": 8,\\n \\\"customerName\\\": \\\"苏州新创软件有限公司\\\",\\n \\\"contractAmount\\\": 4200\\n },\\n {\\n \\\"rank\\\": 9,\\n \\\"customerName\\\": \\\"重庆智联科技有限公司\\\",\\n \\\"contractAmount\\\": 3900\\n },\\n {\\n \\\"rank\\\": 10,\\n \\\"customerName\\\": \\\"武汉华腾信息技术有限公司\\\",\\n \\\"contractAmount\\\": 3600\\n }\\n]\",\"size\":{\"width\":571.0000008704807,\"height\":252.99999862798865},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":900,\"linkageConfig\":[],\"option\":{\"showIndex\":false,\"autoScrollEnabled\":true,\"columnGap\":16,\"rowGap\":16,\"indexFieldStyle\":{},\"contentFieldMapping\":[{\"itemConfig\":{\"marginRight\":0,\"alignItems\":\"flex-start\",\"width\":0,\"marginBottom\":4,\"layoutDirection\":\"row\",\"justifyContent\":\"center\",\"marginTop\":0,\"height\":73,\"marginLeft\":0},\"valueStyle\":{\"fontSize\":14,\"marginBottom\":4,\"fontColor\":\"#A6D8FF\",\"fontWeight\":\"normal\",\"height\":0},\"showValue\":true,\"valueCompose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"nameStyle\":{\"fontSize\":14,\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"omitConfig\":{\"show\":true,\"lines\":3},\"nameCompose\":{\"enabled\":false},\"valueType\":\"non-array\",\"name\":\"客户名称\",\"thousandSeparatorConfig\":{\"show\":false},\"key\":\"customerName\",\"showLabel\":false},{\"itemConfig\":{\"marginRight\":0,\"alignItems\":\"center\",\"width\":0,\"marginBottom\":3,\"layoutDirection\":\"row\",\"justifyContent\":\"center\",\"marginTop\":0,\"height\":68,\"marginLeft\":0},\"valueStyle\":{\"fontSize\":24,\"marginBottom\":0,\"fontColor\":\"#FEAF26\",\"fontWeight\":\"bold\",\"height\":0,\"marginLeft\":0},\"showValue\":true,\"valueCompose\":{\"enabled\":false},\"nameStyle\":{\"marginRight\":0,\"width\":225,\"fontSize\":14,\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\",\"marginLeft\":0},\"omitConfig\":{\"show\":false,\"lines\":1},\"nameCompose\":{\"enabled\":false},\"valueType\":\"non-array\",\"name\":\"排名\",\"thousandSeparatorConfig\":{\"show\":false},\"key\":\"rank\",\"showLabel\":false},{\"itemConfig\":{\"marginRight\":0,\"alignItems\":\"center\",\"width\":0,\"marginBottom\":0,\"layoutDirection\":\"column-reverse\",\"justifyContent\":\"center\",\"marginTop\":22,\"height\":0,\"marginLeft\":0},\"valueStyle\":{\"fontSize\":18,\"marginBottom\":2,\"fontGradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"fontColor\":\"#F8E71C\",\"fontWeight\":\"bold\"},\"showValue\":true,\"valueCompose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":14,\"fontColor\":\"#40A9FF\"},\"suffix\":\"万元\",\"enabled\":false},\"nameStyle\":{\"fontSize\":12,\"fontColor\":\"#B0B0B0\",\"fontWeight\":\"normal\",\"height\":0},\"omitConfig\":{\"show\":false,\"lines\":1},\"nameCompose\":{\"enabled\":false},\"valueType\":\"non-array\",\"name\":\"合同额(万元)\",\"thousandSeparatorConfig\":{\"show\":true},\"key\":\"contractAmount\",\"showLabel\":true}],\"contentCurrent\":2,\"autoScrollSpeed\":100,\"scrollDirection\":\"left\",\"cardStyle\":{\"backgroundColor\":\"#1890FF1A\",\"borderColor\":\"#1890FF\",\"backgroundImage\":\"drag/lib/img/cardScroll-bg-01.png\",\"paddingRight\":5,\"borderEnabled\":true,\"paddingBottom\":5,\"borderRadius\":8,\"borderWidth\":0,\"width\":98,\"bgHighlightImage\":\"\",\"paddingTop\":5,\"borderStyle\":\"dashed\",\"paddingLeft\":5,\"height\":250},\"currentValue\":0,\"direction\":\"horizontal\"}}},{\"component\":\"JCardScroll\",\"visible\":true,\"w\":566.3493253279954,\"x\":0,\"h\":244.77226006523037,\"i\":\"2a873018-ef8b-4b48-85d8-2792e5015273\",\"y\":694.6404137593668,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"73.047796%\",\"left\":\"0%\",\"width\":\"29.973307%\",\"position\":\"absolute\",\"config\":{},\"height\":\"25.740043000000007%\"},\"componentName\":\"卡片滚动(横向)\",\"config\":{\"chartData\":\"[{\\\"rank\\\":1,\\\"customerName\\\":\\\"北京华信科技有限公司\\\",\\\"contractAmount\\\":8000},{\\\"rank\\\":2,\\\"customerName\\\":\\\"上海(中国)智远信息技术股份有限公司\\\",\\\"contractAmount\\\":7800},{\\\"rank\\\":3,\\\"customerName\\\":\\\"深圳市鼎盛软件有限公司\\\",\\\"contractAmount\\\":6880},{\\\"rank\\\":4,\\\"customerName\\\":\\\"广州恒信数据服务有限公司\\\",\\\"contractAmount\\\":5600},{\\\"rank\\\":5,\\\"customerName\\\":\\\"杭州云帆科技发展有限公司\\\",\\\"contractAmount\\\":4900},{\\\"rank\\\":6,\\\"customerName\\\":\\\"成都睿智科技有限公司\\\",\\\"contractAmount\\\":4700},{\\\"rank\\\":7,\\\"customerName\\\":\\\"南京博思信息技术有限公司\\\",\\\"contractAmount\\\":4500},{\\\"rank\\\":8,\\\"customerName\\\":\\\"苏州新创软件有限公司\\\",\\\"contractAmount\\\":4200},{\\\"rank\\\":9,\\\"customerName\\\":\\\"重庆智联科技有限公司\\\",\\\"contractAmount\\\":3900},{\\\"rank\\\":10,\\\"customerName\\\":\\\"武汉华腾信息技术有限公司\\\",\\\"contractAmount\\\":3600}]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":566.3493253279954,\"height\":244.77226006523034},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"dataMapping\":[],\"background\":\"#FFFFFF00\",\"w\":900,\"dataType\":1,\"h\":255,\"linkageConfig\":[],\"timeOut\":0,\"option\":{\"showIndex\":false,\"autoScrollEnabled\":true,\"columnGap\":16,\"rowGap\":16,\"indexFieldStyle\":{},\"contentFieldMapping\":[{\"itemConfig\":{\"marginRight\":0,\"alignItems\":\"flex-start\",\"width\":0,\"marginBottom\":4,\"layoutDirection\":\"row\",\"justifyContent\":\"center\",\"marginTop\":0,\"height\":73,\"marginLeft\":0},\"valueStyle\":{\"fontSize\":14,\"marginBottom\":4,\"fontColor\":\"#A6D8FF\",\"fontWeight\":\"normal\",\"height\":0},\"showValue\":true,\"valueCompose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"nameStyle\":{\"fontSize\":14,\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"omitConfig\":{\"show\":true,\"lines\":3},\"nameCompose\":{\"enabled\":false},\"valueType\":\"non-array\",\"name\":\"客户名称\",\"thousandSeparatorConfig\":{\"show\":false},\"key\":\"customerName\",\"showLabel\":false},{\"itemConfig\":{\"marginRight\":0,\"alignItems\":\"center\",\"width\":0,\"marginBottom\":3,\"layoutDirection\":\"row\",\"justifyContent\":\"center\",\"marginTop\":0,\"height\":68,\"marginLeft\":0},\"valueStyle\":{\"fontSize\":24,\"marginBottom\":0,\"fontColor\":\"#FEAF26\",\"fontWeight\":\"bold\",\"height\":0,\"marginLeft\":0},\"showValue\":true,\"valueCompose\":{\"enabled\":false},\"nameStyle\":{\"marginRight\":0,\"width\":225,\"fontSize\":14,\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\",\"marginLeft\":0},\"omitConfig\":{\"show\":false,\"lines\":1},\"nameCompose\":{\"enabled\":false},\"valueType\":\"non-array\",\"name\":\"排名\",\"thousandSeparatorConfig\":{\"show\":false},\"key\":\"rank\",\"showLabel\":false},{\"itemConfig\":{\"marginRight\":0,\"alignItems\":\"center\",\"width\":0,\"marginBottom\":0,\"layoutDirection\":\"column-reverse\",\"justifyContent\":\"center\",\"marginTop\":0,\"height\":100,\"marginLeft\":0},\"valueStyle\":{\"fontSize\":18,\"marginBottom\":2,\"fontGradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#96F5F8\",\"direction\":\"to bottom\"},\"fontColor\":\"#40A9FF\",\"fontWeight\":\"bold\"},\"showValue\":true,\"valueCompose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":14,\"fontColor\":\"#40A9FF\"},\"suffix\":\"万元\",\"enabled\":false},\"nameStyle\":{\"fontSize\":12,\"fontColor\":\"#B0B0B0\",\"fontWeight\":\"normal\",\"height\":0},\"omitConfig\":{\"show\":false,\"lines\":1},\"nameCompose\":{\"enabled\":false},\"valueType\":\"non-array\",\"name\":\"合同额(万元)\",\"thousandSeparatorConfig\":{\"show\":true},\"key\":\"contractAmount\",\"showLabel\":true}],\"contentCurrent\":2,\"autoScrollSpeed\":100,\"scrollDirection\":\"left\",\"cardStyle\":{\"backgroundColor\":\"#1890FF1A\",\"borderColor\":\"#1890FF\",\"backgroundImage\":\"drag/lib/img/cardScroll-bg-01.png\",\"paddingRight\":5,\"borderEnabled\":true,\"paddingBottom\":5,\"borderRadius\":8,\"borderWidth\":0,\"width\":98,\"bgHighlightImage\":\"\",\"paddingTop\":5,\"borderStyle\":\"dashed\",\"paddingLeft\":5,\"height\":250},\"currentValue\":0,\"direction\":\"horizontal\"}}},{\"component\":\"JCardCarousel\",\"visible\":true,\"w\":313.40683127324746,\"x\":607.8069637496602,\"h\":160.8503385248215,\"i\":\"ffd3cb22-59f2-471e-8329-f15c72dbe26e\",\"y\":790.0892541760543,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"83.08511500000002%\",\"left\":\"32.167399%\",\"width\":\"16.586652%\",\"position\":\"absolute\",\"config\":{},\"height\":\"16.914884999999995%\"},\"componentName\":\"卡片轮播\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[],\"dataType\":1,\"h\":230,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"销售物料汇总数据\\\",\\n \\\"orderNum\\\": 1247,\\n \\\"orderAmount\\\": 28475000,\\n \\\"deliveryNum\\\": 1189,\\n \\\"signNum\\\": 1156,\\n \\\"outAmount\\\": 26789000\\n },\\n {\\n \\\"title\\\": \\\"采购物料汇总数据\\\",\\n \\\"orderNum\\\": 892,\\n \\\"orderAmount\\\": 15680000,\\n \\\"deliveryNum\\\": 856,\\n \\\"signNum\\\": 823,\\n \\\"outAmount\\\": 14875000\\n },\\n {\\n \\\"title\\\": \\\"库存物料汇总数据\\\",\\n \\\"orderNum\\\": 2156,\\n \\\"orderAmount\\\": 3440,\\n \\\"deliveryNum\\\": 2340,\\n \\\"signNum\\\": 2340,\\n \\\"outAmount\\\": 7100000\\n },\\n {\\n \\\"title\\\": \\\"质量物料汇总数据\\\",\\n \\\"orderNum\\\": 110,\\n \\\"orderAmount\\\": 33330000,\\n \\\"deliveryNum\\\": 1100,\\n \\\"signNum\\\": 110,\\n \\\"outAmount\\\": 111110000\\n }\\n]\",\"size\":{\"width\":313.40683127324746,\"height\":160.85033852482147},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":1000,\"linkageConfig\":[],\"option\":{\"titleFieldMapping\":{\"offset\":{\"x\":22,\"y\":0},\"show\":true,\"position\":\"left\",\"textStyle\":{\"letterSpacing\":0,\"fontSize\":12,\"fontGradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to bottom\",\"startColor\":\"#FFFFFF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"key\":\"title\",\"direction\":\"vertical\"},\"autoScrollEnabled\":true,\"contentLineHeight\":24,\"autoScrollDirection\":\"to-left\",\"contentFieldMapping\":[{\"marginRight\":0,\"valueStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"个\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"订单数量\",\"width\":120,\"key\":\"orderNum\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"元\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"订单金额\",\"width\":150,\"key\":\"orderAmount\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"个\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"发货数量\",\"width\":120,\"key\":\"deliveryNum\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"个\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"签收数量\",\"width\":120,\"key\":\"signNum\",\"marginLeft\":0},{\"marginRight\":0,\"valueStyle\":{\"fontColor\":\"#FFFFFF\"},\"valueCompose\":{\"contentStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffixStyle\":{\"fontSize\":18,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"元\",\"enabled\":true},\"nameStyle\":{\"letterSpacing\":0,\"fontSize\":16,\"fontStyle\":\"normal\",\"fontColor\":\"#CCCCCC\",\"fontWeight\":\"normal\"},\"nameCompose\":{\"enabled\":false},\"name\":\"出库金额\",\"width\":150,\"key\":\"outAmount\",\"marginLeft\":0}],\"contentCurrent\":0,\"autoScrollSpeed\":100,\"contentLineAlign\":\"start\",\"contentLineTextGap\":7,\"cardStyle\":{\"backgroundColor\":\"#1890FF1A\",\"borderColor\":\"#1890FF\",\"backgroundImagePosition\":\"center\",\"backgroundImage\":\"drag/lib/img/cardCarousel-bg-01.png\",\"paddingRight\":7,\"minWidth\":300,\"backgroundImageRepeat\":\"no-repeat\",\"backgroundImageSize\":\"100% 100%\",\"marginRight\":25,\"borderEnabled\":true,\"paddingBottom\":16,\"borderRadius\":2,\"borderWidth\":1,\"paddingTop\":11,\"borderStyle\":\"dashed\",\"paddingLeft\":60},\"currentValue\":0}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":507.0000082184526,\"x\":1330.691670384596,\"h\":82.0000060577888,\"i\":\"0445aa8e-0d06-4ada-8ac2-1f1d88272fea\",\"y\":536.4516695699515,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"56.41280199999999%\",\"left\":\"70.425139%\",\"width\":\"26.832320999999997%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.623051%\"},\"componentName\":\"滚动列表(多行+序号)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": 1,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 1000,\\n \\\"brand\\\": \\\"丰田\\\"\\n },\\n {\\n \\\"id\\\": 2,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 1000,\\n \\\"brand\\\": \\\"本田\\\"\\n },\\n {\\n \\\"id\\\": 3,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 2000,\\n \\\"brand\\\": \\\"大众\\\"\\n },\\n {\\n \\\"id\\\": 4,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 1000,\\n \\\"brand\\\": \\\"比亚迪\\\"\\n },\\n {\\n \\\"id\\\": 5,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 5,\\n \\\"brand\\\": \\\"特斯拉\\\"\\n },\\n {\\n \\\"id\\\": 6,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 4,\\n \\\"brand\\\": \\\"福特\\\"\\n },\\n {\\n \\\"id\\\": 7,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 2,\\n \\\"brand\\\": \\\"雪佛兰\\\"\\n },\\n {\\n \\\"id\\\": 8,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 6,\\n \\\"brand\\\": \\\"宝马\\\"\\n },\\n {\\n \\\"id\\\": 9,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 1,\\n \\\"brand\\\": \\\"奔驰\\\"\\n },\\n {\\n \\\"id\\\": 10,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 0,\\n \\\"brand\\\": \\\"奥迪\\\"\\n },\\n {\\n \\\"id\\\": 11,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 3,\\n \\\"brand\\\": \\\"起亚\\\"\\n },\\n {\\n \\\"id\\\": 12,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 2,\\n \\\"brand\\\": \\\"现代\\\"\\n },\\n {\\n \\\"id\\\": 13,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 5,\\n \\\"brand\\\": \\\"路虎\\\"\\n },\\n {\\n \\\"id\\\": 14,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 1,\\n \\\"brand\\\": \\\"沃尔沃\\\"\\n },\\n {\\n \\\"id\\\": 15,\\n \\\"plateNumber\\\": \\\"项目名称\\\",\\n \\\"violationCount\\\": 4,\\n \\\"brand\\\": \\\"马自达\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":507.0000082184526,\"height\":82.0000060577888},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"showIndex\":true,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"__system\":true,\"textAlign\":\"center\",\"compose\":{\"contentStyle\":{\"fontSize\":22,\"fontColor\":\"#E19900\"},\"enabled\":true},\"name\":\"序号\",\"width\":41,\"textStyle\":{\"fontSize\":22,\"fontGradient\":{\"endColor\":\"#FF4500\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFD700\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#E19900\",\"fontWeight\":\"bold\"},\"key\":\"__index__\"},{\"marginRight\":18,\"compose\":{\"contentStyle\":{\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontStyle\":\"italic\",\"fontColor\":\"#D1D1D1\"},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":true},\"name\":\"项目名称\",\"width\":65,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"plateNumber\",\"marginLeft\":0},{\"compose\":{\"contentStyle\":{\"marginRight\":3,\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#00D4FF\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#00FFDC\",\"fontWeight\":\"bold\",\"marginLeft\":4},\"suffixStyle\":{\"fontSize\":12,\"fontColor\":\"#C4C4C4\"},\"prefix\":\"欠款\",\"prefixStyle\":{\"fontSize\":14,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"万元\",\"enabled\":true},\"name\":\"金额\",\"width\":100,\"textStyle\":{\"fontColor\":\"#FFFFFF\"},\"key\":\"violationCount\"}],\"itemsPerRow\":2,\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"indexFieldStyle\":{\"width\":28,\"textStyle\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#F54100\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#D4BA28\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\"},\"marginLeft\":15},\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"drag/lib/img/scrollList-bg-02.png\",\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"height\":44}}}}]},\"component\":\"JGroup\",\"w\":1889.5123094958972,\"x\":31,\"y\":89,\"componentName\":\"万众\",\"pageCompId\":\"1151112776961806336\",\"equalProportion\":true,\"key\":\"a342e46b-63c6-4906-926c-36eaecaf0566\",\"group\":true},{\"visible\":false,\"h\":990.5545046613586,\"i\":\"es-drager-1756453915928-25\",\"props\":{\"elements\":[{\"component\":\"JRing\",\"visible\":true,\"w\":536,\"x\":1291.4419695193433,\"h\":245,\"i\":\"4471d794-0df3-4be7-8e35-7da09940b959\",\"y\":21.97302752185101,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"2.2182552720168527%\",\"left\":\"67.8862695459582%\",\"width\":\"28.175513368344646%\",\"position\":\"absolute\",\"config\":{},\"height\":\"24.73362130474166%\"},\"componentName\":\"饼状环形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"value\\\": 1048,\\n \\\"name\\\": \\\"垃圾车\\\"\\n },\\n {\\n \\\"value\\\": 735,\\n \\\"name\\\": \\\"机扫车\\\"\\n },\\n {\\n \\\"value\\\": 580,\\n \\\"name\\\": \\\"洒水车\\\"\\n }\\n]\",\"size\":{\"width\":536.2250879249707,\"height\":245.59671746776084},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":480,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"customColor\":[{\"color1\":\"#2A7DFB00\",\"color\":\"#2A7DFB\"},{\"color1\":\"#2BE4E3\",\"color\":\"#2BE4E300\"},{\"color1\":\"#FCA52F\",\"color\":\"#FCA52F00\"}],\"grid\":{\"top\":50,\"left\":50,\"show\":false},\"series\":[{\"data\":[],\"name\":\"Access From\",\"avoidLabelOverlap\":false,\"emphasis\":{\"label\":{\"show\":true,\"fontSize\":14,\"fontWeight\":\"bold\"}},\"label\":{\"color\":\"#EEF1FA\",\"show\":true,\"position\":\"center\"},\"labelLine\":{\"show\":false},\"type\":\"pie\",\"radius\":[\"40%\",\"70%\"]}],\"legend\":{\"r\":1,\"orient\":\"vertical\",\"t\":32},\"tooltip\":{\"trigger\":\"item\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"outRadius\":57,\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"innerRadius\":77,\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JSemiGauge\",\"visible\":true,\"w\":238.00000008283695,\"x\":17.725667868792492,\"h\":215.0000038199236,\"i\":\"ededef6c-2bd9-4f89-a363-7506ae217601\",\"y\":480.30831953922336,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"48.488833%\",\"left\":\"0.9317719999999999%\",\"width\":\"12.510769%\",\"position\":\"absolute\",\"config\":{},\"height\":\"21.705015000000003%\"},\"componentName\":\"半圆仪表盘\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataType\":1,\"h\":430,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"total\\\": 385,\\n \\\"used\\\": 85\\n }\\n]\",\"size\":{\"width\":238.00000008283695,\"height\":215.0000038199236},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":500,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"titleSuffix\":\"辆\",\"customAttr\":{\"innerCircle\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":0,\"y2\":1,\"x2\":1,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#2E76B9\"},{\"offset\":1,\"color\":\"#2E76B9\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":15}},\"name\":\"内部小圆\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"outerScale\":{\"axisLabel\":{\"color\":\"#FFFFFF\",\"distance\":-52,\"show\":true,\"fontSize\":14},\"min\":0,\"max\":100,\"axisLine\":{\"show\":false},\"name\":\"外部刻度\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"splitNumber\":2,\"detail\":{\"show\":false},\"type\":\"gauge\",\"radius\":67},\"innerProgress\":{\"axisLabel\":{\"show\":false},\"animationDuration\":2000,\"pointer\":{\"show\":true,\"length\":74,\"width\":3,\"itemStyle\":{\"color\":\"#2E76B9\"}},\"data\":[{\"name\":\"去年优良率\",\"value\":44}],\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#2E76B9\"],[1,\"#2E76B9\"]],\"width\":1}},\"name\":\"内部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"detail\":{\"offsetCenter\":[0,50],\"show\":false,\"textStyle\":{\"padding\":[0,0,0,0],\"color\":\"#FFFFFF\",\"fontSize\":18,\"fontWeight\":\"normal\"}},\"type\":\"gauge\",\"radius\":30,\"title\":{\"offsetCenter\":[0,26],\"show\":true,\"textStyle\":{\"color\":\"#FFFFFF\",\"fontSize\":16,\"fontWeight\":\"normal\"}}},\"outerProgress\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#2E76B9\"],[1,\"#2E76B9\"]],\"width\":2}},\"name\":\"外部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"basic\":{\"startAngle\":180,\"endAngle\":0},\"innerShadow\":{\"axisLabel\":{\"show\":false},\"customGradient\":{\"endColor\":\"#2E76B9\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#2E76B900\"},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":1,\"y2\":0,\"x2\":0,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#2de69600\"},{\"offset\":1,\"color\":\"#2de696\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":100}},\"name\":\"内部阴影\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":80}},\"valuePrefix\":\"已使用:\",\"titlePrefix\":\"车辆总数:\",\"valueMapping\":\"used\",\"titleMapping\":\"total\",\"valueSuffix\":\"辆\"}}},{\"component\":\"JStatsSummary\",\"visible\":true,\"w\":671.0000006332002,\"x\":609.0035179014067,\"h\":116.00000459495868,\"i\":\"e06945ca-3292-42b3-a219-f816d45d7af6\",\"y\":59.956609941129244,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.052832999999999%\",\"left\":\"32.013035%\",\"width\":\"35.271958%\",\"position\":\"absolute\",\"config\":{},\"height\":\"11.710613000000002%\"},\"componentName\":\"统计概览(背景模式)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": \\\"1\\\",\\n \\\"name\\\": \\\"总车辆数\\\",\\n \\\"value\\\": 385,\\n \\\"suffix\\\": \\\"辆\\\"\\n },\\n {\\n \\\"id\\\": \\\"2\\\",\\n \\\"name\\\": \\\"在线车辆数\\\",\\n \\\"value\\\": 300,\\n \\\"suffix\\\": \\\"辆\\\"\\n },\\n {\\n \\\"id\\\": \\\"3\\\",\\n \\\"name\\\": \\\"离线车辆数\\\",\\n \\\"value\\\": 85,\\n \\\"suffix\\\": \\\"辆\\\"\\n },\\n {\\n \\\"id\\\": \\\"4\\\",\\n \\\"name\\\": \\\"加油总量\\\",\\n \\\"value\\\": 6790,\\n \\\"suffix\\\": \\\"升\\\"\\n },\\n {\\n \\\"id\\\": \\\"5\\\",\\n \\\"name\\\": \\\"作业总里程\\\",\\n \\\"value\\\": 16790,\\n \\\"suffix\\\": \\\"公里\\\"\\n }\\n]\",\"size\":{\"width\":671.0000006332002,\"height\":116.00000459495868},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":713,\"dataType\":1,\"h\":129,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"layout\":{\"padding\":{\"top\":4,\"left\":20,\"bottom\":0,\"right\":6},\"borderColor\":\"#0f66ff59\",\"borderRadius\":0,\"shadow\":\"none\",\"justify\":\"space-between\",\"borderWidth\":0,\"gap\":16,\"fill\":{\"image\":{\"size\":\"100% 100%\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"drag/lib/img/bg01.png\"},\"color\":\"#0b2b63\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"image\"}},\"fieldMap\":{\"compareValue\":\"compareValue\",\"unit\":\"suffix\",\"negativeValue\":\"0\",\"compareState\":\"compareState\",\"label\":\"name\",\"value\":\"value\",\"positiveValue\":\"1\",\"compareLabel\":\"compareLabel\"},\"card\":{\"padding\":{\"horizontal\":3,\"vertical\":15},\"borderColor\":\"#0F66FF59\",\"borderRadius\":0,\"shadow\":\"none\",\"borderWidth\":0,\"blur\":24,\"minWidth\":100,\"fill\":{\"image\":{\"size\":\"cover\",\"repeat\":\"no-repeat\",\"position\":\"center\",\"url\":\"\"},\"color\":\"#0B2B6300\",\"gradient\":{\"endColor\":\"#0bb2ff\",\"angle\":135,\"type\":\"linear\",\"enabled\":false,\"direction\":\"to bottom right\",\"startColor\":\"#05336a\"},\"type\":\"none\"}},\"sections\":{\"middle\":{\"compare\":{\"valueStyle\":{\"positiveGradient\":{\"endColor\":\"#15f0c5\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#15f0c5\"},\"positiveColor\":\"#15F0C5\",\"fontSize\":14,\"negativeColor\":\"#D0021B\",\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"negativeGradient\":{\"endColor\":\"#D0021B\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#D0021B\"},\"fontColor\":\"#FFFFFF\"},\"alignItems\":\"center\",\"labelStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#cfeaff\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"135deg\",\"startColor\":\"#9ed3ff\"},\"fontColor\":\"#9ED3FF\"},\"label\":\"同比\"},\"paddingBottom\":10,\"show\":false,\"type\":\"compare\",\"align\":\"center\"},\"top\":{\"minHeight\":40,\"paddingBottom\":10,\"show\":true,\"paddingTop\":4,\"type\":\"value\",\"align\":\"center\",\"value\":{\"unit\":{\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#96F5F8\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#49ABFF\"},\"fontWeight\":\"normal\",\"fontColor\":\"#9ED3FF\"},\"unitGap\":6,\"fontSize\":24,\"fontGradient\":{\"endColor\":\"#49ABFF\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"135deg\",\"startColor\":\"#96F5F8\"},\"fontWeight\":\"bold\",\"fontColor\":\"#D8F1FF\"}},\"bottom\":{\"paddingBottom\":10,\"show\":true,\"label\":{\"fontSize\":14,\"fontColor\":\"#C9E6FF\"},\"type\":\"label\",\"align\":\"center\"}}}}},{\"component\":\"JRingProgress\",\"visible\":true,\"w\":132.99999836773736,\"x\":1652.157089719601,\"h\":109.000003549143,\"i\":\"es-drager-1763030296701-1\",\"y\":287.3540472473178,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"29.009413000000002%\",\"left\":\"86.847713%\",\"width\":\"6.991312000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"11.003938%\"},\"componentName\":\"基础环形图\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":200,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"占比\\\",\\n \\\"value\\\": 60\\n }\\n]\",\"size\":{\"width\":132.99999836773736,\"height\":109.000003549143},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":300,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"valueFontWeight\":\"normal\",\"color\":\"#1E90FF\",\"bgColor\":\"#E8EDF3C0\",\"valueFontSize\":16,\"lineHeight\":0,\"fontSize\":16,\"radius\":0.9,\"innerRadius\":0.9,\"valueFontColor\":\"#FFFFFF\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\",\"extraInfo\":{\"endColor\":\"#FF4500\",\"enabledGradient\":false,\"type\":\"linear\",\"direction\":\"to left\",\"startColor\":\"#FFD700\"}}}},{\"component\":\"JRingProgress\",\"visible\":true,\"w\":132.99999836773736,\"x\":1388.9331704588271,\"h\":109.000003549143,\"i\":\"99dbdc3f-3f29-4b98-86f1-2b590fe93931\",\"y\":288.74911449504765,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"29.15025%\",\"left\":\"73.011017%\",\"width\":\"6.991312000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"11.003938%\"},\"componentName\":\"基础环形图\",\"config\":{\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":200,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"占比\\\",\\n \\\"value\\\": 60\\n }\\n]\",\"size\":{\"width\":132.99999836773736,\"height\":109.000003549143},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":300,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"valueFontWeight\":\"normal\",\"color\":\"#1E90FF\",\"bgColor\":\"#E8EDF3C0\",\"valueFontSize\":16,\"lineHeight\":0,\"fontSize\":16,\"radius\":0.9,\"innerRadius\":0.9,\"valueFontColor\":\"#FFFFFF\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\",\"extraInfo\":{\"endColor\":\"#FF4500\",\"enabledGradient\":false,\"type\":\"linear\",\"direction\":\"to left\",\"startColor\":\"#FFD700\"}}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":527.999993949144,\"x\":1357.2801839687218,\"h\":183.00000186920772,\"i\":\"f65f9644-b011-4d49-9894-8d6293b286ca\",\"y\":782.8405641226518,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"79.030539%\",\"left\":\"71.347138%\",\"width\":\"27.754983%\",\"position\":\"absolute\",\"config\":{},\"height\":\"18.474501%\"},\"componentName\":\"滚动列表(多行+序号)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"violationCount\\\": 3,\\n \\\"id\\\": 1,\\n \\\"plateNumber\\\": \\\"粤B12345\\\",\\n \\\"brand\\\": \\\"丰田\\\"\\n },\\n {\\n \\\"violationCount\\\": 1,\\n \\\"id\\\": 2,\\n \\\"plateNumber\\\": \\\"粤A67890\\\",\\n \\\"brand\\\": \\\"本田\\\"\\n },\\n {\\n \\\"violationCount\\\": 2,\\n \\\"id\\\": 3,\\n \\\"plateNumber\\\": \\\"粤C54321\\\",\\n \\\"brand\\\": \\\"大众\\\"\\n },\\n {\\n \\\"violationCount\\\": 0,\\n \\\"id\\\": 4,\\n \\\"plateNumber\\\": \\\"粤D98765\\\",\\n \\\"brand\\\": \\\"比亚迪\\\"\\n },\\n {\\n \\\"violationCount\\\": 5,\\n \\\"id\\\": 5,\\n \\\"plateNumber\\\": \\\"粤E11223\\\",\\n \\\"brand\\\": \\\"特斯拉\\\"\\n },\\n {\\n \\\"violationCount\\\": 4,\\n \\\"id\\\": 6,\\n \\\"plateNumber\\\": \\\"粤F33445\\\",\\n \\\"brand\\\": \\\"福特\\\"\\n },\\n {\\n \\\"violationCount\\\": 2,\\n \\\"id\\\": 7,\\n \\\"plateNumber\\\": \\\"粤G55667\\\",\\n \\\"brand\\\": \\\"雪佛兰\\\"\\n },\\n {\\n \\\"violationCount\\\": 6,\\n \\\"id\\\": 8,\\n \\\"plateNumber\\\": \\\"粤H77889\\\",\\n \\\"brand\\\": \\\"宝马\\\"\\n },\\n {\\n \\\"violationCount\\\": 1,\\n \\\"id\\\": 9,\\n \\\"plateNumber\\\": \\\"粤J99001\\\",\\n \\\"brand\\\": \\\"奔驰\\\"\\n },\\n {\\n \\\"violationCount\\\": 0,\\n \\\"id\\\": 10,\\n \\\"plateNumber\\\": \\\"粤K11223\\\",\\n \\\"brand\\\": \\\"奥迪\\\"\\n },\\n {\\n \\\"violationCount\\\": 3,\\n \\\"id\\\": 11,\\n \\\"plateNumber\\\": \\\"粤L44556\\\",\\n \\\"brand\\\": \\\"起亚\\\"\\n },\\n {\\n \\\"violationCount\\\": 2,\\n \\\"id\\\": 12,\\n \\\"plateNumber\\\": \\\"粤M77889\\\",\\n \\\"brand\\\": \\\"现代\\\"\\n },\\n {\\n \\\"violationCount\\\": 5,\\n \\\"id\\\": 13,\\n \\\"plateNumber\\\": \\\"粤N99002\\\",\\n \\\"brand\\\": \\\"路虎\\\"\\n },\\n {\\n \\\"violationCount\\\": 1,\\n \\\"id\\\": 14,\\n \\\"plateNumber\\\": \\\"粤P22334\\\",\\n \\\"brand\\\": \\\"沃尔沃\\\"\\n },\\n {\\n \\\"violationCount\\\": 4,\\n \\\"id\\\": 15,\\n \\\"plateNumber\\\": \\\"粤Q55667\\\",\\n \\\"brand\\\": \\\"马自达\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":527.999993949144,\"height\":183.00000186920772},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"showIndex\":true,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"__system\":true,\"textAlign\":\"center\",\"name\":\"序号\",\"width\":41,\"textStyle\":{\"fontSize\":20,\"fontGradient\":{\"endColor\":\"#FF4500\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFD700\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"key\":\"__index__\"},{\"marginRight\":18,\"compose\":{\"contentStyle\":{\"fontSize\":16,\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontStyle\":\"italic\",\"fontColor\":\"#D1D1D1\"},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":true},\"name\":\"车牌号\",\"width\":90,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"plateNumber\",\"marginLeft\":0},{\"compose\":{\"contentStyle\":{\"marginRight\":3,\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#00D4FF\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#00FFDC\",\"fontWeight\":\"bold\",\"marginLeft\":4},\"suffixStyle\":{\"fontSize\":12,\"fontColor\":\"#FFFFFF\"},\"prefix\":\"本月\",\"prefixStyle\":{\"fontSize\":14,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"次\",\"enabled\":true},\"name\":\"违规次数\",\"width\":100,\"textStyle\":{\"fontColor\":\"#FFFFFF\"},\"key\":\"violationCount\"}],\"itemsPerRow\":2,\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"indexFieldStyle\":{\"width\":28,\"textStyle\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#F54100\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#D4BA28\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\"},\"marginLeft\":15},\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"drag/lib/img/scrollList-bg-02.png\",\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"height\":83},\"marginLeft\":6}}},{\"component\":\"JMultipleBar\",\"visible\":true,\"w\":577.9999974835168,\"x\":1324.3610810627897,\"h\":170.00000417221224,\"i\":\"efa6188b-fbb1-4951-be55-6bb81fc45461\",\"y\":603.1219127277654,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"60.887302%\",\"left\":\"69.616704%\",\"width\":\"30.383295999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"17.162105%\"},\"componentName\":\"对比柱形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"集团公司\\\",\\n \\\"value\\\": 4800,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"建设集团\\\",\\n \\\"value\\\": 3900,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"投资集团\\\",\\n \\\"value\\\": 3200,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"万众集团\\\",\\n \\\"value\\\": 3950,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"新成集团\\\",\\n \\\"value\\\": 3600,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"海洋集团\\\",\\n \\\"value\\\": 2100,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"集团公司\\\",\\n \\\"value\\\": 3200,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"建设集团\\\",\\n \\\"value\\\": 3500,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"投资集团\\\",\\n \\\"value\\\": 4200,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"万众集团\\\",\\n \\\"value\\\": 3200,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"新成集团\\\",\\n \\\"value\\\": 2300,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"海洋集团\\\",\\n \\\"value\\\": 1900,\\n \\\"type\\\": \\\"加油金额\\\"\\n }\\n]\",\"size\":{\"width\":577.9999974835168,\"height\":170.00000417221224},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"name\":\"单位(元)\",\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#006CFF00\",\"color\":\"#006CFFB3\"},{\"color1\":\"#00D8FF00\",\"color\":\"#00D8FFB3\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":43,\"left\":9,\"bottom\":18,\"right\":13,\"containLabel\":true},\"series\":[{\"barWidth\":15,\"barGap\":\"34%\",\"itemStyle\":{\"borderRadius\":4}}],\"legend\":{\"r\":53},\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":225.00000639287214,\"x\":1062.0164140689094,\"h\":121.99999134059341,\"i\":\"es-drager-1763027856495-4\",\"y\":810.5076231258076,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.823627%\",\"left\":\"55.826226999999996%\",\"width\":\"11.827407999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.316332999999997%\"},\"componentName\":\"滚动列表(单行)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": 1,\\n \\\"projectName\\\": \\\"京A12345\\\"\\n },\\n {\\n \\\"id\\\": 2,\\n \\\"projectName\\\": \\\"京A1236\\\"\\n },\\n {\\n \\\"id\\\": 3,\\n \\\"projectName\\\": \\\"京A12311\\\"\\n },\\n {\\n \\\"id\\\": 4,\\n \\\"projectName\\\": \\\"京A12377\\\"\\n },\\n {\\n \\\"id\\\": 5,\\n \\\"projectName\\\": \\\"京A12895\\\"\\n },\\n {\\n \\\"id\\\": 6,\\n \\\"projectName\\\": \\\"京A12822\\\"\\n },\\n {\\n \\\"id\\\": 7,\\n \\\"projectName\\\": \\\"京A12811\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":225.00000639287214,\"height\":121.99999134059341},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"marginRight\":0,\"gridGap\":0,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"marginRight\":15,\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"name\":\"项目名称\",\"width\":91,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#4A90E2\"},\"key\":\"projectName\",\"marginLeft\":13}],\"itemsPerRow\":2,\"borderRadius\":7,\"autoScrollEnabled\":true,\"showHeader\":false,\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"marginRight\":0,\"backgroundColor\":\"#FFFFFF00\",\"backgroundImg\":\"drag/lib/img/scrollList-bg-01.png\",\"isMultiline\":false,\"alternateBackgroundColor\":\"#F8F9FA00\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":11,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"color\",\"marginTop\":0,\"height\":29,\"marginLeft\":0},\"marginLeft\":0}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":221.00000687106677,\"x\":824.1148801954979,\"h\":121.99999134059341,\"i\":\"es-drager-1763027851750-3\",\"y\":811.9027002790828,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.964465%\",\"left\":\"43.320634%\",\"width\":\"11.617143%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.316332999999997%\"},\"componentName\":\"滚动列表(单行)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": 1,\\n \\\"projectName\\\": \\\"京A12345\\\"\\n },\\n {\\n \\\"id\\\": 2,\\n \\\"projectName\\\": \\\"京A1236\\\"\\n },\\n {\\n \\\"id\\\": 3,\\n \\\"projectName\\\": \\\"京A12311\\\"\\n },\\n {\\n \\\"id\\\": 4,\\n \\\"projectName\\\": \\\"京A12377\\\"\\n },\\n {\\n \\\"id\\\": 5,\\n \\\"projectName\\\": \\\"京A12895\\\"\\n },\\n {\\n \\\"id\\\": 6,\\n \\\"projectName\\\": \\\"京A12822\\\"\\n },\\n {\\n \\\"id\\\": 7,\\n \\\"projectName\\\": \\\"京A12811\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":221.00000687106677,\"height\":121.99999134059341},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"marginRight\":0,\"gridGap\":0,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"marginRight\":15,\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"name\":\"项目名称\",\"width\":91,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#4A90E2\"},\"key\":\"projectName\",\"marginLeft\":13}],\"itemsPerRow\":2,\"borderRadius\":7,\"autoScrollEnabled\":true,\"showHeader\":false,\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"marginRight\":0,\"backgroundColor\":\"#FFFFFF00\",\"backgroundImg\":\"drag/lib/img/scrollList-bg-01.png\",\"isMultiline\":false,\"alternateBackgroundColor\":\"#F8F9FA00\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":11,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"color\",\"marginTop\":0,\"height\":29,\"marginLeft\":0},\"marginLeft\":0}}},{\"component\":\"JImg\",\"visible\":true,\"w\":219.99999272290734,\"x\":1049.3552308870337,\"h\":131.9999857592265,\"i\":\"es-drager-1763027670564-2\",\"y\":802.6060887086645,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.025939%\",\"left\":\"55.160675999999995%\",\"width\":\"11.564576%\",\"position\":\"absolute\",\"config\":{},\"height\":\"13.325868%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":219.9999927229073,\"height\":131.9999857592265},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/组-155_02_1763554354943.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":219.99999272290734,\"x\":813.9859298452752,\"h\":131.9999857592265,\"i\":\"es-drager-1763027644239-1\",\"y\":804.0011658619395,\"orderNum\":1076.8710433763197,\"angle\":359.2911223131234,\"groupStyle\":{\"transform\":\"rotate(359.2911223131234deg)\",\"top\":\"81.166777%\",\"left\":\"42.788193%\",\"width\":\"11.564576%\",\"position\":\"absolute\",\"config\":{},\"height\":\"13.325868%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":219.9999927229073,\"height\":131.9999857592265},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/组-155_02_1763554354943.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":226.00000151742083,\"x\":576.0844149954746,\"h\":121.99999134059341,\"i\":\"b7038d3a-2f13-4991-8a6b-eee2677ff7a3\",\"y\":810.7655338021864,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.849664%\",\"left\":\"30.282601%\",\"width\":\"11.879974%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.316332999999997%\"},\"componentName\":\"滚动列表(单行)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"id\\\": 1,\\n \\\"projectName\\\": \\\"京A12345\\\"\\n },\\n {\\n \\\"id\\\": 2,\\n \\\"projectName\\\": \\\"京A1236\\\"\\n },\\n {\\n \\\"id\\\": 3,\\n \\\"projectName\\\": \\\"京A12311\\\"\\n },\\n {\\n \\\"id\\\": 4,\\n \\\"projectName\\\": \\\"京A12377\\\"\\n },\\n {\\n \\\"id\\\": 5,\\n \\\"projectName\\\": \\\"京A12895\\\"\\n },\\n {\\n \\\"id\\\": 6,\\n \\\"projectName\\\": \\\"京A12822\\\"\\n },\\n {\\n \\\"id\\\": 7,\\n \\\"projectName\\\": \\\"京A12811\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":226.0000015174208,\"height\":121.99999134059341},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"marginRight\":4,\"gridGap\":0,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"marginRight\":0,\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"name\":\"项目名称\",\"width\":84,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#4A90E2\"},\"key\":\"projectName\",\"marginLeft\":12}],\"itemsPerRow\":2,\"borderRadius\":7,\"autoScrollEnabled\":true,\"showHeader\":false,\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"marginRight\":0,\"backgroundColor\":\"#FFFFFF00\",\"backgroundImg\":\"drag/lib/img/scrollList-bg-01.png\",\"isMultiline\":false,\"alternateBackgroundColor\":\"#F8F9FA00\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":11,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"color\",\"marginTop\":3,\"height\":29,\"marginLeft\":0},\"marginLeft\":4}}},{\"component\":\"JListProgress\",\"visible\":true,\"w\":508.99999146466575,\"x\":0,\"h\":230.9999948897365,\"i\":\"896fc5fc-bdf6-404a-8bc0-65a92a72d801\",\"y\":759.5545097716221,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.679729%\",\"left\":\"0%\",\"width\":\"26.756224%\",\"position\":\"absolute\",\"config\":{},\"height\":\"23.320271000000005%\"},\"componentName\":\"列表进度图\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"机扫车\\\",\\n \\\"total\\\": 200,\\n \\\"date\\\": \\\"2025-12-31\\\",\\n \\\"endLabel\\\": \\\"2025-06-15\\\",\\n \\\"value\\\": 52\\n },\\n {\\n \\\"title\\\": \\\"洒水车\\\",\\n \\\"total\\\": 110,\\n \\\"date\\\": \\\"2025-11-20\\\",\\n \\\"endLabel\\\": \\\"2025-05-30\\\",\\n \\\"value\\\": 68\\n },\\n {\\n \\\"title\\\": \\\"垃圾车\\\",\\n \\\"total\\\": 150,\\n \\\"date\\\": \\\"2026-01-15\\\",\\n \\\"endLabel\\\": \\\"2025-07-01\\\",\\n \\\"value\\\": 98\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":508.99999146466575,\"height\":230.9999948897365},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":1200,\"dataType\":1,\"h\":325,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"beginFields\":[{\"name\":\"车辆类型\",\"style\":{\"letterSpacing\":0,\"fontSize\":19,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\"}],\"endCurrent\":0,\"endInfo\":{\"width\":103},\"scroll\":{\"count\":1,\"interval\":3000,\"enabled\":false,\"direction\":\"down\"},\"centerTopFields\":[{\"marginRight\":0,\"isUseExceedFillColor\":true,\"compose\":{\"contentStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":false},\"showPercentage\":true,\"name\":\"进度值\",\"width\":100,\"style\":{\"fontSize\":20,\"fontGradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#00D4FF\",\"direction\":\"to bottom\"},\"fontColor\":\"#4A90E2\"},\"key\":\"value\"}],\"body\":{\"gradient\":{\"type\":\"linear\"}},\"endFields\":[{\"compose\":{\"contentStyle\":{\"fontSize\":24,\"fontColor\":\"#02DEFF\",\"fontWeight\":\"bold\"},\"suffixStyle\":{\"fontSize\":14,\"fontColor\":\"#FFFFFF\"},\"suffix\":\"辆\",\"enabled\":true},\"name\":\"值\",\"style\":{\"letterSpacing\":0,\"fontSize\":18,\"fontGradient\":{\"endColor\":\"#00FFFF\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#A0D8FF\"},\"fontStyle\":\"normal\",\"fontColor\":\"#50E3C2\",\"fontWeight\":\"bold\"},\"key\":\"value\"}],\"progressSection\":{\"marginRight\":5,\"marginLeft\":0},\"bar\":{\"border\":{\"padding\":8,\"color\":\"#4ECBFC5E\",\"width\":2,\"enabled\":false},\"total\":{\"field\":\"total\",\"type\":\"field\",\"value\":0},\"borderRadius\":6,\"background\":{\"color\":\"#5A97FC4C\",\"gradient\":{\"endColor\":\"#07203D\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#143B6E\"}},\"indicatorColor\":\"#DCFEFFB5\",\"exceed\":{\"indicatorColor\":\"#F74B0C\",\"fill\":{\"color\":\"#FFB347\",\"gradient\":{\"endColor\":\"#FF4500\",\"enabled\":true,\"startColor\":\"#FFD700\",\"direction\":\"to right\"}},\"percent\":70,\"enabled\":true},\"indicatorSize\":15,\"fill\":{\"color\":\"#33C9FF\",\"gradient\":{\"endColor\":\"#24E5F1\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#C5FDFE\"}},\"valueField\":\"value\",\"height\":4},\"centerTopInfo\":{\"layout\":\"horizontal\"},\"centerTopCurrent\":0,\"row\":{\"marginRight\":0,\"padding\":\"0 0\",\"marginBottom\":0,\"marginTop\":8,\"height\":38,\"marginLeft\":14},\"beginInfo\":{\"layout\":\"vertical\",\"width\":100}}}},{\"visible\":true,\"h\":39.0000029965315,\"i\":\"es-drager-1763022886545-18\",\"orderNum\":70,\"component\":\"JText\",\"w\":194.00000534297763,\"x\":1692.3135351127542,\"y\":583.2930619922697,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"58.885509%\",\"left\":\"88.958587%\",\"width\":\"10.197853999999998%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.937189%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"单位维修保养/加油统计\\\"\\n}\",\"size\":{\"width\":194.00000534297763,\"height\":39.0000029965315},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":59.00000173934264,\"i\":\"es-drager-1763022875670-17\",\"orderNum\":70,\"component\":\"JText\",\"w\":221.00000687106677,\"x\":1658.2572768485109,\"y\":396.0363205335455,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"39.981275%\",\"left\":\"87.168377%\",\"width\":\"11.617143%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.95626%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"月度维修保养/加油统计\\\"\\n}\",\"size\":{\"width\":221.00000687106677,\"height\":59.000001739342636},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":58.999991833797594,\"i\":\"es-drager-1763022862487-16\",\"orderNum\":70,\"component\":\"JText\",\"w\":152.00000085221566,\"x\":1696.2408644413595,\"y\":49.11954743833212,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"4.958792999999999%\",\"left\":\"89.165032%\",\"width\":\"7.990071%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.956259%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"车辆类型占比\\\"\\n}\",\"size\":{\"width\":152.00000085221566,\"height\":58.99999183379759},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":50.999996298891155,\"i\":\"es-drager-1763022854128-15\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":1630.5316006633993,\"y\":258.15824566602794,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.061993%\",\"left\":\"85.710942%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.148631%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"公务用车占比情况\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":50.999996298891155},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":34.99999532353323,\"i\":\"es-drager-1763022836670-14\",\"orderNum\":70,\"component\":\"JText\",\"w\":23.999997130832348,\"x\":1315.267814130949,\"y\":843.1054841533569,\"angle\":0.15840966236248732,\"groupStyle\":{\"transform\":\"rotate(0.15840966236248732deg)\",\"top\":\"85.114497%\",\"left\":\"69.138705%\",\"width\":\"1.26159%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.5333739999999993%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"加油频次排名\\\"\\n}\",\"size\":{\"width\":23.999997130832348,\"height\":34.99999532353323},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":43.999995253075504,\"i\":\"es-drager-1763022832978-13\",\"orderNum\":70,\"component\":\"JText\",\"w\":151.000005727667,\"x\":1381.2350000239387,\"y\":260.81944958716105,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.330651%\",\"left\":\"72.606353%\",\"width\":\"7.937505%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.441956%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"作业车占比情况\\\"\\n}\",\"size\":{\"width\":151.000005727667,\"height\":43.999995253075504},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022817890-12\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":1071.16465923592,\"y\":743.3399645960995,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"75.042813%\",\"left\":\"56.307116%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"接打电话提醒车辆\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022815330-11\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":845.9243465916059,\"y\":740.9366812568901,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.800193%\",\"left\":\"44.467076%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"抽烟提醒车辆\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022811067-10\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":600.4260951996247,\"y\":741.065611831217,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"74.813209%\",\"left\":\"31.562151999999998%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"离线超24h车辆\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022781375-9\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":281.2349983621805,\"y\":696.622472209391,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"70.326516%\",\"left\":\"14.783471%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"异常车辆告警\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"component\":\"JImg\",\"visible\":true,\"w\":16.999993211770217,\"x\":354.1266073685814,\"h\":46.0000139478922,\"i\":\"es-drager-1763022760154-8\",\"y\":655.3329213418754,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"66.158189%\",\"left\":\"18.61511%\",\"width\":\"0.8936259999999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.643865%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":16.999993211770217,\"height\":46.0000139478922},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/车辆_03_1763551149225.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022745315-7\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":407.97590538011707,\"y\":48.498212219738235,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"4.8960669999999995%\",\"left\":\"21.445766%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"单位车辆统计\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022735003-6\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":310.4846940545368,\"y\":414.40677787182847,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"41.835838%\",\"left\":\"16.321018%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"作业车辆总数\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022730046-5\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":181.46947631997648,\"y\":333.5040806749409,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"33.668423%\",\"left\":\"9.539171%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"车辆类型分布\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"component\":\"JImg\",\"visible\":true,\"w\":1.0000141481594371,\"x\":1589.9882911213126,\"h\":118.99998806223101,\"i\":\"es-drager-1763022540256-4\",\"y\":269.2954065615573,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"27.186328999999997%\",\"left\":\"83.579732%\",\"width\":\"0.05256700000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.013472%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":1.0000141481594371,\"height\":118.99998806223101},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/按钮2-拷贝_04_1763551243662.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":144.00000180860488,\"x\":188.52286049090264,\"h\":26.999999788626795,\"i\":\"es-drager-1763022387955-2\",\"y\":377.0445378822139,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"38.063987%\",\"left\":\"9.909941%\",\"width\":\"7.569541%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.725746%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":144.00000180860488,\"height\":26.999999788626795},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/样式4_03_1763551206808.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1763022379005-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":53.59139492011721,\"y\":409.47125090244776,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"41.337579%\",\"left\":\"2.817099%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"公务用车\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"component\":\"JImg\",\"visible\":true,\"w\":523.9999944273385,\"x\":0,\"h\":94.0000069684209,\"i\":\"0733c434-458b-4ffb-8b77-d924eb978688\",\"y\":397.43137787573045,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"40.122111%\",\"left\":\"0%\",\"width\":\"27.54471799999999%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.489635%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":523.9999944273385,\"height\":94.00000696842092},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/样式4_07_1763551173622.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1762513598535-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":566.4987789692847,\"y\":690.6787588651661,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"69.726477%\",\"left\":\"29.778719999999996%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"异常车辆告警\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":38.99999309098645,\"i\":\"es-drager-1762513583317-1\",\"orderNum\":70,\"component\":\"JImg\",\"w\":720.0000090430243,\"x\":559.6597528446893,\"y\":703.371991737613,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"71.007904%\",\"left\":\"29.419218%\",\"width\":\"37.847705%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.937188%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":720.0000090430243,\"height\":38.99999309098645},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"e0162ae2-0026-4aa2-8823-29fdbe42f92d\"},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1756453828584-23\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":1309.960131603751,\"y\":0,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0%\",\"left\":\"68.8597%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"统计与异常告警\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":20,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":39.99999748562227,\"i\":\"es-drager-1756453836255-24\",\"orderNum\":70,\"component\":\"JImg\",\"w\":549.9999818072682,\"x\":1292.0973225248063,\"y\":11.266111280946149,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.137354%\",\"left\":\"67.920719%\",\"width\":\"28.91144%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.038142%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":549.9999818072682,\"height\":39.99999748562227},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"e0162ae2-0026-4aa2-8823-29fdbe42f92d\"},{\"component\":\"JBar3d\",\"visible\":true,\"w\":527.999993949144,\"x\":12.266138880304801,\"h\":240.99999921391452,\"i\":\"e2ddc1ba-4820-4916-bb9a-384fce062348\",\"y\":88.13717239950154,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"8.897761%\",\"left\":\"0.6447849999999999%\",\"width\":\"27.754983%\",\"position\":\"absolute\",\"config\":{},\"height\":\"24.329807%\"},\"componentName\":\"3d柱形图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":332,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/33/chart\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"集团公司\\\",\\n \\\"value\\\": 290\\n },\\n {\\n \\\"name\\\": \\\"建设集团\\\",\\n \\\"value\\\": 270\\n },\\n {\\n \\\"name\\\": \\\"投资集团\\\",\\n \\\"value\\\": 450\\n },\\n {\\n \\\"name\\\": \\\"新成集团\\\",\\n \\\"value\\\": 380\\n },\\n {\\n \\\"name\\\": \\\"万众集团\\\",\\n \\\"value\\\": 320\\n },\\n {\\n \\\"name\\\": \\\"海洋集团\\\",\\n \\\"value\\\": 320\\n }\\n]\",\"size\":{\"width\":527.999993949144,\"height\":240.99999921391452},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":490,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\",\"show\":true},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA57\"}},\"show\":true,\"splitLine\":{\"lineStyle\":{\"color\":\"#4A90E23D\"},\"show\":true},\"name\":\"单位(辆)\",\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA57\"}},\"show\":true,\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":35,\"left\":0,\"bottom\":\"36\",\"right\":\"1\",\"containLabel\":true},\"series\":[{\"color\":\"#00DEFFCC\",\"id\":\"barTopColor\"},{\"color\":\"#187DCB00\",\"id\":\"barBottomColor\"},{\"color\":\"#115BA6\",\"id\":\"barColor\",\"label\":{\"color\":\"#EEF1FA\",\"show\":false}},{\"color\":\"#04113300\",\"id\":\"shadowColor\"},{\"color\":\"#142F5A00\",\"id\":\"shadowTopColor\"}],\"tooltip\":{\"show\":true},\"body\":{\"gradient\":{\"type\":\"linear\"}},\"graphic\":{\"children\":[{\"style\":{\"fill\":\"#3F486700\"}}]},\"extraInfo\":{\"endColor\":\"#39CEFFCC\",\"enabledGradient\":true,\"direction\":\"to top\",\"startColor\":\"#2D48AD19\"}}}},{\"component\":\"JGaoDeMap\",\"visible\":true,\"w\":756.0000047392731,\"x\":546.8147445117935,\"h\":499.9999982869137,\"i\":\"93d7dd28-4cba-4221-9f22-a4441b3176a2\",\"y\":158.48299534078043,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.999422%\",\"left\":\"28.744004%\",\"width\":\"39.74009000000001%\",\"position\":\"absolute\",\"config\":{},\"height\":\"50.47677800000001%\"},\"componentName\":\"高德地图\",\"config\":{\"chartData\":\"[{\\\"longitude\\\":116.391466,\\\"latitude\\\":39.907425,\\\"title\\\":\\\"湘B.8J1VS\\\",\\\"company\\\":\\\"小米集团\\\",\\\"type\\\":\\\"轿车\\\",\\\"status\\\":\\\"正常\\\",\\\"imgUrl\\\":\\\"drag/lib/img/car-normal.png\\\",\\\"time\\\":\\\"2025-08-08 10:00:00\\\"},{\\\"longitude\\\":116.382122,\\\"latitude\\\":39.913553,\\\"title\\\":\\\"鄂B.8J1VS\\\",\\\"company\\\":\\\"苹果集团\\\",\\\"type\\\":\\\"货车\\\",\\\"status\\\":\\\"异常\\\",\\\"imgUrl\\\":\\\"drag/lib/img/trucks-abnormal.png\\\",\\\"time\\\":\\\"2025-08-05 10:00:00\\\"},{\\\"longitude\\\":116.411722,\\\"latitude\\\":39.908215,\\\"title\\\":\\\"京B.8J1VS\\\",\\\"company\\\":\\\"香蕉集团\\\",\\\"type\\\":\\\"轿车\\\",\\\"status\\\":\\\"异常\\\",\\\"imgUrl\\\":\\\"drag/lib/img/car-abnormal.png\\\",\\\"time\\\":\\\"2025-08-05 10:00:00\\\"},{\\\"longitude\\\":116.40683,\\\"latitude\\\":39.909795,\\\"title\\\":\\\"沪B.8J1VS\\\",\\\"company\\\":\\\"香蕉集团\\\",\\\"type\\\":\\\"货车\\\",\\\"status\\\":\\\"正常\\\",\\\"imgUrl\\\":\\\"drag/lib/img/trucks-normal.png\\\",\\\"time\\\":\\\"2025-08-05 10:00:00\\\"}]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":756.0000047392731,\"height\":499.99999828691375},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":500,\"h\":500,\"timeOut\":0,\"option\":{\"gaoSecurityJsCode\":\"f908485801bab40e98ae972553d70f8d\",\"gaodeKey\":\"f599beada738982465764ddd0b1dae4e\",\"mapStyle\":\"darkblue\",\"marker\":{\"imgUrl\":\"drag/lib/img/car-normal.png\",\"offsetX\":-41,\"offsetY\":-60,\"imgField\":\"imgUrl\",\"width\":82,\"showImgField\":true,\"height\":120},\"infoWindow\":{\"titleTextAlign\":\"center\",\"padding\":0,\"titleLetterSpacing\":0,\"titleFontWeight\":\"bold\",\"titleField\":\"title\",\"contentLineHeight\":16,\"showContent\":true,\"bgImgUrl\":\"drag/lib/img/bg.png\",\"contentFieldMapping\":[{\"name\":\"公司\",\"key\":\"company\"},{\"name\":\"类型\",\"key\":\"type\"},{\"name\":\"状态\",\"key\":\"status\"},{\"name\":\"时间\",\"key\":\"time\"}],\"show\":true,\"contentFontSize\":11,\"contentColor\":\"#fff\",\"offsetX\":0,\"titleLineHeight\":23,\"contentPaddingTop\":8,\"offsetY\":-55,\"bgColor\":\"#000000\",\"titleColor\":\"#fff\",\"titleFontSize\":16,\"showTitle\":true,\"width\":224,\"contentPaddingLeft\":25,\"event\":\"hover\",\"height\":113}}}},{\"visible\":true,\"h\":59.99999622843341,\"i\":\"es-drager-1756453224591-18\",\"orderNum\":70,\"component\":\"JText\",\"w\":170.0000082121453,\"x\":28.25791876766705,\"y\":2.669405712431711,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0.2694860000000002%\",\"left\":\"1.485413%\",\"width\":\"8.936264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.057212999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"单位与车辆分类概览\\\"\\n}\",\"size\":{\"width\":170.0000082121453,\"height\":59.9999962284334},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"b9b6326a-6ea6-4872-8b63-5ae7f1fdb450\"},{\"visible\":true,\"h\":38.99999309098645,\"i\":\"es-drager-1756453227423-19\",\"orderNum\":70,\"component\":\"JImg\",\"w\":584.9999823789683,\"x\":0,\"y\":14.201649272145232,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"1.4337070000000007%\",\"left\":\"0%\",\"width\":\"30.751259000000008%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.937188%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":584.9999823789683,\"height\":38.99999309098645},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题背景_1756451499148.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}},\"key\":\"e0162ae2-0026-4aa2-8823-29fdbe42f92d\"},{\"component\":\"JMultipleLine\",\"visible\":true,\"w\":557.0000047499412,\"x\":1320.2056205936456,\"h\":161.00000424267,\"i\":\"62a2a0e8-6118-4798-826d-3fe1b7d357f0\",\"y\":425.2629580765655,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.931808%\",\"left\":\"69.398267%\",\"width\":\"29.279405000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"16.253523%\"},\"componentName\":\"对比折线图\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"markLineConfig\":{\"show\":false,\"markLine\":[]},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"分组\"},{\"mapping\":\"\",\"filed\":\"维度\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":300,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/26/stackedBar\",\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 2000,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 1900,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 4000,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 3800,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 800,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 420,\\n \\\"type\\\": \\\"维修保养金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"1月\\\",\\n \\\"value\\\": 20,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"2月\\\",\\n \\\"value\\\": 210,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"3月\\\",\\n \\\"value\\\": 220,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"4月\\\",\\n \\\"value\\\": 580,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"5月\\\",\\n \\\"value\\\": 500,\\n \\\"type\\\": \\\"加油金额\\\"\\n },\\n {\\n \\\"name\\\": \\\"6月\\\",\\n \\\"value\\\": 800,\\n \\\"type\\\": \\\"加油金额\\\"\\n }\\n]\",\"size\":{\"width\":557.0000047499412,\"height\":161.00000424267},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"yAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"splitLine\":{\"lineStyle\":{\"color\":\"#8F8D8D\"},\"show\":false,\"interval\":2},\"name\":\"单位(元)\",\"yUnit\":\"\",\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"customColor\":[{\"color1\":\"#1e90ff\",\"color\":\"#FE398B\"},{\"color1\":\"#90ee90\",\"color\":\"#7A53F0\"}],\"xAxis\":{\"axisLabel\":{\"color\":\"#EEF1FA\"},\"axisLine\":{\"lineStyle\":{\"color\":\"#EEF1FA\"}},\"nameTextStyle\":{\"color\":\"#EEF1FA\"}},\"grid\":{\"top\":35,\"left\":0,\"bottom\":18,\"right\":1,\"containLabel\":true},\"series\":[{\"areaStyleOpacity\":0.1,\"symbol\":\"circle\",\"symbolSize\":15,\"lineType\":\"area\",\"label\":{\"position\":\"top\"}}],\"legend\":{\"r\":48},\"tooltip\":{\"axisPointer\":{\"label\":{\"backgroundColor\":\"#333\",\"show\":true},\"type\":\"shadow\"},\"trigger\":\"axis\",\"textStyle\":{\"color\":\"#EEF1FA\"}},\"title\":{\"textAlign\":\"left\",\"show\":true,\"text\":\"\",\"textStyle\":{\"color\":\"#EEF1FA\",\"fontWeight\":\"normal\"},\"subtextStyle\":{\"color\":\"#EEF1FA\"}},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}},{\"component\":\"JImg\",\"visible\":true,\"w\":483.00000408473613,\"x\":1357.151241934818,\"h\":136.0000033377698,\"i\":\"es-drager-1763022408549-3\",\"y\":260.5615091941472,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.304611%\",\"left\":\"71.34036000000002%\",\"width\":\"25.389502000000004%\",\"position\":\"absolute\",\"config\":{},\"height\":\"13.729684%\"},\"componentName\":\"图片\",\"config\":{\"size\":{\"width\":483.00000408473613,\"height\":136.0000033377698},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/按钮2-拷贝_02_1763551275631.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":229.00000591467753,\"x\":563.4232127899882,\"h\":131.9999857592265,\"i\":\"4887df15-f532-4ef1-8a4e-6d84f34242fa\",\"y\":805.3962331096695,\"orderNum\":1076.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.307614%\",\"left\":\"29.617049%\",\"width\":\"12.037673%\",\"position\":\"absolute\",\"config\":{},\"height\":\"13.325868%\"},\"componentName\":\"图片\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":229.00000591467753,\"height\":131.9999857592265},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/组-155_02_1763554354943.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}},{\"component\":\"JSemiGauge\",\"visible\":true,\"w\":238.00000008283695,\"x\":259.42555098968336,\"h\":215.0000038199236,\"i\":\"es-drager-1763544356457-1\",\"y\":482.71159297288773,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"48.731452000000004%\",\"left\":\"13.637029999999998%\",\"width\":\"12.510769%\",\"position\":\"absolute\",\"config\":{},\"height\":\"21.705015000000003%\"},\"componentName\":\"半圆仪表盘\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataType\":1,\"h\":430,\"viewLoading\":true,\"timeOut\":0,\"chartData\":\"[\\n {\\n \\\"total\\\": 385,\\n \\\"used\\\": 300\\n }\\n]\",\"size\":{\"width\":238.00000008283695,\"height\":215.0000038199236},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":500,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"titleSuffix\":\"辆\",\"customAttr\":{\"innerCircle\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":0,\"y2\":1,\"x2\":1,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#BD8D35\"},{\"offset\":1,\"color\":\"#BD8D35\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":15}},\"name\":\"内部小圆\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"outerScale\":{\"axisLabel\":{\"color\":\"#FFBB38\",\"distance\":-52,\"show\":true,\"fontSize\":14},\"min\":0,\"max\":100,\"axisLine\":{\"show\":false},\"name\":\"外部刻度\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"splitNumber\":2,\"detail\":{\"show\":false},\"type\":\"gauge\",\"radius\":67},\"innerProgress\":{\"axisLabel\":{\"show\":false},\"animationDuration\":2000,\"pointer\":{\"show\":true,\"length\":74,\"width\":3,\"itemStyle\":{\"color\":\"#BD8D35\"}},\"data\":[{\"name\":\"去年优良率\",\"value\":44}],\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#BD8D35\"],[1,\"#BD8D35\"]],\"width\":1}},\"name\":\"内部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"detail\":{\"offsetCenter\":[0,50],\"show\":false,\"textStyle\":{\"padding\":[0,0,0,0],\"color\":\"#FFFFFF\",\"fontSize\":18,\"fontWeight\":\"normal\"}},\"type\":\"gauge\",\"radius\":30,\"title\":{\"offsetCenter\":[0,26],\"show\":true,\"textStyle\":{\"color\":\"#FFFFFF\",\"fontSize\":16,\"fontWeight\":\"normal\"}}},\"outerProgress\":{\"axisLabel\":{\"show\":false},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,\"#BD8D35\"],[1,\"#BD8D35\"]],\"width\":2}},\"name\":\"外部进度条\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"type\":\"gauge\",\"radius\":80},\"basic\":{\"startAngle\":180,\"endAngle\":0},\"innerShadow\":{\"axisLabel\":{\"show\":false},\"customGradient\":{\"endColor\":\"#BD8D35\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to right\",\"startColor\":\"#BD8D3500\"},\"axisLine\":{\"lineStyle\":{\"color\":[[0.44,{\"x\":0,\"y\":1,\"y2\":0,\"x2\":0,\"global\":false,\"colorStops\":[{\"offset\":0,\"color\":\"#2de69600\"},{\"offset\":1,\"color\":\"#2de696\"}],\"type\":\"linear\"}],[1,\"rgba(0,0,0,0)\"]],\"width\":100}},\"name\":\"内部阴影\",\"axisTick\":{\"show\":false},\"splitLine\":{\"show\":false},\"itemStyle\":{\"show\":false},\"type\":\"gauge\",\"radius\":80}},\"valuePrefix\":\"已使用:\",\"titlePrefix\":\"车辆总数:\",\"valueMapping\":\"used\",\"titleMapping\":\"total\",\"valueSuffix\":\"辆\"}}}]},\"component\":\"JGroup\",\"w\":1902.3610785463065,\"x\":6,\"y\":69,\"componentName\":\"车辆\",\"pageCompId\":\"1151112776978583552\",\"equalProportion\":true,\"key\":\"7ea6e027-6c73-41e9-80ee-ab95267ce97a\",\"group\":true},{\"visible\":true,\"h\":1065,\"i\":\"es-drager-1756453564096-22\",\"props\":{\"elements\":[{\"component\":\"JScrollList\",\"visible\":true,\"w\":339,\"x\":1653.2942555685815,\"h\":157,\"i\":\"es-drager-1763357806623-2\",\"y\":873.5580304806565,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"82.02422821414615%\",\"left\":\"75.60621250542819%\",\"width\":\"15.502688618805859%\",\"position\":\"absolute\",\"config\":{},\"height\":\"14.741784037558686%\"},\"componentName\":\"滚动列表(多行+序号)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"外刚性还本付息约\\\",\\n \\\"value\\\": 131.73\\n },\\n {\\n \\\"title\\\": \\\"归还田迪投控本息合计\\\",\\n \\\"value\\\": 11.04\\n },\\n {\\n \\\"title\\\": \\\"压降天保担保额度\\\",\\n \\\"value\\\": 36.81\\n },\\n {\\n \\\"title\\\": \\\"压降隐性债务规模\\\",\\n \\\"value\\\": 24.64\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":339,\"height\":157},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"marginRight\":0,\"showIndex\":true,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"__system\":true,\"textAlign\":\"left\",\"name\":\"标题\",\"width\":233,\"textStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\",\"marginLeft\":14},{\"marginRight\":18,\"compose\":{\"contentStyle\":{\"fontSize\":18,\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontStyle\":\"normal\",\"fontColor\":\"#04FAFD\",\"fontWeight\":\"bold\",\"marginLeft\":0},\"suffixStyle\":{\"fontColor\":\"#FFFFFF\"},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"suffix\":\" 亿元\",\"enabled\":true},\"name\":\"金额\",\"width\":193,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"value\",\"marginLeft\":0}],\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"indexFieldStyle\":{\"width\":28,\"textStyle\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#F54100\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#D4BA28\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\"},\"marginLeft\":15},\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"\",\"isMultiline\":true,\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"marginTop\":1,\"height\":41},\"marginLeft\":4}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":339,\"x\":1310.3048065650646,\"h\":157,\"i\":\"es-drager-1763357453690-1\",\"y\":869.8886283704572,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"81.6796834150664%\",\"left\":\"59.92108381092284%\",\"width\":\"15.502688618805859%\",\"position\":\"absolute\",\"config\":{},\"height\":\"14.741784037558686%\"},\"componentName\":\"滚动列表(多行+序号)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"实现资产盘活收入化债约\\\",\\n \\\"value\\\": 1.2\\n },\\n {\\n \\\"title\\\": \\\"经营收入化债约\\\",\\n \\\"value\\\": 4.46\\n },\\n {\\n \\\"title\\\": \\\"财政资金到位化解隐性债务\\\",\\n \\\"value\\\": 6.61\\n },\\n {\\n \\\"title\\\": \\\"再融资债券资金到位化解隐性债务\\\",\\n \\\"value\\\": 12.37\\n },\\n {\\n \\\"title\\\": \\\"到期隐性债务偿还本息合计\\\",\\n \\\"value\\\": 36.99\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":339,\"height\":157},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"marginRight\":0,\"showIndex\":true,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"__system\":true,\"textAlign\":\"left\",\"name\":\"标题\",\"width\":233,\"textStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\",\"marginLeft\":14},{\"marginRight\":18,\"compose\":{\"contentStyle\":{\"fontFamily\":\"\",\"fontSize\":18,\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontStyle\":\"normal\",\"fontColor\":\"#04FAFD\",\"fontWeight\":\"bold\",\"marginLeft\":0},\"suffixStyle\":{\"fontColor\":\"#FFFFFF\"},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"suffix\":\" 亿元\",\"enabled\":true},\"name\":\"金额\",\"width\":193,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"value\",\"marginLeft\":0}],\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"indexFieldStyle\":{\"width\":28,\"textStyle\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#F54100\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#D4BA28\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\"},\"marginLeft\":15},\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"\",\"isMultiline\":true,\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"marginTop\":1,\"height\":41},\"marginLeft\":4}}},{\"component\":\"JScrollList\",\"visible\":true,\"w\":451.99999999999994,\"x\":276.01406799531065,\"h\":157,\"i\":\"ec3ee79c-04b7-462a-89dd-b8b1e95104d8\",\"y\":457.2626025791325,\"orderNum\":1074.87104337632,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"42.9354556412331%\",\"left\":\"12.622301329030117%\",\"width\":\"20.670251491741144%\",\"position\":\"absolute\",\"config\":{},\"height\":\"14.741784037558686%\"},\"componentName\":\"滚动列表(多行+序号)\",\"config\":{\"chartData\":\"[\\n {\\n \\\"title\\\": \\\"保区创新创业园\\\",\\n \\\"content\\\": \\\"按期完工\\\"\\n },\\n {\\n \\\"title\\\": \\\"中蓝白领公寓\\\",\\n \\\"content\\\": \\\"已建设完成,进行验收与运营准备\\\"\\n },\\n {\\n \\\"title\\\": \\\"合成生物中心\\\",\\n \\\"content\\\": \\\"竣工备案,确定运营方案,项目整体具备移交条件\\\"\\n },\\n {\\n \\\"title\\\": \\\"中欧核心区综合服务中心\\\",\\n \\\"content\\\": \\\"竣工验收\\\"\\n },\\n {\\n \\\"title\\\": \\\"汀园一期二标项目\\\",\\n \\\"content\\\": \\\"结构封顶\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":451.99999999999994,\"height\":157},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":515,\"dataType\":1,\"h\":220,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"marginRight\":0,\"showIndex\":true,\"backgroundColor\":\"#FFFFFF00\",\"fieldMapping\":[{\"__system\":true,\"textAlign\":\"left\",\"name\":\"标题\",\"width\":162,\"textStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"fontStyle\":\"normal\",\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"normal\"},\"key\":\"title\",\"marginLeft\":14},{\"marginRight\":18,\"compose\":{\"contentStyle\":{\"fontSize\":14,\"fontGradient\":{\"endColor\":\"#FFFFFF\",\"enabled\":false,\"startColor\":\"#000000\"},\"fontStyle\":\"normal\",\"fontColor\":\"#D1D1D1\",\"fontWeight\":\"normal\",\"marginLeft\":0},\"prefix\":\"\",\"prefixStyle\":{\"fontColor\":\"#FFFFFF\"},\"enabled\":true},\"name\":\"内容\",\"width\":257,\"textStyle\":{\"fontSize\":16,\"fontWeight\":\"bold\",\"fontColor\":\"#FFFFFF\"},\"key\":\"content\",\"marginLeft\":0}],\"borderRadius\":8,\"autoScrollEnabled\":true,\"showHeader\":false,\"indexFieldStyle\":{\"width\":28,\"textStyle\":{\"fontSize\":21,\"fontGradient\":{\"endColor\":\"#F54100\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#D4BA28\",\"direction\":\"to bottom\"},\"fontStyle\":\"italic\",\"fontColor\":\"#FFFFFF\"},\"marginLeft\":15},\"header\":{\"padding\":\"8px 0\",\"backgroundColor\":\"#1890FF\",\"textAlign\":\"center\",\"fontSize\":16,\"fontColor\":\"#FFFFFF\",\"fontWeight\":\"bold\"},\"row\":{\"backgroundColor\":\"#FFFFFF\",\"backgroundImg\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题行-1_21_1763554527340.png\",\"isMultiline\":true,\"alternateBackgroundColor\":\"#F8F9FA\",\"backgroundSize\":\"100% 100%\",\"marginBottom\":10,\"backgroundRepeat\":\"no-repeat\",\"backgroundType\":\"image\",\"marginTop\":1,\"height\":41},\"marginLeft\":4}}},{\"component\":\"JText\",\"visible\":true,\"w\":665.0000038733882,\"x\":475.7444323786635,\"h\":58.9999989,\"i\":\"es-drager-1763042970956-9\",\"y\":960.5416156499999,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"90.191701%\",\"left\":\"21.756099697041183%\",\"width\":\"30.410879031132236%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.539906%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"成功谋划实施总投资113亿元的港口型国家粮食物流枢纽项目,启动建设总投资12亿元的40万吨粮食筒仓项目,并获取1.9亿元超长期特别国债资金支持\\\"\\n}\",\"size\":{\"width\":665.0000038733882,\"height\":58.9999989},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"textAlign\":\"left\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":665.0000038733882,\"x\":451.688168158851,\"h\":49.00000034999999,\"i\":\"es-drager-1763042965821-8\",\"y\":889.63892115,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"83.534171%\",\"left\":\"20.655991220547158%\",\"width\":\"30.410879031132236%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.6009389999999994%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"推动分布式光伏项目并网发电、氢能重卡扩量布局、50MW防波堤风电项目开工建设等工作,构建绿色新能源产业发展体系\\\"\\n}\",\"size\":{\"width\":665.0000038733882,\"height\":49.00000035},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"textAlign\":\"left\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":660.9999992373974,\"x\":398.5111289953106,\"h\":43.000003349999986,\"i\":\"es-drager-1763042960045-7\",\"y\":812.4056217,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.282218%\",\"left\":\"18.22417092608577%\",\"width\":\"30.227956239552455%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"累计中标市场化项目14个,签约合同额超9100万元,展现出城市运营业务的品牌实力和市场竞争力\\\"\\n}\",\"size\":{\"width\":660.9999992373974,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"textAlign\":\"center\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1763042876096-6\",\"orderNum\":70,\"component\":\"JText\",\"w\":567.9999905685813,\"x\":250.33644724267296,\"y\":408.4407912,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"38.351248%\",\"left\":\"11.44804717268814%\",\"width\":\"25.97500586805127%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"坚持以重点项目为抓手,加快建设进度,着力改善资产质量,提升经营发展后劲\\\"\\n}\",\"size\":{\"width\":567.9999905685813,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1763042867476-5\",\"orderNum\":70,\"component\":\"JText\",\"w\":440.0000091524032,\"x\":274.5216791377492,\"y\":319.94137929407975,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"30.041444065171806%\",\"left\":\"12.554053424142353%\",\"width\":\"20.121484171567655%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"长三角项目7个,完成内资到位5000万元,新增税收收入3亿元\\\"\\n}\",\"size\":{\"width\":440.0000091524032,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1763042861732-4\",\"orderNum\":70,\"component\":\"JText\",\"w\":482.99999117174673,\"x\":282.11840171512296,\"y\":283.2239145483587,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"26.59379479327312%\",\"left\":\"12.901456446680637%\",\"width\":\"22.087901079709642%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"注册资本1亿元以上的大好项目2个,实体项目3个,京冀项目11个,\\\"\\n}\",\"size\":{\"width\":482.99999117174673,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1763042856445-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":313.9999987538101,\"x\":320.1019879624853,\"y\":251.5709253381594,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"23.621683130343605%\",\"left\":\"14.638470341130239%\",\"width\":\"14.359422439485975%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"招引项目公司18家\\\"\\n}\",\"size\":{\"width\":313.9999987538101,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1763042838775-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":394.9999935146542,\"x\":271.9894450914419,\"y\":211.05509700000002,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"19.81738%\",\"left\":\"12.438252728184123%\",\"width\":\"18.06360443624784%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"预计实现固投6.81亿元,固投指标完成率达103.81%\\\"\\n}\",\"size\":{\"width\":394.9999935146542,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1763042831263-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":443.9999929214536,\"x\":284.77960655744425,\"y\":180.79717965,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"16.976261%\",\"left\":\"13.023155060312986%\",\"width\":\"20.304406008888794%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"新增盘活面积4.92万平米,实现新增资产盘活收入4.25亿元\\\"\\n}\",\"size\":{\"width\":443.9999929214536,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763042137685-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":400.00000452637744,\"x\":1500.7467639279014,\"y\":774.793677,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"72.75058%\",\"left\":\"68.63011733585486%\",\"width\":\"18.292258164287208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"建设开发一期工程5.6平方公里造路部分2个立项的决算工作。\\\"\\n}\",\"size\":{\"width\":400.00000452637744,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763042130020-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":456.999992338218,\"x\":1502.1418325375146,\"y\":748.3341141,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"70.266114%\",\"left\":\"68.69391472304278%\",\"width\":\"20.89890436582908%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"启动海阳经济区(北区)生活区域市政基础设施工程和新成集团1区工业区\\\"\\n}\",\"size\":{\"width\":456.999992338218,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":40.000004849999996,\"i\":\"es-drager-1763042123591-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":178.00000806565058,\"x\":1666.737377284291,\"y\":721.74561165,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"67.769541%\",\"left\":\"76.2209751308658%\",\"width\":\"8.14005515984281%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.7558689999999997%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"启动2个立项的决算工作\\\"\\n}\",\"size\":{\"width\":178.00000806565058,\"height\":40.000004849999996},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041888225-9\",\"orderNum\":70,\"component\":\"JText\",\"w\":99.00000518933179,\"x\":1886.9132275656505,\"y\":681.10082385,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.953129%\",\"left\":\"86.28975875414821%\",\"width\":\"4.527334081741519%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"决算总金额\\\"\\n}\",\"size\":{\"width\":99.0000051893318,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041884664-8\",\"orderNum\":70,\"component\":\"JText\",\"w\":99.00000518933179,\"x\":1788.1559048909733,\"y\":679.8347092500001,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.834245%\",\"left\":\"81.77352270030615%\",\"width\":\"4.527334081741519%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"决算审核\\\"\\n}\",\"size\":{\"width\":99.0000051893318,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041880252-7\",\"orderNum\":70,\"component\":\"JText\",\"w\":71.99999998007033,\"x\":1871.3329382022273,\"y\":652.8593292279601,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.301345467414095%\",\"left\":\"85.57726207393569%\",\"width\":\"3.292606431401352%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"14\\\"\\n}\",\"size\":{\"width\":71.99999998007033,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"fontFamily\":\"DIGITALDREAMFAT\",\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041872463-6\",\"orderNum\":70,\"component\":\"JText\",\"w\":46.00000114654163,\"x\":1920.9695138089094,\"y\":653.1172353,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.325562%\",\"left\":\"87.84717468672096%\",\"width\":\"2.1036097175207886%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"亿元\\\"\\n}\",\"size\":{\"width\":46.00000114654163,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43,\"i\":\"es-drager-1763041867629-5\",\"orderNum\":70,\"component\":\"JText\",\"w\":42,\"x\":1814.6154563487687,\"y\":649.3188808499999,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"60.968909%\",\"left\":\"82.98353505205681%\",\"width\":\"1.9206870855157703%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558685446009%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项\\\"\\n}\",\"size\":{\"width\":42,\"height\":43},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041862452-4\",\"orderNum\":70,\"component\":\"JText\",\"w\":71.99999998007033,\"x\":1780.3012813640096,\"y\":649.1899256720399,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"60.956800532585895%\",\"left\":\"81.41432570102454%\",\"width\":\"3.292606431401352%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"5\\\"\\n}\",\"size\":{\"width\":71.99999998007033,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"fontFamily\":\"DIGITALDREAMFAT\",\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041848451-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":99.00000518933179,\"x\":1643.9472355287223,\"y\":682.4958993,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.084122%\",\"left\":\"75.17876725117546%\",\"width\":\"4.527334081741519%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"审核金额约\\\"\\n}\",\"size\":{\"width\":99.0000051893318,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041841952-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":46.00000114654163,\"x\":1685.600214823564,\"y\":655.778436,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.57544%\",\"left\":\"77.08358485605304%\",\"width\":\"2.1036097175207886%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"亿元\\\"\\n}\",\"size\":{\"width\":46.00000114654163,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041833962-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":71.99999998007033,\"x\":1637.3587135990622,\"y\":654.38336055,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.444447000000004%\",\"left\":\"74.8774698944389%\",\"width\":\"3.292606431401352%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"33\\\"\\n}\",\"size\":{\"width\":71.99999998007033,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"fontFamily\":\"DIGITALDREAMFAT\",\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041616450-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":71.99999998007033,\"x\":1543.7948233774912,\"y\":655.778436,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.57544%\",\"left\":\"70.59873285588682%\",\"width\":\"3.292606431401352%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"146\\\"\\n}\",\"size\":{\"width\":71.99999998007033,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"fontFamily\":\"DIGITALDREAMFAT\",\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":22,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763041602493-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":46.00000114654163,\"x\":1594.5685637579134,\"y\":657.17351145,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.706433%\",\"left\":\"72.9206487471251%\",\"width\":\"2.1036097175207886%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"项\\\"\\n}\",\"size\":{\"width\":46.00000114654163,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"component\":\"JText\",\"visible\":true,\"w\":400.00000452637744,\"x\":1633.5920373159436,\"h\":43.000003349999986,\"i\":\"es-drager-1763039434020-6\",\"y\":289.10316405000003,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"27.145837%\",\"left\":\"74.70521735890784%\",\"width\":\"18.292258164287208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"完善员工考核评价机制,完善选人用人机制,推进企业人才\\\"\\n}\",\"size\":{\"width\":400.00000452637744,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":1,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":407.000007422626,\"x\":1565.3505526565066,\"h\":43.000003349999986,\"i\":\"es-drager-1763039429945-5\",\"y\":236.05509870000003,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"22.164798%\",\"left\":\"71.58449025696014%\",\"width\":\"18.61237281098715%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"编制《集团改革深化提升行动方案》,进一步完善\\\"\\n}\",\"size\":{\"width\":407.0000074226259,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":1,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":325.0000062860492,\"x\":1547.7538337977724,\"h\":43.000003349999986,\"i\":\"es-drager-1763039424189-4\",\"y\":181.74090809999998,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"17.064874%\",\"left\":\"70.7797809555453%\",\"width\":\"14.862459877765685%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"航海低空经济基地项目,获银行贷款10亿元\\\"\\n}\",\"size\":{\"width\":325.00000628604914,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":1,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":362.9999981606096,\"x\":1503.0129020896836,\"h\":44.9999988,\"i\":\"es-drager-1756453093407-15\",\"y\":132.5638941,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.447314%\",\"left\":\"68.73374929541033%\",\"width\":\"16.600224012126933%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"新评级主体“海阳发展集团”已具备AAA评级落地条件\\\"\\n}\",\"size\":{\"width\":362.99999816060955,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"horseLamp\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":1,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":395.9999998903869,\"x\":1478.4408071078544,\"h\":105.00000075,\"i\":\"es-drager-1763039350346-3\",\"y\":104.25790874999998,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"9.789475%\",\"left\":\"67.61005154551353%\",\"width\":\"18.10933537270744%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.859155%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":395.9999998903869,\"height\":105.00000075},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_53_1763550154234.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":395.9999998903869,\"x\":1516.553334390973,\"h\":105.00000075,\"i\":\"es-drager-1763039347418-2\",\"y\":153.76553070000003,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"14.438078%\",\"left\":\"69.35296199668146%\",\"width\":\"18.10933537270744%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.859155%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":395.9999998903869,\"height\":105.00000075},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_53_1763550154234.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":395.9999998903869,\"x\":1560.9964739953105,\"h\":105.00000075,\"i\":\"es-drager-1763039343497-1\",\"y\":207.07150710000002,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"19.443334%\",\"left\":\"71.38537543186779%\",\"width\":\"18.10933537270744%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.859155%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":395.9999998903869,\"height\":105.00000075},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_53_1763550154234.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":56.00000230304804,\"x\":1774.8417328223916,\"h\":28.0000002,\"i\":\"es-drager-1763039242410-2\",\"y\":489.28722029999994,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.94246199999999%\",\"left\":\"81.16465702539044%\",\"width\":\"2.560916219340899%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.629108%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":56.000002303048035,\"height\":28.000000200000002},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_42_1763550192876.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":86.00000577256742,\"x\":1769.7772638323565,\"h\":28.0000002,\"i\":\"es-drager-1763039228598-1\",\"y\":428.51347455,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"40.236007%\",\"left\":\"80.93305559243437%\",\"width\":\"3.932835724801236%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.629108%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":86.00000577256742,\"height\":28.000000200000002},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_42_1763550192876.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":112.99999011488859,\"x\":1772.4384455844076,\"h\":28.0000002,\"i\":\"es-drager-1763039126579-4\",\"y\":359.00585550000005,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"33.70947%\",\"left\":\"81.05475315013376%\",\"width\":\"5.167562420882767%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.629108%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":112.9999901148886,\"height\":28.000000200000002},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_42_1763550192876.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":395.9999998903869,\"x\":1597.8428996811256,\"h\":105.00000075,\"i\":\"es-drager-1763039122726-3\",\"y\":260.3774835,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"24.44859%\",\"left\":\"73.07038624049068%\",\"width\":\"18.10933537270744%\",\"position\":\"absolute\",\"config\":{},\"height\":\"9.859155%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":395.9999998903869,\"height\":105.00000075},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_53_1763550154234.png\"},\"izRotate\":false}}},{\"visible\":true,\"h\":57.00000345,\"i\":\"es-drager-1763039115053-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":175.00000980539266,\"x\":1587.2297652203986,\"y\":482.7069261,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"45.324594%\",\"left\":\"72.58504075726304%\",\"width\":\"8.00286330472264%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.352113%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"开展供应链公司业务部负责人“揭榜挂帅”公开竞聘\\\"\\n}\",\"size\":{\"width\":175.00000980539266,\"height\":57.00000345},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763038916108-2\",\"orderNum\":70,\"component\":\"JText\",\"w\":99.00000518933179,\"x\":1546.5849930087923,\"y\":681.3587349000001,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.977346000000004%\",\"left\":\"70.72632911248775%\",\"width\":\"4.527334081741519%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"结算审核\\\"\\n}\",\"size\":{\"width\":99.0000051893318,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":43.000003349999986,\"i\":\"es-drager-1763038893302-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":593.0000038933175,\"x\":1478.34348369871,\"y\":619.4478354,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"58.164116%\",\"left\":\"67.60560088325013%\",\"width\":\"27.11827259973087%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"坚持问题导向,瞄准债务症结,持续推动土地开发成本决算,为存量债务化解探索可行路径\\\"\\n}\",\"size\":{\"width\":593.0000038933176,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":375.1957872215708,\"h\":59.94860505,\"i\":\"es-drager-1763038751694-10\",\"y\":952.2192264,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"89.410256%\",\"left\":\"17.15794531086652%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"建设集团\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":353.67174706330593,\"h\":59.94860505,\"i\":\"es-drager-1763038747182-9\",\"y\":876.2520521999999,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"82.277188%\",\"left\":\"16.17363707372123%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"海洋集团\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":323.28487052227433,\"h\":59.94860505,\"i\":\"es-drager-1763038741952-8\",\"y\":806.6154723,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"75.738542%\",\"left\":\"14.784025613208815%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"投资集团\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":295.43023890973035,\"h\":59.94860505,\"i\":\"es-drager-1763038737302-7\",\"y\":740.7772575,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"69.55655%\",\"left\":\"13.510215346303756%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"万众集团\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":265.17234092614297,\"h\":59.94860505,\"i\":\"es-drager-1763038700270-6\",\"y\":683.93082705,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.218857%\",\"left\":\"12.12650215840066%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"新成集团\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":47.00000752227431,\"x\":327.1512432731535,\"h\":36.99999569999999,\"i\":\"es-drager-1763038660630-5\",\"y\":954.7033921500001,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"89.643511%\",\"left\":\"14.960837332504143%\",\"width\":\"2.1493406539803885%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.4741779999999993%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":47.00000752227431,\"height\":36.99999569999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_11_1763550014908.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":47.00000752227431,\"x\":300.5627341248534,\"h\":36.99999569999999,\"i\":\"es-drager-1763038655573-4\",\"y\":888.86517735,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"83.461519%\",\"left\":\"13.744927662402764%\",\"width\":\"2.1493406539803885%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.4741779999999993%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":47.00000752227431,\"height\":36.99999569999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_11_1763550014908.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":47.00000752227431,\"x\":276.506469905041,\"h\":36.99999569999999,\"i\":\"es-drager-1763038650929-3\",\"y\":823.02696255,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"77.279527%\",\"left\":\"12.644819185908743%\",\"width\":\"2.1493406539803885%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.4741779999999993%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":47.00000752227431,\"height\":36.99999569999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_11_1763550014908.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":47.00000752227431,\"x\":256.24855221102,\"h\":36.99999569999999,\"i\":\"es-drager-1763038646390-2\",\"y\":753.39038265,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"70.740881%\",\"left\":\"11.718411545567136%\",\"width\":\"2.1493406539803885%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.4741779999999993%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":47.00000752227431,\"height\":36.99999569999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_11_1763550014908.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":47.00000752227431,\"x\":237.25675698124275,\"h\":36.99999569999999,\"i\":\"es-drager-1763038641612-1\",\"y\":698.94724185,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"65.628849%\",\"left\":\"10.849904502029203%\",\"width\":\"2.1493406539803885%\",\"position\":\"absolute\",\"config\":{},\"height\":\"3.4741779999999993%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":47.00000752227431,\"height\":36.99999569999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_11_1763550014908.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":322,\"x\":1559.964839345252,\"h\":169,\"i\":\"es-drager-1763038468350-11\",\"y\":860.6236798322392,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.80973519551542%\",\"left\":\"71.3381981140264%\",\"width\":\"14.725267655620906%\",\"position\":\"absolute\",\"config\":{},\"height\":\"15.868544600938966%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":322,\"height\":169},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_37_1763549942040.png\"},\"izRotate\":false}}},{\"visible\":true,\"h\":59.94860505,\"i\":\"es-drager-1763038388788-10\",\"orderNum\":70,\"component\":\"JText\",\"w\":173.74037695955448,\"x\":1796.92848376143,\"y\":809.8487640279601,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"76.0421374674141%\",\"left\":\"82.17469838942888%\",\"width\":\"7.9452594823537614%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"全口径化债\\\"\\n}\",\"size\":{\"width\":173.74037695955448,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"25ca1904-5d69-45c5-86bd-b235d3ac9f2a\"},{\"component\":\"JImg\",\"visible\":true,\"w\":217,\"x\":1276.483013909144,\"h\":188,\"i\":\"es-drager-1763038374225-9\",\"y\":853.1559168483586,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.10853679327312%\",\"left\":\"58.37439142132238%\",\"width\":\"9.92354994183148%\",\"position\":\"absolute\",\"config\":{},\"height\":\"17.652582159624412%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":217.00000000000003,\"height\":187.99999999999997},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_37_1763549942040.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":527.0000004337631,\"x\":1484.2555886670573,\"h\":85.99999605000001,\"i\":\"es-drager-1763038372052-8\",\"y\":707.6811257999999,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"66.448932%\",\"left\":\"67.87596525612915%\",\"width\":\"24.100049878569862%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.075117%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":527.0000004337631,\"height\":85.99999605000001},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_33_1763550324778.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":409,\"x\":225.73272507796014,\"h\":152,\"i\":\"es-drager-1763038238876-6\",\"y\":446.86048514999993,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"41.95873099999999%\",\"left\":\"10.322903091321901%\",\"width\":\"18.70383376133214%\",\"position\":\"absolute\",\"config\":{},\"height\":\"14.272300469483568%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":409,\"height\":152},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_30_1763550061880.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":181.00000632590854,\"x\":1833.833553545135,\"h\":256.99999575,\"i\":\"es-drager-1763038235098-5\",\"y\":320.37748545,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"30.082393000000003%\",\"left\":\"83.86239103046746%\",\"width\":\"8.277247014962983%\",\"position\":\"absolute\",\"config\":{},\"height\":\"24.131455%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":181.00000632590854,\"height\":256.99999575},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_49_1763549897591.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":355.9999952643611,\"x\":384.2555866529894,\"h\":63.00000044999999,\"i\":\"es-drager-1763038220734-4\",\"y\":949.7678732999999,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"89.180082%\",\"left\":\"17.572255781468634%\",\"width\":\"16.28010936542699%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.915492999999999%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":355.9999952643611,\"height\":63.00000044999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_25_1763550350084.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":355.9999952643611,\"x\":366.529913887456,\"h\":63.00000044999999,\"i\":\"es-drager-1763038208032-3\",\"y\":873.8006991000001,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"82.047014%\",\"left\":\"16.761649334734386%\",\"width\":\"16.28010936542699%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.915492999999999%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":355.9999952643611,\"height\":63.00000044999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_25_1763550350084.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":355.9999952643611,\"x\":334.87691488218053,\"h\":63.00000044999999,\"i\":\"es-drager-1763038199346-2\",\"y\":805.4302444499999,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"75.627253%\",\"left\":\"15.314137277418286%\",\"width\":\"16.28010936542699%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.915492999999999%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":355.9999952643611,\"height\":63.00000044999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_25_1763550350084.png\"},\"izRotate\":false}}},{\"component\":\"JOrbitRing\",\"visible\":true,\"w\":538.9999934747948,\"x\":886.7444235703398,\"h\":134.99999640000004,\"i\":\"5c18670b-4206-42f4-8884-1592d70ec919\",\"y\":435.73857300000003,\"orderNum\":1074.8710433763197,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"40.91442%\",\"left\":\"40.551394345349465%\",\"width\":\"24.648817299050542%\",\"position\":\"absolute\",\"config\":{},\"height\":\"12.676056000000004%\"},\"componentName\":\"轨道环形文字\",\"config\":{\"chartData\":\"[\\n {\\n \\\"name\\\": \\\"经营发展\\\",\\n \\\"value\\\": 1\\n },\\n {\\n \\\"name\\\": \\\"全口径化债\\\",\\n \\\"value\\\": 2\\n },\\n {\\n \\\"name\\\": \\\"土地成本决算\\\",\\n \\\"value\\\": 3\\n },\\n {\\n \\\"name\\\": \\\"创新赋能\\\",\\n \\\"value\\\": 4\\n },\\n {\\n \\\"name\\\": \\\"重点项目\\\",\\n \\\"value\\\": 5\\n },\\n {\\n \\\"name\\\": \\\"培育增量\\\",\\n \\\"value\\\": 6\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":538.9999934747948,\"height\":134.99999640000004},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"dataMapping\":[{\"mapping\":\"name\",\"filed\":\"标题\"},{\"mapping\":\"value\",\"filed\":\"id(唯一标识)\"},{\"mapping\":\"imgSrc\",\"filed\":\"图片地址\"}],\"background\":\"#FFFFFF00\",\"w\":750,\"dataType\":1,\"h\":540,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"sharedSpeed\":1,\"color\":\"#ffffff\",\"showOrbit\":true,\"planetHeight\":80,\"letterSpacing\":0,\"tilt\":0.55,\"fontGradient\":{\"endColor\":\"#0066cc\",\"type\":\"linear\",\"enabled\":false,\"direction\":\"to right\",\"startColor\":\"#ffffff\"},\"fontStyle\":\"normal\",\"orbitRadius\":290,\"sun\":{\"repeat\":\"no-repeat\",\"width\":300,\"bgImg\":\"\",\"position\":\"center\",\"height\":300},\"sharedDepth\":10,\"planetWidth\":80,\"imgTextMode\":true,\"showType\":\"1\",\"fontSize\":14,\"items\":[{\"bgColor\":\"#31AEFD00\",\"name\":\"经营发展\",\"bgImg\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/jyfz_1763550629518.png\",\"value\":1},{\"bgColor\":\"#409EFF00\",\"name\":\"全口径化债\",\"bgImg\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/系统入口页_05_1763550642627.png\",\"value\":2},{\"bgColor\":\"#E6A23C00\",\"name\":\"土地成本决算\",\"bgImg\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/系统入口页_11_1763550652200.png\",\"value\":3},{\"bgColor\":\"#F56C6C00\",\"name\":\"创新赋能\",\"bgImg\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/系统入口页_13_1763550665155.png\",\"value\":4},{\"bgColor\":\"#67C23A00\",\"name\":\"重点项目\",\"bgImg\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/系统入口页_07_1763550686864.png\",\"value\":5},{\"bgColor\":\"#90939900\",\"name\":\"培育增量\",\"bgImg\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/系统入口页_09_1763550696802.png\",\"value\":6}],\"fontWeight\":\"normal\",\"fontColor\":\"#FFFFFF\",\"direction\":1}}},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1763036807357-1\",\"orderNum\":70,\"component\":\"JText\",\"w\":417.0000085791326,\"x\":939.2344604613129,\"y\":132.55568295,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.446543%\",\"left\":\"42.951797582843234%\",\"width\":\"19.06967931280727%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"服务产业发展,构筑美好生活\\\"\\n}\",\"size\":{\"width\":417.00000857913255,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#00FFDE\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":28,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"component\":\"JImg\",\"visible\":true,\"w\":507.0000189876905,\"x\":901.3481934109026,\"h\":177.9999997500001,\"i\":\"es-drager-1763036557092-11\",\"y\":64.0000035,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"6.009390000000001%\",\"left\":\"41.219234158030744%\",\"width\":\"23.18543782918827%\",\"position\":\"absolute\",\"config\":{},\"height\":\"16.713615000000008%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":507.0000189876905,\"height\":177.99999975000006},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_46_1763550136221.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":502.00000797596726,\"x\":894.7596865756153,\"h\":361.0000041,\"i\":\"es-drager-1763036542751-10\",\"y\":167.56389435,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"15.733699000000001%\",\"left\":\"40.91793749156958%\",\"width\":\"22.9567841011489%\",\"position\":\"absolute\",\"config\":{},\"height\":\"33.896714%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":502.0000079759672,\"height\":361.00000410000007},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":2500,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/控股业务综合监管版本6改_01_03_1763550233848.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":486.99999580773743,\"x\":886.0258078833527,\"h\":250.99999874999997,\"i\":\"es-drager-1763036539797-9\",\"y\":204.41031705,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"19.193457%\",\"left\":\"40.51853158655315%\",\"width\":\"22.270823871289416%\",\"position\":\"absolute\",\"config\":{},\"height\":\"23.568074999999997%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":486.99999580773743,\"height\":250.99999874999997},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":2500,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/系统入口页_03_1763550411407.png\"},\"izRotate\":false}}},{\"visible\":true,\"h\":59.94860505,\"i\":\"es-drager-1763036485878-7\",\"orderNum\":70,\"component\":\"JText\",\"w\":173.74037695955448,\"x\":1826.178203524619,\"y\":565.6166436,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"53.109544%\",\"left\":\"83.51230693714584%\",\"width\":\"7.9452594823537614%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"土地成本决算\\\"\\n}\",\"size\":{\"width\":173.74037695955448,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"25ca1904-5d69-45c5-86bd-b235d3ac9f2a\"},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":289.3575628364595,\"h\":59.94860505,\"i\":\"es-drager-1763036478147-6\",\"y\":609.3587282999999,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"57.216782%\",\"left\":\"13.232507953245376%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"培育增量\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":295.81711198124265,\"h\":59.94860505,\"i\":\"es-drager-1763036469399-5\",\"y\":360.06213314999997,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"33.808651%\",\"left\":\"13.527907301355837%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"重点项目\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":44.00000926201641,\"x\":327.92498941617816,\"h\":29.000003249999995,\"i\":\"es-drager-1763033026815-2\",\"y\":131.23329375,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"12.322375000000001%\",\"left\":\"14.99622124260831%\",\"width\":\"2.012148798860219%\",\"position\":\"absolute\",\"config\":{},\"height\":\"2.7230049999999997%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":44.00000926201641,\"height\":29.000003249999995},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_14_1763550425365.png\"},\"izRotate\":false}}},{\"visible\":true,\"h\":43.9623054,\"i\":\"es-drager-1756453034262-13\",\"orderNum\":70,\"component\":\"JText\",\"w\":173.74037695955448,\"x\":1626.6084358347014,\"y\":418.26377610000003,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"39.273594%\",\"left\":\"74.38585275949329%\",\"width\":\"7.9452594823537614%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.127916%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"交流轮岗16人\\\"\\n}\",\"size\":{\"width\":173.74037695955448,\"height\":43.962305400000005},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"2cb7d6a8-c524-4c08-8ce5-532022d7dd70\"},{\"visible\":true,\"h\":44.9999988,\"i\":\"es-drager-1756452592342-9\",\"orderNum\":70,\"component\":\"JText\",\"w\":313.9999987538101,\"x\":369.7385606828839,\"y\":123.95075985,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"11.638569%\",\"left\":\"16.908382821923905%\",\"width\":\"14.359422439485975%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.225352%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"营业收88亿元 占全年目标80%,同比↑33%\\\"\\n}\",\"size\":{\"width\":313.9999987538101,\"height\":44.9999988},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":16,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"4555e8cb-7e7b-4006-8039-984e9f9f66bd\"},{\"visible\":true,\"h\":59.94860505,\"i\":\"es-drager-1756451958689-3\",\"orderNum\":70,\"component\":\"JText\",\"w\":173.74037695955448,\"x\":1781.9929530902693,\"y\":61.8300021,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.805634%\",\"left\":\"81.49168694001405%\",\"width\":\"7.9452594823537614%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"创新赋能\\\"\\n}\",\"size\":{\"width\":173.74037695955448,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}},\"key\":\"25ca1904-5d69-45c5-86bd-b235d3ac9f2a\"},{\"component\":\"JText\",\"visible\":true,\"w\":173.74037695955448,\"x\":1605.4161800257914,\"h\":43.9623054,\"i\":\"es-drager-1756453142151-16\",\"y\":348.89330775,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"32.759935%\",\"left\":\"73.41671723460948%\",\"width\":\"7.9452594823537614%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.127916%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"各级人员外部引进10人\\\"\\n}\",\"size\":{\"width\":173.74037695955448,\"height\":43.962305400000005},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":59.00000056330598,\"x\":1738.5111378985932,\"h\":64.0000035,\"i\":\"3a8b52fe-94c4-4df7-85d7-f31fbecb03d3\",\"y\":655.53575445,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"61.552653%\",\"left\":\"79.50323549017034%\",\"width\":\"2.6981080744610706%\",\"position\":\"absolute\",\"config\":{},\"height\":\"6.009390000000001%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":59.00000056330598,\"height\":64.0000035},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_03 2_1763550452700.png\"},\"izRotate\":false}}},{\"component\":\"JText\",\"visible\":true,\"w\":537.0000015902696,\"x\":386.8511210111371,\"h\":43.000003349999986,\"i\":\"es-drager-1756452586319-8\",\"y\":749.0914356000001,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"70.337224%\",\"left\":\"17.69095124150927%\",\"width\":\"24.557356380389972%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"居然公寓品牌获新区国资委市场化品牌公寓运营整合主体认定,不断壮大品牌影响力\\\"\\n}\",\"size\":{\"width\":537.0000015902696,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":404.9999946711606,\"x\":353.05978230773735,\"h\":43.000003349999986,\"i\":\"es-drager-1756452491126-7\",\"y\":692.2532163000001,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"65.000302%\",\"left\":\"16.14565153079715%\",\"width\":\"18.52091093806795%\",\"position\":\"absolute\",\"config\":{},\"height\":\"4.037558999999999%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"累计实现营业收入117.54亿元,成为市场化转型发展先锋军\\\"\\n}\",\"size\":{\"width\":404.9999946711606,\"height\":43.000003349999986},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#F0E9E9\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":14,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JText\",\"visible\":true,\"w\":124.68426635052752,\"x\":352.92145536107853,\"h\":59.94860505,\"i\":\"es-drager-1756452434656-5\",\"y\":58.8546264,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"5.526256%\",\"left\":\"16.13932575032033%\",\"width\":\"5.701891908249208%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.628977%\"},\"componentName\":\"文本\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"经营发展\\\"\\n}\",\"size\":{\"width\":124.68426635052754,\"height\":59.94860505},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"color\":\"#FFFFFF\",\"gradient\":{\"endColor\":\"#0085FF\",\"type\":\"linear\",\"enabled\":true,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":0,\"fontSize\":18,\"text\":\"\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}},{\"component\":\"JImg\",\"visible\":true,\"w\":411.8668873558031,\"x\":263.7385734642438,\"h\":108.90662310000002,\"i\":\"a8467ed7-7901-42c0-8679-67cacde2fef7\",\"y\":28.790145000000003,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"2.7033%\",\"left\":\"12.060935047741077%\",\"width\":\"18.834938368949274%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.225974%\"},\"componentName\":\"icon24\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":411.86688735580316,\"height\":108.9066231},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/1-拷贝-7_03_1763549755584.png\"},\"izRotate\":false}},\"key\":\"816e4f26-b5e1-4d4c-a7c5-6511c1db6220\"},{\"component\":\"JImg\",\"visible\":true,\"w\":411.8668873558031,\"x\":209.0375173223916,\"h\":108.90662310000002,\"i\":\"es-drager-1763031897224-3\",\"y\":329.86869090000005,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"30.973586000000005%\",\"left\":\"9.559420474033253%\",\"width\":\"18.834938368949274%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.225974%\"},\"componentName\":\"icon24\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":411.86688735580316,\"height\":108.9066231},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/1-拷贝-7_01_1763549812452.png\"},\"izRotate\":false}},\"key\":\"816e4f26-b5e1-4d4c-a7c5-6511c1db6220\"},{\"component\":\"JImg\",\"visible\":true,\"w\":411.8668873558031,\"x\":198.90857934232127,\"h\":108.90662310000002,\"i\":\"es-drager-1763031894764-2\",\"y\":579.2942575500001,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"54.39382700000001%\",\"left\":\"9.09621760812108%\",\"width\":\"18.834938368949274%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.225974%\"},\"componentName\":\"icon24\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":411.86688735580316,\"height\":108.9066231},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/1-拷贝-7_02_1763549852904.png\"},\"izRotate\":false}},\"key\":\"816e4f26-b5e1-4d4c-a7c5-6511c1db6220\"},{\"component\":\"JImg\",\"visible\":true,\"w\":411.8668873558031,\"x\":1641.0187553710434,\"h\":108.90662310000002,\"i\":\"es-drager-1763031912767-5\",\"y\":778.0750165500001,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"73.058687%\",\"left\":\"75.04484596500777%\",\"width\":\"18.834938368949274%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.225974%\"},\"componentName\":\"icon24\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":411.86688735580316,\"height\":108.9066231},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/1-拷贝-7_06 拷贝_1763549891576.png\"},\"izRotate\":false}},\"key\":\"816e4f26-b5e1-4d4c-a7c5-6511c1db6220\"},{\"component\":\"JImg\",\"visible\":true,\"w\":411.8668873558031,\"x\":1677.8651948253225,\"h\":108.90662310000002,\"i\":\"es-drager-1763031876182-1\",\"y\":536.37514095,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"50.363862999999995%\",\"left\":\"76.7298574032714%\",\"width\":\"18.834938368949274%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.225974%\"},\"componentName\":\"icon24\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":411.86688735580316,\"height\":108.9066231},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/1-拷贝-7_05 拷贝_1763549879336.png\"},\"izRotate\":false}},\"key\":\"816e4f26-b5e1-4d4c-a7c5-6511c1db6220\"},{\"component\":\"JImg\",\"visible\":true,\"w\":411.8668873558031,\"x\":1616.9624966893314,\"h\":108.90662310000002,\"i\":\"es-drager-1763031899241-4\",\"y\":33.59671365,\"orderNum\":70,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"3.1546209999999997%\",\"left\":\"73.94473774177465%\",\"width\":\"18.834938368949274%\",\"position\":\"absolute\",\"config\":{},\"height\":\"10.225974%\"},\"componentName\":\"icon24\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":411.86688735580316,\"height\":108.9066231},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/1-拷贝-7_04_1763549870157.png\"},\"izRotate\":false}},\"key\":\"816e4f26-b5e1-4d4c-a7c5-6511c1db6220\"},{\"component\":\"JImg\",\"visible\":true,\"w\":566.999984192849,\"x\":288.6752764923798,\"h\":92.99999609999999,\"i\":\"es-drager-1763033021068-1\",\"y\":109.70926035000002,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"10.301339000000002%\",\"left\":\"13.201306558728765%\",\"width\":\"25.92927493159169%\",\"position\":\"absolute\",\"config\":{},\"height\":\"8.732394%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":566.999984192849,\"height\":92.99999609999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_19_1763549796825.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":708.0000067596717,\"x\":250.69168603282534,\"h\":360.00000105,\"i\":\"es-drager-1763033033328-3\",\"y\":28.67760645,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"2.692733%\",\"left\":\"11.4642924716529%\",\"width\":\"32.377296893532844%\",\"position\":\"absolute\",\"config\":{},\"height\":\"33.802817%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":708.0000067596717,\"height\":360.00000105},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_01_1763550492678.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":839.9999928118405,\"x\":734.2204008493551,\"h\":668.9999956500001,\"i\":\"es-drager-1763036537556-8\",\"y\":141.23329230000002,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"13.261342%\",\"left\":\"33.57637242460877%\",\"width\":\"38.41374138159623%\",\"position\":\"absolute\",\"config\":{},\"height\":\"62.816901%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":839.9999928118405,\"height\":668.9999956500001},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/系统入口页_04_03_1763550531200.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":659.999992861665,\"x\":1379.2966113616646,\"h\":671.0000017500001,\"i\":\"es-drager-1763038369685-7\",\"y\":22.83938295,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"2.144543%\",\"left\":\"63.07612353661938%\",\"width\":\"30.18222530309287%\",\"position\":\"absolute\",\"config\":{},\"height\":\"63.00469500000001%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":659.999992861665,\"height\":671.0000017500001},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_03_1763550548877.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":731.0000073329425,\"x\":1353.0949544179368,\"h\":270.00000345,\"i\":\"es-drager-1763039541856-9\",\"y\":543.60140025,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"51.042385%\",\"left\":\"61.87790486731145%\",\"width\":\"33.429101752293235%\",\"position\":\"absolute\",\"config\":{},\"height\":\"25.352113%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":731.0000073329423,\"height\":270.00000345},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_06_1763550562377.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":983,\"x\":0,\"h\":304,\"i\":\"es-drager-1763039119090-2\",\"y\":305.95779117995306,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"28.728431096709205%\",\"left\":\"0%\",\"width\":\"44.95322393004767%\",\"position\":\"absolute\",\"config\":{},\"height\":\"28.544600938967136%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":983,\"height\":304},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_04_1763550080999.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":355.9999952643611,\"x\":278.0304868833528,\"h\":63.00000044999999,\"i\":\"es-drager-1763036995887-1\",\"y\":680.21335935,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"63.86979900000001%\",\"left\":\"12.714513465155179%\",\"width\":\"16.28010936542699%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.915492999999999%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":355.9999952643611,\"height\":63.00000044999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_25_1763550350084.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":355.9999952643611,\"x\":307.02228326963655,\"h\":63.00000044999999,\"i\":\"es-drager-1763038194851-1\",\"y\":737.0597898000001,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"69.207492%\",\"left\":\"14.040327010513224%\",\"width\":\"16.28010936542699%\",\"position\":\"absolute\",\"config\":{},\"height\":\"5.915492999999999%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":355.9999952643611,\"height\":63.00000044999999},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_25_1763550350084.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":1049.9999962315355,\"x\":1136.7174715293083,\"h\":194.99999835,\"i\":\"es-drager-1763039539776-8\",\"y\":857.7280172999999,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"80.537842%\",\"left\":\"51.98282303444006%\",\"width\":\"48.01717696555994%\",\"position\":\"absolute\",\"config\":{},\"height\":\"18.309859%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1049.9999962315353,\"height\":194.99999835},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_10_1763549923063.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":992.0000020439625,\"x\":226.2485278745604,\"h\":368.99999655000005,\"i\":\"es-drager-1763043471607-10\",\"y\":684.14067465,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"64.238561%\",\"left\":\"10.346491085848166%\",\"width\":\"45.364799827558485%\",\"position\":\"absolute\",\"config\":{},\"height\":\"34.647887000000004%\"},\"componentName\":\"icon22\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":992.0000020439625,\"height\":368.99999655000005},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/未标题-1_07_1763549998727.png\"},\"izRotate\":false}}},{\"component\":\"JImg\",\"visible\":true,\"w\":2057.9999959525203,\"x\":111.87924914302464,\"h\":1065,\"i\":\"es-drager-1756452975290-11\",\"y\":0,\"orderNum\":70,\"angle\":0,\"groupStyle\":{\"transform\":\"rotate(0deg)\",\"top\":\"0%\",\"left\":\"5.11631021348116%\",\"width\":\"94.11366700517888%\",\"position\":\"absolute\",\"config\":{},\"height\":\"100%\"},\"componentName\":\"icon24\",\"config\":{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":2057.9999959525203,\"height\":1065},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[],\"show\":false},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":-1,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"izGallery\":true,\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/控股业务综合监管版本6改_11_1763550114546.png\"},\"izRotate\":false}}}]},\"component\":\"JGroup\",\"w\":2186.7174677608436,\"x\":-181,\"y\":23,\"componentName\":\"总览图\",\"pageCompId\":\"1151112777012137984\",\"equalProportion\":false,\"key\":\"64c403ce-8e74-45bf-9bdd-620348b3218d\",\"group\":true},{\"component\":\"JImg\",\"visible\":true,\"w\":45,\"x\":1854,\"h\":45,\"i\":\"e1f80194-96dc-4201-b615-7b607ae986ad\",\"y\":7,\"orderNum\":70,\"componentName\":\"图片\",\"pageCompId\":\"1151112777028915200\",\"key\":\"d25da919-3018-489d-b036-6b761ae9f043\"},{\"component\":\"JCurrentTime\",\"visible\":true,\"w\":309,\"x\":17,\"h\":33,\"i\":\"cef21ff4-9c5c-48bc-89fc-29a197a17ac3\",\"y\":15,\"orderNum\":70,\"componentName\":\"当前时间\",\"pageCompId\":\"1151112777045692416\",\"key\":\"257448bf-3fe8-491e-b3e1-c386ab97b099\"},{\"component\":\"JImg\",\"visible\":true,\"w\":1911,\"x\":-2,\"h\":70,\"i\":\"5425948a-c724-4ea6-803a-00d4a5b70813\",\"y\":0,\"orderNum\":0,\"componentName\":\"图片\",\"pageCompId\":\"1151112777075052544\",\"key\":\"c17de8af-9926-449f-8f73-aaef011b77fb\"},{\"component\":\"JImg\",\"visible\":true,\"w\":679,\"x\":608,\"h\":46,\"i\":\"7dfacde7-dd88-45ec-824e-e071f091bad5\",\"y\":85,\"orderNum\":70,\"componentName\":\"图片\",\"pageCompId\":\"1151112777112801280\",\"key\":\"e0c4ef30-5c9a-43a7-88ea-d24e7b63d8b3\"}]', '', '2011441918119228928', '1', 'admin', '2025-11-19 18:26:18', 'admin', '2025-11-19 21:18:03', '', 2, 3, 20, 0); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776819200000', NULL, '1151069555267260416', NULL, 'JText', '{\"borderColor\":\"#FFFFFF00\",\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"数值\"}],\"dataType\":1,\"h\":60,\"viewLoading\":true,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"chartData\":\"{\\n \\\"value\\\": \\\"集团业务综合管理平台\\\"\\n}\",\"size\":{\"width\":496.27777777777777,\"height\":60},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":170,\"turnConfig\":{\"type\":\"_blank\",\"url\":\"\"},\"linkType\":\"url\",\"linkageConfig\":[],\"option\":{\"openUrl\":\"\",\"isLink\":false,\"body\":{\"fontFamily\":\"DIGITALDREAMFAT\",\"color\":\"#FFFFFFE6\",\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":false,\"startColor\":\"#FFFFFF\",\"direction\":\"to bottom\"},\"letterSpacing\":6,\"fontSize\":30,\"text\":\"\",\"fontStyle\":\"italic\",\"fontWeight\":\"bold\",\"marginTop\":0,\"marginLeft\":0},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"modal\":{\"backgroundColor\":\"#363636\",\"backgroundImage\":\"\",\"backgroundSize\":\"100% 100%\",\"backgroundPosition\":\"center center\",\"title\":\"标题\",\"sizeMode\":\"full\",\"backgroundRepeat\":\"no-repeat\",\"titleBgColor\":\"#1F1F1F\"},\"openType\":\"_blank\"}}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776861143040', NULL, '1151069555267260416', NULL, 'JTabToggle', '{\"chartData\":\"[\\n {\\n \\\"label\\\": \\\"总览图\\\",\\n \\\"value\\\": \\\"1\\\"\\n },\\n {\\n \\\"label\\\": \\\"新成业务板块\\\",\\n \\\"value\\\": \\\"2\\\"\\n },\\n {\\n \\\"label\\\": \\\"万众业务板块\\\",\\n \\\"value\\\": \\\"3\\\"\\n },\\n {\\n \\\"label\\\": \\\"投资业务板块\\\",\\n \\\"value\\\": \\\"4\\\"\\n },\\n {\\n \\\"label\\\": \\\"建设业务板块\\\",\\n \\\"value\\\": \\\"5\\\"\\n },\\n {\\n \\\"label\\\": \\\"车辆业务版块\\\",\\n \\\"value\\\": \\\"6\\\"\\n }\\n]\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"height\":70},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"dataMapping\":[{\"mapping\":\"\",\"filed\":\"文本\"},{\"mapping\":\"\",\"filed\":\"数值\"}],\"background\":\"#FFFFFF00\",\"w\":680,\"dataType\":1,\"h\":70,\"viewLoading\":true,\"timeOut\":0,\"option\":{\"personalizedMode\":false,\"normal\":{\"imgUrl\":\"\",\"backgroundColor\":\"#3A414D00\",\"borderColor\":\"#0692A4\",\"color\":\"#FFFFFF\",\"borderWidth\":0,\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to bottom\",\"startColor\":\"#FFFFFF\"},\"fontSize\":14,\"backgroundSize\":\"contain\",\"backgroundPosition\":\"center center\",\"backgroundRepeat\":\"no-repeat\",\"fontWeight\":\"bold\"},\"active\":{\"imgUrl\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/背景1_1756454238980.png\",\"backgroundColor\":\"#0A73FF00\",\"borderColor\":\"#0692A4\",\"color\":\"#FFFFFF\",\"borderWidth\":0,\"gradient\":{\"endColor\":\"#0066CC\",\"type\":\"linear\",\"enabled\":true,\"direction\":\"to bottom\",\"startColor\":\"#FFFFFF\"},\"fontSize\":18,\"backgroundSize\":\"contain\",\"backgroundPosition\":\"center center\",\"backgroundRepeat\":\"no-repeat\"},\"time\":60,\"autoPlay\":true,\"items\":[{\"normalImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTAzNTY5Mjc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTAzNTY5Mzc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVERkQ1Rjg5NzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVERkQ1RjhBNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgBiAwEiAAIRAQMRAf/EAGsAAQACAwEAAAAAAAAAAAAAAAABBQIDBAYBAQEBAAAAAAAAAAAAAAAAAAACARAAAwABBAADBQkBAAAAAAAAAAECESESAwQxQVFxgSITBWGRodHhMkJSYoIRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/APOgAAAAAAAAAASAACBKAywCQUxqABLQA7+l1Y5lV8jUwtMt4w34e37wOAFnfRieB1Nzdy81try9F+uB1Opxcsy6eXW/Kz6LQCsJLd9PrbM7nVQs3safjrlZ8l9hHV+n8fNEU97d7suWsLDx5oCpBev6ZwTcw3XxtpfFPks/1KIASiCUBmACktQAJUF10uKo4f4v5iq9tS6/b/0vEpTbHPycbVTTTnwAvdj5lUcUxMXK1U4e7Gdr1/PBx/TFyKeSePS3sS0/1r+Bw8vb5uZJXTaWpC7PKo+Uqaj0Ave5N8s1xTW93trjWnhrnX3ZNPWuo4eFJJp78tpvGKz5eGfX3lVPc54nZNtSRPb5olRNtSvBLQD0Td/PhuVUvPx6Nzo9E1jT2o8sdM97sS8q697ycwAlEEoDMAFMagAS1IAAAAAAAAAAEoglAZgApLUSASoAAAAAAAAAAAlAAZgApj//2Q==\",\"width\":98,\"activeImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTA4REI1QTc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTA4REI1Qjc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDhEQjU4NzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDhEQjU5NzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgBiAwEiAAIRAQMRAf/EAHIAAQEBAQEBAQAAAAAAAAAAAAABBQQDAgYBAQEBAQEAAAAAAAAAAAAAAAABAgQFEAACAQMCAwYHAQAAAAAAAAAAAQIRIQMSBEEiEzFRcZHBQmGBobEyUoIUEQEBAQEBAQEAAAAAAAAAAAAAARESAjEh/9oADAMBAAIRAxEAPwCgEPQcQAABAABACAAABAAAAA+wAUCA69rt45VKU2lFWrXjwJbn6smuQh3S2sVicoyjKUbyo+A2+3x5FFyd3qqq9yM9T6vNcINL/Ng0drk4Lm0U9eCJt9nDLGDeqsq1pSio/AncXms4hrvYYlKMW5c1aXXD+TILPUvxLLAgBpkABB6EANAae1xyjj9r11lplGv4/NGWekcs4NOLaa7DHqbGpcaunqpxxqMYSj2pUdf1dzm2Ouk1C0uWnnf6HLk3OTJaUm6Hz18ih01J6e4zzcxrqbrX3KlkjLGnq1aXD1+x5YJOOPEklR6qtpv3fAz47rLGOlSaRI7jJGKjGTSXcTi5i9TdbTc+tCqTTrzWdLOydreKPzx0Ld5k6635nOXz5xn1dACG2QAAehACoAAAQAAQAAAQigAAAgA9bCwBpCxLAEAgAAgAAgBFCAAAAB//2Q==\",\"compVals\":[\"es-drager-1756453564096-22\"],\"value\":\"1\"},{\"normalImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTA4REI1RTc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTA4REI1Rjc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDhEQjVDNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDhEQjVENzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgCSAwEiAAIRAQMRAf/EAG0AAQADAQEBAAAAAAAAAAAAAAABAgUGBAMBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAgQDBgMJAAAAAAAAAAABAhEDIVESBDFBYXGRIjITBfBiFIGh0fFCUpKiUxEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A59kFpFQAAAAAAAAAAAAAAASAAAAAAAABMipaRUAAaOz2UNyquUk8cNLph1AzgbF32+007kHOMW6KOht/jQ+dn22V6zGSVJSlx+WmGHaBlg2bex2/papTTxa1KuVeHTj2Hk2m09Sdtz8lxy4dAPCDQls9UbSt+aak3X5S+w2cL3iutUdVGNcW/j4oBmA1N1srW3t6nKSk/LGVKvu5EbfYRu2Hcco1qqeLhmn1fIDNBr3NjbtNpW7s6c8Eu+h8IbFz26uNUbl5soU494GeDWue3W/SUoTTajKT4+KmWVOBkgAAAAAEyKlpFQBpu9Pb7S3obi5yk8MlgZheVyU0lJ1UVRAff67cfvl3mt7fGMbcayVYuVynTTTxZHPlozlFNJ0UuIHUy9SVppUlNxioyWbWmUl0VeJ4PbU1S3OL8M9UXTDg08eBkrcXYyU1J6kqJ15ZEx3V6C0xnJJck2BsS1W7EaRk5uE4qieGqWNcsOBHtt1ygraUo080oxjSnWT/ADMr6y//AKS/kyj3FyUPTcnpXIDdW7jcVzQ5SUYylSaTjh/bErsLkXZ0W1V1jrrnJ0fcqGM91dlD0nJ6MitrcXLKahJxrl0A6OUpzhO4lLVH1FBL9S5Spzpy+48G3ttbdRlRyctcYPHUtNKUXXu5mX9RdclPU9UcE8isLs4S1xbUs+0Dorl1Ri4WYxlctqko05Pzaft4o5turqWhclblri2pLmUbriwAAAAACZFS0ioAAAAAAAJAAAAAAAAAAAAAAAAAllQAAAAAACQAAAAAAAAAAAAAAAAAB//Z\",\"activeImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTAzNTY5Njc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTAzNTY5Nzc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDM1Njk0NzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDM1Njk1NzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgCSAwEiAAIRAQMRAf/EAHQAAQADAQEAAAAAAAAAAAAAAAABBAUCAwEBAQEBAQAAAAAAAAAAAAAAAAECBAUQAAICAQMCAwYGAwAAAAAAAAABEQIDIRIEMVFhcZFBgcEiUhOhsdHxMhThYnIRAQEBAQEBAQAAAAAAAAAAAAABERICITH/2gAMAwEAAhEDEQA/AJAB6DiCAAABAAAAACCAAAABAAAACAAAACvUgAqABc43FrmUttP/AJ09SW59qyapA0snDxxvq7JexbWcYuC8uOtlo2+vhH6me41zVAGlTi4fty7Tq1uU9vh1K/H4++1Hf+N5/AdROaqAuW425UVP5WVp9x1w+NTL82RqNUqzq2OpmnN/FEF/kcXHhpubas+lXHwIw8RZMTu2p0jXp3nx7DqZq83cUAaeTiUxtpUyXjyj8jxpxHbCskQ93X/WOpOoc1SINK/Cp9tWpZNqtrPxjt+Rmlll/EswABUAQAPUAg0gXnkth49Nradm2UTq13ZJNzGiM2a1Lj1/tZvrfqaHDVVSs2U1bvHhEa9jIJVnVNJ6PqT152ZPiz1lb9t9scKLWaUPxejt5KSpwZUUsnpaU406NPUzVmyJ7lZylHuJrnyVUKzS8zHFzGuputK00wr5bO+21Vp3ev8Agjg5N1FRK1Y62SrHvb/cz/7OX67erOHmu67HZ7ew4uYnX1rrk1vv2OzVU7fMk6/rqc8TIni20UuVu87PX0RmPkZHT7bs9vY5x5r49KOJHHxe/rcdr3ra6Tmu9VX1ePu/Yp4KNYEnEu26tHruW2Ijz9PaZ/38m5W3Oa9DmuS1Lbqtp9xPFh0275EqumKtbXootWPY+sfEwW5Z1XJalt1XD7nLcmvPnGfV0IANMgAA9AAaQAIIAAAAEAAAAIAAAAihAAAAgAAAPbQjQA0hoAABABAAAEAAAQAAIAIoAAIAAAAAf//Z\",\"compVals\":[\"es-drager-1762421939532-37\"],\"value\":\"2\"},{\"normalImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTAzNTY5QTc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTAzNTY5Qjc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDM1Njk4NzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDM1Njk5NzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgCSAwEiAAIRAQMRAf/EAHIAAQADAQEBAAAAAAAAAAAAAAABAgUDBgQBAQEBAAAAAAAAAAAAAAAAAAACARAAAgEDAgMEBgsBAAAAAAAAAAECEQMEIRIxQVFhkTIT8HGBIhQFobHR8UJSYpLS4lMVEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDz4AAAAAAAAAAAAAAABIAAAAAgSgJoCwKY5AAloAaGHhQyFVyaeum3TTtAzwbF35faadyDnGLdFHY2/toRiYFq/bjOW5NunH+rAyAbP/KUlPa1Wq2VfRtOunOmh8mLieZO25+C45cOwD4QaEsPdG2rfimpN1/SWwMOF73rrVHVRjXVv09KAZpJp5OFasW9zlJSfhjKlX3ciMfAjdsO45RrVU97h1T7XyAzQa9zBt2m0oXZ09VO+hwhguWOrjVG5cekKce8DPBq3Pl9vylKE02oyk+PvU6dKcDKAEoglAXABSXIAEqDTd2ePi29jcXKUnp0WhmF5XJTSUnVRVEB3+Ov/nl3m1gNRx46paSbq31a69dp5s+i3lXbdNsqbVRe11+sD0tudZzjGGsGlVvR+J9O0zfljdFCUZaT3RdHTwtPX01M15t9uu+XeVWVeiqKckvWwNeW63YjSMnNwnFUT0rLWvTTgR8uuuUFbSlGnilGMaU7W/vMr4y//pL9zKvIuSh5bk9q5Abiy43Fc2OUlGMpUmk4/wAtSuDci7Oy2qusd9esnR9yoY7yrsoeW5PZ0K2si5ZTUJONegHo5SnOE7iT3R8xQS/EuTpzpy+g+DHttY6jKm5y3xg9dy20pRdvdzMv4i45Ke57o6J9CsLs4S3xbUuvrA9DcuqMXCzGMrltUlGnJ+Lb7eKPON1dS0Lkrct8W1JcyjddWAJRBKAuACkuQAJUAAAAABIAAAAAAAAAAAACUQSgLgApLkACVAAAEgAAAAAAAAAAAAAAAlAAXABTH//Z\",\"activeImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTA4REI2Mjc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTA5ODhBMDc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDhEQjYwNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDhEQjYxNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgCSAwEiAAIRAQMRAf/EAHYAAQADAQEAAAAAAAAAAAAAAAABBAUCAwEBAQEBAQAAAAAAAAAAAAAAAAECBAUQAAIBAwIDBAgHAAAAAAAAAAABAhESAyEEMVGRQWFxIoGxwfEyUhMUodHhYnLiFREBAQEBAQEBAAAAAAAAAAAAAAEREgIhMf/aAAwDAQACEQMRAD8AkAHoOIIAAAEAAAAAIIAAAAEAAAAIAAAAK9SACoAFzbbWOZVbaf8AHTqS3PtWTVIGlk2eOl8XJLsVrI2+0x5YKTrV6cf6sx3M1rms4Gn/AJ6kpWtVr5avxrXTtpoVtvt75Qc/hnX8C9Q5qoC5LbXKCh8UlKte462e2hl82RqmqUa6tjqZqc38UQX9xtceGFzbUnwi6ewjDs1kxObarpTXhzr38idTNXm7igDTybSGNtKGSdPCnqPGG0csKyUo7uP7acR1DmqRBpT2UPpqUJJtKUn305eozSyy/iWYAAqAIAHqAQaQLzySw7eFracm2UTqU3JJN1pojNmtS49fus3zvqam0ajhjqlxbr4vv8DEPWG4yQpa6U0XWpn152ZF8+sv1uwlWUko6xa9PFlHYN0UZJ6Sui6acGnqUXusr1vfU5W4yrRSl1McXLGu5rSlWGFeWTnbKK05vX9CNjkugoJSjTjJKNPS37zP+5y/PLqzh5puNjk7eReLmJ19a63MZ32OTUU5eZJx/PU52mRPFbBVdVd4yevRGY9xkcPpuTt5HOPNPHpB0qOPi9/W45TnGU0nWN6ivm7/AEe4p4INYEnSrldGD1uVtKU8enaZ/wBfJcpXOseBzHJKErotp8xPFh0255EouGKMZTgqSjTsfG32mC3VnUMkoSui6Pmct1NefOM+roQAaZAAB6AA0gAQQAAAAIAAAAQAAABFCAAABAAAAe2hGgBpDQAEAgAAAAIAAAgAAACKgAAQAAAAA//Z\",\"compVals\":[\"es-drager-1756456982092-28\"],\"value\":\"3\"},{\"normalImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTA0N0Y5Rjc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTA0N0ZBMDc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDQ3RjlENzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDQ3RjlFNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgCSAwEiAAIRAQMRAf/EAG0AAQADAQEBAAAAAAAAAAAAAAABAgUEBgMBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAgMEBwgDAAAAAAAAAAABAhEDIRIEMUFRIvBhcYGRMhOhsdHxQpIUBcFS0hEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AwmQWZUAAAAAAAAAAAABIAAAAAAAAAAASypZlQABo2NLauWfUk5RlXLtWV/JbQM4G8/1liUFlk60k61WNPh1d5mw0ue1Wks9eXDll2MDjBrT0Npu5btZnctpb1R+zcc+m0jnO36nkm5L7QOEHfLR5o21b801JuvUX0Gjhe5rjVHVRjXFvp0oBnA0tTorVi3mcpKT8sZU/jcRY0Mbll3HKNaqnNs4p9b3AZwNa5ordptKF2dOynuPjDQuVhXGqNy28IU2+IGeDUn+vh6SlCabUZSe3mpw9xlgAAAAAEsqWZUAeg0Df41eaibVMeKxVMeOzrPPn1V64kkpNZdlHsA9LdvQdtJt8ykvr+FfHuMzSepPTyafkkmszwwxocS1t9fXLxPlC/ctusZNY17wN+ay2pXo20qxwShzqT216lt6Y8v6xtpRkpYTzRdHTytPHpiZcdTdjP1FJ5uNSVqr0VRTkl2sDWlmt2FSMnNxlFUTwrLHsw2EfrrrlBQSlGnmlGMad7fzMv8u//eX3Mq79yUcjk8q3Aba1UbiuZHKSinKk0nH/AFiV0NyLs5YKrrHPXjJ4+CMd6q7KHpuTy8CLd+5aTUG414AehlKc4Tmk80c6gl9S3Om+m72HDp7bWnUZUzOWeMHjmWWlKLr8N5mfkXHJTzOscEysLs4SzxbUuPaB6C5dSi4WoxlO2qSjTc/Nl79qPOt1dS0Lkrcs8XSXEo3XFgAAAAAEsqWZUAAAABIAAAAAAAAAAAAAAAAAAASyoAAAASAAAAAAAAAAAAAAAAAAAAA//9k=\",\"activeImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTA0N0Y5Qjc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTA0N0Y5Qzc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDM1NjlDNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDQ3RjlBNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgCSAwEiAAIRAQMRAf/EAHUAAQADAQEAAAAAAAAAAAAAAAABBAUCAwEBAQEBAQAAAAAAAAAAAAAAAAECBAUQAAIBAwIDBAkFAAAAAAAAAAABAhESAyEEMVGRQYEiMvBhcbHB0fFSE6HhQmIUEQEBAQEBAQEAAAAAAAAAAAAAARESAiEx/9oADAMBAAIRAxEAPwCQAeg4ggAAAQAAAAAggAAAAQAAAAgAAAAr1IAKgAXcW3xzxXyqnWnZT0oS3Fk1RBrvYYnFWt1o9ar06FGOC7HWkr6+HTSRme5Wr5sVgaMtpjbnDHc5wXNa/oeGDbXThf5ZtroOonNVAXJba5QUPNJSr3HWz20MviyNU1SjXVsdTNOb+KIL+42uPDC5tqT4RdPgRh2iyYnNtV0prw519fInUzV5u4oA08m0hjbShknT2U9x4w2jlhWSlHdx/rTiOoc1SINKeyh+NShJNqMpP105e4zSyy/iWYAAqAIAHqAQaQNnZ1/BXWnCnfxVNTGO1lmqJSapw1Mepsa83G9kyxcEm+Nfu+Vepn7e+WCTT8kk1Xhp2FVbvMv5s84ZZwdYtrWveYniyN31tbMlbjeRQSrHRKPiT+RX2DbSjJPzXRdNODT1M9Z8kZXqTu5hbjKtFKXUcXMOputKVYYV4ZOdsorTm9f2I2OS6CglKNOMko072/qZ/wDpy/fLqzh5puNjk7eQ4+YnX1rrcxnfY5NRTl4knH56nO0yJ4rYKrqrvbJ69EZj3GRw/G5O3kc4808ekHSo4+L39bjlOcZTSdY3qK+71930KeCDWBJ0q5XRg9blbSlPb07TP/PkuUrnWPA5jklCV0W1LmJ4sOm3PIlFwxRjKcFSUadj42/EwW6s6hklCV0XR8zluprz5xn1dCADTIAAPQAGkACCAAAABAAAACAAAAIoQAAAIAkEAD20I0ANIaAAgEAAAABAAAEAAAARUAAAQAAAAH//2Q==\",\"compVals\":[\"es-drager-1762481722941-1\"],\"value\":\"4\"},{\"normalImgUrl\":\"data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAEAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1RTA5ODhBMzc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1RTA5ODhBNDc1QjIxMUYwODEyNDhEMEUwRjNFMkIzOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjVFMDk4OEExNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjVFMDk4OEEyNzVCMjExRjA4MTI0OEQwRTBGM0UyQjM4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAGBYWIhgiNyAgN0IvKi9CQDU0NDVARkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRgEaIiIsJiw1ISE1RjUsNUZGRjs7RkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZG/8AAEQgARgCSAwEiAAIRAQMRAf/EAHAAAQADAQEBAAAAAAAAAAAAAAABAgUDBAYBAQEBAAAAAAAAAAAAAAAAAAACARAAAgECAwUGBQQDAAAAAAAAAAECEQMhEgQxQVFhMvBxkaEiE7FCUmIFgfGSFKLSIxEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AwAAAAAAAAAAAAAAAkAAAAAABAlATQFgUxyABLQA99uxYdn3ZOdU6NKm0DwA1Hp9MrSvP3MrdF0+OzZuKWtCrmnc0051wVf8AGn1PcBnA1p6Gy3ct2szuW0t6o/LcefS6TPO3n6JuSw5AeEHvlo80bat9U1Juv2l9Bo4XvVdao6qMa4t9u1AM4GlqdFasW8zlJSfTGVK+W4afQxuWXcco1qqerZxT5vcBmg17mit2m0oXZ07qfA4Q0Llp1cao3LbwhTb4gZ4NW5+Ph7SlCabUZSe31U4fAygBKIJQFwAUlyABKg29PNw0uW7NRi9iwrk+ZLm/IxABvPWx9lScf+Tk4Zftp8TlZvRs6ZytRxzLqxq+Rku5Jw9uvpTrTmdI6m7FRipNKDrEDenHLblejbSrHBKHrUntryW3tj5PxjbUYyUsJ5oujp0tPHtiZi1N1T9zM83GvbwC1V6KopyS72BryzW7EaRk5uMoqieFZY92Gwj8ddcoKCUo06pRjGlObf7mV/cv/XL+TKvUXJQ9tyeVbgNxauNxXMjlJRUpUmk4/wC2JTQ3IuzktqrrHPXjJ4+CoZD1V2UPbcnk4Fbd+5aTUJONeAH0UpTnCc0nmjnUEvmW50303eR4NPba06jKmZyzxg8cyy0pRc/DeZn9i45KeZ5o4JlYXZwlni2pce8D6G5dSi4Woxlctqko03Pqy/rtR843V1LQuStyzxdJcSjdcWAJRBKAuACkuQAJUAAAASAAAAAAAAAAAAAACUQSgLgApLkACVAAAkAAAAAAAAAAAAAAAAlAAXABTH//2Q==\",\"activeImgUrl\":\"drag/lib/img/navItem05_hover.jpg\",\"compVals\":[\"es-drager-1762494714798-7\"],\"value\":\"5\"},{\"marginRight\":0,\"normalBackgroundImage\":\"\",\"marginBottom\":0,\"compVals\":[\"es-drager-1756453915928-25\"],\"activeBackgroundImage\":\"\",\"value\":\"6\",\"showHideComps\":[],\"marginTop\":0,\"marginLeft\":0}],\"currentValue\":\"1\"}}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776882114560', NULL, '1151069555267260416', NULL, 'JGroup', '{\"borderColor\":\"#FFFFFF00\",\"size\":{},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\"}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776903086080', NULL, '1151069555267260416', NULL, 'JGroup', '{\"borderColor\":\"#FFFFFF00\",\"size\":{},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\"}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776924057600', NULL, '1151069555267260416', NULL, 'JGroup', '{\"borderColor\":\"#FFFFFF00\",\"size\":{},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\"}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776945029120', NULL, '1151069555267260416', NULL, 'JGroup', '{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1701,\"height\":892.5},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\"}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776961806336', NULL, '1151069555267260416', NULL, 'JGroup', '{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1863.9707102873572,\"height\":954.8876076075659},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\"}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112776978583552', NULL, '1151069555267260416', NULL, 'JGroup', '{\"borderColor\":\"#FFFFFF00\",\"size\":{},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\"}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112777012137984', NULL, '1151069555267260416', NULL, 'JGroup', '{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1858.2777764077778,\"height\":964.8264952628838},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\"}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112777028915200', NULL, '1151069555267260416', NULL, 'JImg', '{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":45.833333333333314,\"height\":45.833333333333314},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/标题组件-20_05_1756436293567.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112777045692416', NULL, '1151069555267260416', NULL, 'JCurrentTime', '{\"chartData\":\"\",\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":309.1666666666667,\"height\":33},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":280,\"dataType\":1,\"h\":33,\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"hourlySystem\":\"24\",\"format\":\"YYYY-MM-DD hh:mm:ss\",\"body\":{\"color\":\"#FFFFFF\",\"letterSpacing\":0,\"text\":\"\",\"fontWeight\":\"normal\",\"marginTop\":0,\"marginLeft\":0},\"showWeek\":\"show\",\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"}}}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112777075052544', NULL, '1151069555267260416', NULL, 'JImg', '{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":1911.6666666666667,\"height\":70},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/title_1756435933097.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}', 'admin', '2025-11-19 21:18:03', NULL, NULL); +INSERT INTO `onl_drag_page_comp` (`id`, `parent_id`, `page_id`, `comp_id`, `component`, `config`, `create_by`, `create_time`, `update_by`, `update_time`) VALUES ('1151112777112801280', NULL, '1151069555267260416', NULL, 'JImg', '{\"borderColor\":\"#FFFFFF00\",\"size\":{\"width\":679,\"height\":46.72222222222222},\"actionConfig\":{\"operateType\":\"modal\",\"modalName\":\"\",\"url\":\"\"},\"background\":\"#FFFFFF00\",\"w\":450,\"dataType\":1,\"h\":300,\"linesConfig\":{\"connectLine\":[]},\"url\":\"http://api.ghb.com/mock/42/nav\",\"timeOut\":0,\"option\":{\"padding\":0,\"backgroundColor\":\"#FFFFFF00\",\"borderRadius\":0,\"rotateTime\":1000,\"opacity\":1,\"body\":{\"url\":\"https://ghbdev.oss-cn-beijing.aliyuncs.com/jimureport/images/导航背景_1756451547138.png\"},\"card\":{\"rightHref\":\"\",\"size\":\"default\",\"extra\":\"\",\"title\":\"\"},\"izRotate\":false}}', 'admin', '2025-11-19 21:18:03', NULL, NULL); + + +-- -author:chenrui---date:2025-11-20-----for:[QQYUN-14043]【AI流程】工具节点 - 直接调用api插件工具,不使用LLM--- +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`) VALUES ('1991101089171214338', 'admin', '2025-11-19 19:07:47', 'admin', '2025-11-20 15:04:46', 'A05A01A01', NULL, 'ghb', '示例_工具节点', '', '', 'THEN(\n start.tag(\'start-node\'),\n tools.tag(\'254198464640012288\'),\n end.tag(\'254201908310396928\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":418,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"254198464640012288\",\"type\":\"tools\",\"x\":768,\"y\":451,\"properties\":{\"text\":\"工具调用\",\"options\":{\"tools\":{\"pluginId\":\"1983851685196451842\",\"pluginName\":\"会议管理\",\"pluginCategory\":\"plugin\",\"toolName\":\"meetingRoomQuery\",\"toolDescr\":\"查询会议室(会议地点)列表\",\"toolParameters\":[{\"name\":\"pageNo\",\"description\":\"页码数\",\"required\":true,\"type\":\"String\",\"location\":\"Query\",\"value\":\"1\"},{\"name\":\"pageSize\",\"description\":\"每页数量\",\"required\":true,\"type\":\"String\",\"location\":\"Query\",\"value\":\"10\"},{\"name\":\"name\",\"description\":\"会议室名称,可以使用*来模糊查询比如(*123*)\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"http://localhost:8080/ghbboot\",\"path\":\"/eoa/metting/eoaMettingRoom/list\",\"method\":\"GET\",\"headers\":{\"12222\":\"123\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}},{\"id\":\"254201908310396928\",\"type\":\"end\",\"x\":1272,\"y\":429,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"254198464640012288\"}],\"height\":114,\"width\":332}}],\"edges\":[{\"id\":\"254198464644206592\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"254198464640012288\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"254198464640012288_input\",\"pointsList\":[{\"x\":466,\"y\":403},{\"x\":566,\"y\":403},{\"x\":502,\"y\":403},{\"x\":602,\"y\":403}]},{\"id\":\"254201908314591232\",\"type\":\"base-edge\",\"sourceNodeId\":\"254198464640012288\",\"targetNodeId\":\"254201908310396928\",\"sourceAnchorId\":\"254198464640012288_output\",\"targetAnchorId\":\"254201908310396928_input\",\"pointsList\":[{\"x\":934,\"y\":403},{\"x\":1034,\"y\":403},{\"x\":1006,\"y\":403},{\"x\":1106,\"y\":403}]}]}', 'enable', '{\"outputs\":[{\"field\":\"result\",\"name\":\"res\",\"nodeId\":\"254198464640012288\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"}]}'); + +UPDATE onl_drag_page set type ='0', tenant_id = 0 WHERE id = '1151069555267260416'; \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_1__mcp_demo.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_1__mcp_demo.sql new file mode 100644 index 0000000..0bc005f --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_1__mcp_demo.sql @@ -0,0 +1,12 @@ +-- MCP和插件初始化数据 ---- +-- MCP 和 插件 +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1983474860536475649', NULL, '高德MCP', '高德MCP,包含查询路线、路况、天气等工具', 'mcp', 'sse', 'https://mcp.amap.com/sse?key=???', '', '[{\"name\":\"maps_direction_bicycling\",\"description\":\"骑行路径规划用于规划骑行通勤方案,规划时会考虑天桥、单行线、封路等情况。最大支持 500km 的骑行路线规划\",\"parameters\":[{\"name\":\"origin\",\"description\":\"出发点经纬度,坐标格式为:经度,纬度\",\"required\":true},{\"name\":\"destination\",\"description\":\"目的地经纬度,坐标格式为:经度,纬度\",\"required\":true}]},{\"name\":\"maps_direction_driving\",\"description\":\"驾车路径规划 API 可以根据用户起终点经纬度坐标规划以小客车、轿车通勤出行的方案,并且返回通勤方案的数据。\",\"parameters\":[{\"name\":\"origin\",\"description\":\"出发点经纬度,坐标格式为:经度,纬度\",\"required\":true},{\"name\":\"destination\",\"description\":\"目的地经纬度,坐标格式为:经度,纬度\",\"required\":true}]},{\"name\":\"maps_direction_transit_integrated\",\"description\":\"根据用户起终点经纬度坐标规划综合各类公共(火车、公交、地铁)交通方式的通勤方案,并且返回通勤方案的数据,跨城场景下必须传起点城市与终点城市\",\"parameters\":[{\"name\":\"origin\",\"description\":\"出发点经纬度,坐标格式为:经度,纬度\",\"required\":true},{\"name\":\"destination\",\"description\":\"目的地经纬度,坐标格式为:经度,纬度\",\"required\":true},{\"name\":\"city\",\"description\":\"公共交通规划起点城市\",\"required\":true},{\"name\":\"cityd\",\"description\":\"公共交通规划终点城市\",\"required\":true}]},{\"name\":\"maps_direction_walking\",\"description\":\"根据输入起点终点经纬度坐标规划100km 以内的步行通勤方案,并且返回通勤方案的数据\",\"parameters\":[{\"name\":\"origin\",\"description\":\"出发点经度,纬度,坐标格式为:经度,纬度\",\"required\":true},{\"name\":\"destination\",\"description\":\"目的地经度,纬度,坐标格式为:经度,纬度\",\"required\":true}]},{\"name\":\"maps_distance\",\"description\":\"测量两个经纬度坐标之间的距离,支持驾车、步行以及球面距离测量\",\"parameters\":[{\"name\":\"origins\",\"description\":\"起点经度,纬度,可以传多个坐标,使用竖线隔离,比如120,30|120,31,坐标格式为:经度,纬度\",\"required\":true},{\"name\":\"destination\",\"description\":\"终点经度,纬度,坐标格式为:经度,纬度\",\"required\":true},{\"name\":\"type\",\"description\":\"距离测量类型,1代表驾车距离测量,0代表直线距离测量,3步行距离测量\"}]},{\"name\":\"maps_geo\",\"description\":\"将详细的结构化地址转换为经纬度坐标。支持对地标性名胜景区、建筑物名称解析为经纬度坐标\",\"parameters\":[{\"name\":\"address\",\"description\":\"待解析的结构化地址信息\",\"required\":true},{\"name\":\"city\",\"description\":\"指定查询的城市\"}]},{\"name\":\"maps_regeocode\",\"description\":\"将一个高德经纬度坐标转换为行政区划地址信息\",\"parameters\":[{\"name\":\"location\",\"description\":\"经纬度\",\"required\":true}]},{\"name\":\"maps_ip_location\",\"description\":\"IP 定位根据用户输入的 IP 地址,定位 IP 的所在位置\",\"parameters\":[{\"name\":\"ip\",\"description\":\"IP地址\",\"required\":true}]},{\"name\":\"maps_schema_personal_map\",\"description\":\"用于行程规划结果在高德地图展示。将行程规划位置点按照行程顺序填入lineList,返回结果为高德地图打开的URI链接,该结果不需总结,直接返回!\",\"parameters\":[{\"name\":\"orgName\",\"description\":\"行程规划地图小程序名称\",\"required\":true},{\"name\":\"lineList\",\"description\":\"行程列表\",\"required\":true}]},{\"name\":\"maps_around_search\",\"description\":\"周边搜,根据用户传入关键词以及坐标location,搜索出radius半径范围的POI\",\"parameters\":[{\"name\":\"keywords\",\"description\":\"搜索关键词\",\"required\":true},{\"name\":\"location\",\"description\":\"中心点经度纬度\",\"required\":true},{\"name\":\"radius\",\"description\":\"搜索半径\"}]},{\"name\":\"maps_search_detail\",\"description\":\"查询关键词搜或者周边搜获取到的POI ID的详细信息\",\"parameters\":[{\"name\":\"id\",\"description\":\"关键词搜或者周边搜获取到的POI ID\",\"required\":true}]},{\"name\":\"maps_text_search\",\"description\":\"关键字搜索 API 根据用户输入的关键字进行 POI 搜索,并返回相关的信息\",\"parameters\":[{\"name\":\"keywords\",\"description\":\"查询关键字\",\"required\":true},{\"name\":\"city\",\"description\":\"查询城市\"},{\"name\":\"citylimit\",\"description\":\"是否限制城市范围内搜索,默认不限制\"}]},{\"name\":\"maps_schema_navi\",\"description\":\" Schema唤醒客户端-导航页面,用于根据用户输入终点信息,返回一个拼装好的客户端唤醒URI,用户点击该URI即可唤起对应的客户端APP。唤起客户端后,会自动跳转到导航页面。\",\"parameters\":[{\"name\":\"lon\",\"description\":\"终点经度\",\"required\":true},{\"name\":\"lat\",\"description\":\"终点纬度\",\"required\":true}]},{\"name\":\"maps_schema_take_taxi\",\"description\":\"根据用户输入的起点和终点信息,返回一个拼装好的客户端唤醒URI,直接唤起高德地图进行打车。直接展示生成的链接,不需要总结\",\"parameters\":[{\"name\":\"slon\",\"description\":\"起点经度\"},{\"name\":\"slat\",\"description\":\"起点纬度\"},{\"name\":\"sname\",\"description\":\"起点名称\"},{\"name\":\"dlon\",\"description\":\"终点经度\",\"required\":true},{\"name\":\"dlat\",\"description\":\"终点纬度\",\"required\":true},{\"name\":\"dname\",\"description\":\"终点名称\",\"required\":true}]},{\"name\":\"maps_weather\",\"description\":\"根据城市名称或者标准adcode查询指定城市的天气\",\"parameters\":[{\"name\":\"city\",\"description\":\"城市名称或者adcode\",\"required\":true}]}]', 'enable', 1, '{\"tool_count\":15}', 'admin', '2025-10-29 18:03:53', 'admin', '2025-11-13 17:15:48', 'A04', NULL); +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1986312214909321217', NULL, '商品采购助手', '为AI Agent提供商品管理API(DEMO)', 'plugin', 'api', NULL, '', '[{\"name\":\"list_products\",\"description\":\"查询可购买的商品列表,支持按分类和关键词筛选;商品的id只能通过这个接口查询。\",\"path\":\"/demo/shop/products\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"category\",\"description\":\"商品分类,可选值: \\\"电子产品\\\", \\\"图书\\\", \\\"生活用品\\\", \\\"食品\\\"\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"},{\"name\":\"keyword\",\"description\":\"搜索关键词,用于在商品名称和描述中搜索\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"id\",\"description\":\"商品ID,对应下单的ProductId\",\"type\":\"String\"},{\"name\":\"name\",\"description\":\"商品名称\",\"type\":\"String\"},{\"name\":\"price\",\"description\":\"价格(元)\",\"type\":\"String\"},{\"name\":\"category\",\"description\":\"分类\",\"type\":\"String\"},{\"name\":\"description\",\"description\":\"描述\",\"type\":\"String\"},{\"name\":\"stock\",\"description\":\"库存数量\",\"type\":\"String\"}]},{\"name\":\"check_stock\",\"description\":\"查询指定商品的当前库存情况\",\"path\":\"/demo/shop/stock\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"productId\",\"description\":\"商品ID\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"productId\",\"description\":\"商品ID\",\"type\":\"String\"},{\"name\":\"productName\",\"description\":\"商品名称\",\"type\":\"String\"},{\"name\":\"stock\",\"description\":\"当前库存数量\",\"type\":\"String\"},{\"name\":\"available\",\"description\":\"是否有货\",\"type\":\"Boolean\"}]},{\"name\":\"create_order\",\"description\":\"为用户创建购买订单\",\"path\":\"/demo/shop/purchase\",\"method\":\"POST\",\"enabled\":true,\"parameters\":[{\"name\":\"productId\",\"description\":\"要购买的商品的ID,对应商品的信息的id\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"quantity\",\"description\":\" 购买数量,必须大于0\",\"type\":\"Integer\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"userId\",\"description\":\"用户ID,用于关联订单\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"id\",\"description\":\"订单ID\",\"type\":\"String\"},{\"name\":\"productId\",\"description\":\"商品ID\",\"type\":\"String\"},{\"name\":\"productName\",\"description\":\"商品名称\",\"type\":\"String\"},{\"name\":\"quantity\",\"description\":\"购买数量\",\"type\":\"String\"},{\"name\":\"unitPrice\",\"description\":\"单价\",\"type\":\"String\"},{\"name\":\"totalAmount\",\"description\":\"总金额\",\"type\":\"String\"},{\"name\":\"status\",\"description\":\"订单状态\",\"type\":\"String\"},{\"name\":\"createTime\",\"description\":\"创建时间\",\"type\":\"String\"}]},{\"name\":\"confirm_payment\",\"description\":\"确认订单支付,扣减商品库存\",\"path\":\"/demo/shop/stock/deduct\",\"method\":\"POST\",\"enabled\":true,\"parameters\":[{\"name\":\"orderId\",\"description\":\"订单ID\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"orderId\",\"description\":\"订单ID\",\"type\":\"String\"},{\"name\":\"productId\",\"description\":\"商品ID\",\"type\":\"String\"},{\"name\":\"productName\",\"description\":\"商品名称\",\"type\":\"String\"},{\"name\":\"deductedQuantity\",\"description\":\"扣减的数量\",\"type\":\"String\"},{\"name\":\"remainingStock\",\"description\":\"剩余库存\",\"type\":\"String\"},{\"name\":\"orderStatus\",\"description\":\"订单状态\",\"type\":\"String\"}]},{\"name\":\"get_order_details\",\"description\":\"查询指定订单的详细信息\",\"path\":\"/demo/shop/order\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"orderId\",\"description\":\"订单ID\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]},{\"name\":\"get_categories\",\"description\":\"获取所有可用的商品分类\",\"path\":\"/demo/shop/categories\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[],\"responses\":[]}]', 'enable', 1, '{\"tokenParamName\":\"X-Access-Token\",\"tool_count\":6,\"authType\":\"token\",\"tokenParamValue\":\"\"}', 'admin', '2025-11-06 13:58:31', 'admin', '2025-11-13 17:16:57', 'A05A01A01', NULL); +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1988091188723412994', NULL, 'BraveSearch', '基于Brave的网络检索插件,支持使用Brave搜索资料', 'plugin', 'api', 'https://api.search.brave.com', '{\"Accept\":\"*/*\",\"X-Subscription-Token\":\"???\"}', '[{\"name\":\"search\",\"description\":\"从搜索引擎根据问题搜索结果\",\"path\":\"/res/v1/web/search\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"count\",\"description\":\"查询数量\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"10\"},{\"name\":\"q\",\"description\":\"查询内容(问题)\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"title\",\"description\":\"结果标题\",\"type\":\"String\"},{\"name\":\"description\",\"description\":\"结果描述\",\"type\":\"String\"},{\"name\":\"url\",\"description\":\"结果原文地址\",\"type\":\"String\"}]}]', 'enable', 1, '{\"tokenParamName\":\"X-Subscription-Token\",\"tool_count\":1,\"authType\":\"token\",\"tokenParamValue\":\"BSATNKM5e6Hm_2LewptVvLSn0eDzWf6\"}', 'admin', '2025-11-11 11:47:31', 'admin', '2025-11-13 17:16:52', 'A05A01A01', NULL); +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1988208474780168193', NULL, 'Unsplash', '图片搜索插件,支持用关键词搜索相关图片', 'plugin', 'api', 'https://api.unsplash.com', '{\"Accept-Version\":\"v1\",\"Authorization\":\"Client-ID ???\"}', '[{\"name\":\"search_photos\",\"description\":\"通过接口查询与关键词相关的图片列表。\",\"path\":\"/search/photos\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"page\",\"description\":\"分页页码,数字类型\",\"type\":\"Number\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"1\"},{\"name\":\"per_page\",\"description\":\"每页数量\",\"type\":\"Number\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"1\"},{\"name\":\"query\",\"description\":\"关键词,对图片的描述(查询条件)\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"results\",\"description\":\"查询到的结果集合\",\"type\":\"Array\"},{\"name\":\"urls\",\"description\":\"结果集合中的图片地址,根据清晰度有多个选项\",\"type\":\"Array\"}]}]', 'enable', 1, '{\"tokenParamName\":\"Authorization\",\"tool_count\":1,\"authType\":\"token\",\"tokenParamValue\":\"Client-ID Ixug6rX2j1PMb08A0HRpwny8dAWi1vBLN1gymow75LQ\"}', 'admin', '2025-11-11 19:33:34', 'admin', '2025-11-13 17:16:45', 'A05A01A01', NULL); + +-- AI Flow +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`) VALUES ('1988073273760501762', 'admin', '2025-11-11 10:36:20', 'admin', '2025-11-13 15:56:07', 'A05A01A01', NULL, 'ghb', '软文生成器', '', '', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'251170889153376256\'),\n llm.tag(\'251190209648521216\'),\n llm.tag(\'251190922428542976\'),\n llm.tag(\'251246385341919232\'),\n end.tag(\'251191126401740800\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":332.8947368421053,\"y\":589.0526315789475,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"目的地\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"from\",\"name\":\"出发地\",\"type\":\"string\",\"required\":true},{\"field\":\"time\",\"name\":\"出发时间\",\"type\":\"string\",\"required\":true},{\"field\":\"peopleNum\",\"name\":\"人数\",\"type\":\"number\",\"required\":true}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"251170889153376256\",\"type\":\"llm\",\"x\":685.9473684210526,\"y\":381.05263157894734,\"properties\":{\"text\":\"意图和需求分析\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是自驾游攻略生成的需求分析师\\n\\n## 目标:\\n- 分析用户提供的主题信息,归纳整理出实际需求列表。\\n\\n## 技能:\\n1. 信息提取与分析能力\\n2. 旅游规划与建议能力\\n3. 数据查询与整合能力\\n\\n## 工作流:\\n1. 收集用户提供的主题信息并进行分类。\\n2. 确定用户的目标和需求,并归纳整理。\\n3. 列出需要查询的资料和天气、路况信息。\\n\\n## 输出格式:\\n- 列表形式,包含目标和需求、查询资料列表、天气和路况信息。\\n\\n## 限制:\\n- 不得提供未经过验证的信息。\\n- 所有数据需标注来源,不确定信息用[需核实]标记。\"},{\"role\":\"user\",\"content\":\"出发地:{{from}}\\n目的地:{{userQuestion}}\\n计划时间:{{time}}\\n人数:{{peopleNum}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"userQuestion\",\"nodeId\":\"start-node\"},{\"field\":\"from\",\"name\":\"from\",\"nodeId\":\"start-node\"},{\"field\":\"time\",\"name\":\"time\",\"nodeId\":\"start-node\"},{\"field\":\"peopleNum\",\"name\":\"peopleNum\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"251190209648521216\",\"type\":\"llm\",\"x\":943.2631578947365,\"y\":632.0526315789475,\"properties\":{\"text\":\"资料查询\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"## 角色\\n\\n\\n你是一名 自驾游资料查询师(DataAgent),专注于为下游的“攻略生成Agent”提供精准、结构化的自驾游资料。\\n\\n\\n------\\n\\n\\n## 职责目标\\n 1. 根据输入内容(出发地、目的地、行程需求等),直接执行资料查询任务,不再向用户提问。\\n 2. 收集并整理以下四类信息:\\n  - 🚗 路线与导航规划信息\\n  - 🏞️ 沿途及目的地的景点和游玩项目\\n  - 🏨 住宿与周边美食信息\\n  - ☁️ 沿途及目的地天气信息\\n 3. 输出清晰、结构化的数据结果,供下一个Agent生成攻略使用。\\n\\n\\n------\\n\\n\\n## 能力与工具\\n\\n\\n- maps 工具\\n - 查询路线与导航规划信息(距离、时长、推荐路线、途经地)。\\n - 查询沿途及目的地的住宿与餐饮信息。\\n - 查询沿途及目的地的实时或近期天气信息。\\n\\n\\n- search 工具\\n - 查询沿途及目的地的景点、游玩项目、特色体验、门票及评价等。\\n\\n\\n------\\n\\n\\n## 工作流程\\n 1. 接收任务\\n  - 使用用户提供的现有信息(不提问、不二次确认)。\\n 2. 资料查询\\n  - 调用 maps 工具 获取路线、住宿、美食、天气。\\n  - 调用 search 工具 获取景点和游玩项目。\\n 3. 资料整理\\n  - 将查询结果按类型整理成结构化资料包。\\n  - 每条数据需注明来源(maps / search)。\\n 4. 结果输出\\n  - 输出格式清晰,便于下游Agent直接使用。\\n\\n\\n------\\n\\n\\n## 输出格式示例\\n\\n\\n```\\n{\\n  \\\"route_info\\\": [\\n    {\\n      \\\"from\\\": \\\"北京\\\",\\n      \\\"to\\\": \\\"张家口\\\",\\n      \\\"distance\\\": \\\"220km\\\",\\n      \\\"duration\\\": \\\"3小时\\\",\\n      \\\"route_detail\\\": \\\"经京藏高速G6\\\",\\n      \\\"source\\\": \\\"maps\\\"\\n    }\\n  ],\\n  \\\"sights\\\": [\\n    {\\n      \\\"name\\\": \\\"崇礼滑雪场\\\",\\n      \\\"tags\\\": [\\\"滑雪\\\", \\\"冬季运动\\\"],\\n      \\\"description\\\": \\\"亚洲知名滑雪胜地\\\",\\n      \\\"source\\\": \\\"search\\\"\\n    }\\n  ],\\n  \\\"hotels\\\": [\\n    {\\n      \\\"name\\\": \\\"张家口云顶假日酒店\\\",\\n      \\\"rating\\\": \\\"4.6\\\",\\n      \\\"address\\\": \\\"崇礼区奥运大道88号\\\",\\n      \\\"source\\\": \\\"maps\\\"\\n    }\\n  ],\\n  \\\"foods\\\": [\\n    {\\n      \\\"name\\\": \\\"张家口烧麦\\\",\\n      \\\"type\\\": \\\"地方特色\\\",\\n      \\\"recommendation\\\": \\\"崇礼老街美食街\\\",\\n      \\\"source\\\": \\\"maps\\\"\\n    }\\n  ],\\n  \\\"weather\\\": [\\n    {\\n      \\\"location\\\": \\\"崇礼\\\",\\n      \\\"condition\\\": \\\"晴\\\",\\n      \\\"temperature\\\": \\\"5°C~12°C\\\",\\n      \\\"wind\\\": \\\"微风\\\",\\n      \\\"source\\\": \\\"maps\\\"\\n    }\\n  ]\\n}\\n```\\n\\n\\n------\\n\\n\\n## 限制与规范\\n - 不生成行程攻略、总结或建议性文字。\\n - 不提问用户,只执行既定任务。\\n - 不包含任何虚构或未经验证的信息。\\n - 不涉及隐私、政治或违法内容。\\n - 不确定的数据需以 [需核实] 标识。\\n\\n\"},{\"role\":\"user\",\"content\":\"需求:{{demand}}\"}],\"plugins\":[{\"pluginId\":\"1983474860536475649\",\"pluginName\":\"高德\",\"category\":\"mcp\"},{\"pluginId\":\"1988091188723412994\",\"pluginName\":\"BraveSearch\",\"category\":\"mcp\"}]},\"inputParams\":[{\"field\":\"text\",\"name\":\"demand\",\"nodeId\":\"251170889153376256\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"251190922428542976\",\"type\":\"llm\",\"x\":1225.0526315789468,\"y\":370.6842105263157,\"properties\":{\"text\":\"生成文章\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":15,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色定位:实地体验派自驾游攻略博主\\n\\n\\n你是一名热爱公路旅行、记录真实体验的自驾游达人博主。  \\n你的任务是为读者打造一份**能直接照着走的实地自驾游攻略**——兼顾实用性与可读性,让人看完就想出发。  \\n---\\n## 目标\\n1. 输出结构清晰、完整且可直接使用的自驾游攻略。  \\n2. 以**亲历者口吻**撰写内容,语言自然、有温度、具感染力。  \\n3. 帮助用户在有限时间内,完成一次轻松、安全、体验丰富的公路旅程。  \\n---\\n## 技能\\n1. **路线规划高手**:能结合季节、路况、天气等因素,规划最顺路、最合理的行程。  \\n2. **信息整合专家**:能整合住宿、美食、加油点、景点开放时间等要素,构成完整旅程。  \\n3. **避坑指导员**:能在攻略中明确提示潜在风险与替代路线,确保安全顺利出行。  \\n4. **文案风格创作者**:文风真实、有共鸣,兼具实用与情感温度。  \\n---\\n## 工作流程\\n1. **接收资料**\\n   - 使用上游 DataAgent 提供的结构化资料(路线、天气、景点、住宿、美食等)。  \\n   - 明确出发地点、目的地,时间和人数。\\n2. **内容整合**\\n   - 基于资料内容,编排合理的日程与路线逻辑。  \\n   - 为每天生成住宿与周边推荐。  \\n   - 根据沿途特点补充打卡点、体验亮点与避坑提醒。  \\n3. **配图搜索**\\n   - 基于文章中的景点,使用图片搜索工具搜索相关图片\\n   - 获取图片链接后,嵌入到文章中。\\n   - 每篇文章可以使用3张左右图片。\\n3. **攻略输出**\\n   - 输出格式固定、排版规范、语气一致、可直接用于图文发布。  \\n---\\n## 输出格式规范\\n\\n\\n\\n\\n攻略必须严格包含以下结构模块(顺序固定):  \\n\\n\\n\\n\\n### 标题\\n一句话吸引读者,让人有“立刻出发”的冲动。  \\n> 示例:  \\n> 「这条西北环线美到窒息,一路风光大片连连!」  \\n---\\n### 重要概述信息(开篇摘要)\\n以简洁的段落或表格概述行程关键信息:  \\n- 出发地与目的地  \\n- 推荐出行季节  \\n- 建议行程天数  \\n- 总里程 / 主要路线  \\n- 车辆与路况建议  \\n- 是否适合家庭 / 情侣 / 越野爱好者  \\n\\n\\n\\n\\n> 示例:  \\n> **推荐季节**:9月下旬 - 10月中旬  \\n> **总里程**:约820公里  \\n> **适合人群**:喜欢自然风光与摄影的旅行者  \\n---\\n### 行程安排(按天)\\n分天描述路线、行驶距离、推荐出发时间、路况建议:  \\n- 每天路线与行驶信息  \\n- 沿途休息站 / 加油点  \\n- 建议游玩节奏  \\n\\n\\n\\n\\n> 示例:  \\n> **Day 1:成都 → 理县(约220km / 4小时)**  \\n> 上午出发,经成绵高速转都汶高速,全程路况优。途中可在汶川服务区短暂停留休息。  \\n---\\n### 每日住宿与周边推荐\\n为每天行程提供住宿推荐及周边美食娱乐选项:  \\n- 酒店名称、星级、亮点  \\n- 周边美食推荐(餐厅/夜宵/特色菜)  \\n- 休闲娱乐建议  \\n\\n\\n\\n\\n> 示例:  \\n> **住宿推荐**:理县瑞云山居(¥380起 / 含早餐)  \\n> **周边美食**:理县藏餐坊(推荐青稞酒与手抓羊)  \\n---\\n### 沿途打卡与景点推荐\\n精选每段路线的代表性景点与小众体验点,注明特色与亮点:  \\n> 示例:  \\n> - 毕棚沟:秋色摄影圣地,10月最佳观赏期  \\n> - 古尔沟温泉:天然碳酸泉,适合行程末放松  \\n---\\n### 避坑提醒\\n实地经验总结,包括但不限于:  \\n- 天气与季节风险  \\n- 路段注意事项(隧道、陡坡、限速)  \\n- 油站/信号盲区提示  \\n- 门票与政策更新  \\n\\n\\n> 示例:  \\n> - 国庆期间毕棚沟限流,建议提前预约。  \\n> - 高原路段昼夜温差大,请携带保暖衣物。  \\n---\\n### 结语\\n以温暖、真实的语气收尾,让读者感受到旅途的意义与期待。  \\n> 示例:  \\n> “这条路,值得你放慢脚步去感受。愿每一次出发,都有风景,也有故事。”  \\n---\\n## 风格要求\\n- 文字自然、口语化、有画面感。  \\n- 语气积极向上,不生硬、不堆砌。  \\n- 以**“亲身体验分享”**为写作视角。  \\n- 适合直接发布到公众号 / 小红书 / 旅游类平台。  \\n---\\n## 限制与合规说明\\n- 不虚构信息,所有数据基于实际资料。  \\n- 不涉及隐私、歧视或违法内容。  \\n- 直接输出攻略正文,不输出系统提示、元信息或额外解释。  \\n\\n\"},{\"role\":\"user\",\"content\":\"## 需求\\n{{demand}}\\n## 资料\\n{{resources}}\"}],\"plugins\":[{\"pluginId\":\"1988208474780168193\",\"pluginName\":\"图片搜索\",\"category\":\"mcp\"}]},\"inputParams\":[{\"field\":\"text\",\"name\":\"demand\",\"nodeId\":\"251170889153376256\"},{\"field\":\"text\",\"name\":\"resources\",\"nodeId\":\"251190209648521216\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"251191126401740800\",\"type\":\"end\",\"x\":1743.594965675057,\"y\":370.28146453089266,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{{result}}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"result\",\"nodeId\":\"251246385341919232\"}],\"height\":136,\"width\":332}},{\"id\":\"251246385341919232\",\"type\":\"llm\",\"x\":1438.6270022883289,\"y\":637.045766590389,\"properties\":{\"text\":\"润色并存储\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":10,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色  \\n你是一个**出行攻略与富文本排版专家(TravelContentStylist)**,负责将普通文本文章转化为专业级、高端杂志风格的HTML富文本内容。\\n\\n\\n---\\n\\n\\n## 🎯 目标  \\n1. 将输入的原始文章格式化为整洁、美观、结构统一的HTML富文本(视觉风格参考蓝灰色出行攻略模板)。  \\n2. 自动提取文章的标题、副标题、关键词、摘要等信息。  \\n3. 将富文本与提取信息一并存储到系统中。  \\n\\n\\n---\\n\\n\\n## 💡 富文本设计规范  \\n\\n\\n### 页面整体样式示意 \\n```html\\n
\\n\\n\\n\\n\\n 
\\n   

文章主标题

\\n   

文章副标题

\\n 
\\n\\n\\n\\n\\n  \\n 
\\n   

出行季节:金秋10月(2023-10-25)

\\n   

推荐天数:1-2天(可夜宿济南/南京)

\\n   

总公里数:约1200km

\\n   

人数与车型建议:5人,推荐中大型SUV或MPV,空间舒适。

\\n 
\\n  \\n
\\n```  \\n设计说明\\n1. 整体布局\\n  - 最大宽度约 880px,居中显示,整体背景为浅灰白色(#FAFBFC)。\\n  - 内边距较大(40px),四周有圆角(12px)和轻微阴影(box-shadow: 0 0 12px rgba(0,0,0,0.05)),营造卡片式感觉。\\n  - 使用了中文常用的字体组合(苹方、微软雅黑),兼顾现代感与易读性,文字颜色为深灰色(#333)。\\n2. 标题部分\\n  - 主标题突出(2em,蓝色 #1A73E8,粗体),副标题较小(1.2em,深灰色),并在底部有分隔线强化层次感。\\n3. 信息概览模块\\n  - 背景为淡蓝色(#F3F7FC),左侧有蓝色竖条(4px),像标签式信息卡。\\n  - 内部列出了出行季节、推荐天数、总公里数和人数/车型建议,文字加粗强调关键信息。\\n  - 模块与下方内容有明显间距(28px),便于视觉区分。\\n4. 行程安排模块\\n  - 模块标题蓝色,带下划分隔线,列表为有序列表,行距 1.8,便于阅读行程顺序。\\n  - 每天的路线、里程和时间都清晰标注,关键内容加粗突出。\\n5. 沿途打卡模块\\n  - 模块标题与行程安排相同风格,列表为无序列表,展示沿途景点和推荐打卡地。\\n  - 行距同样较大(1.8),保持阅读舒适度。\\n6. 注意事项模块(避坑提醒)\\n  - 模块标题用红色(#D93025)和粉色分隔线(#F3C1BE),突出警示性质。\\n  - 列表中重点信息加粗(施工提醒、天气因素、油费与通行),提醒用户注意行程安全和预算。\\n整体风格 清爽、层次分明、信息易抓取,既有蓝色调的出行信息模块,又有红色警示提醒,结合圆角卡片和阴影设计,使文章既专业又具有亲和力。\\n\\n\\n3. 提取标题、副标题、关键词、摘要并生成结构化信息:\\n\\n\\n{\\n  \\\"title\\\": \\\"文章主标题\\\",\\n  \\\"subtitle\\\": \\\"文章副标题\\\",\\n  \\\"keywords\\\": [\\\"关键词1\\\", \\\"关键词2\\\", \\\"关键词3\\\"],\\n  \\\"summary\\\": \\\"简要描述文章主题与亮点。\\\"\\n}\\n4. 将HTML富文本与提取信息存入系统。\\n5. 返回执行结果(不输出HTML内容本身):\\n{\\n  \\\"status\\\": \\\"success\\\",\\n  \\\"message\\\": \\\"富文本文章已成功优化并入库。\\\"\\n}\\n\\n\\n\\n\\n⸻\\n\\n\\n\\n\\n## 输出要求\\n- 不输出HTML正文,只返回操作结果。\\n- 富文本需使用

包裹正文段落,整体结构采用

容器。\\n- 排版需超过一般模板美感(具备视觉层次、柔和色彩与可印刷风格)。\\n- 输出格式严格为JSON结果对象,保证系统可解析与存储。\\n- 注意原文内容不要丢失,特别是配图等信息\\n\\n\\n\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{content}}\"}],\"plugins\":[{\"pluginId\":\"1988146198605819905\",\"pluginName\":\"ghbBoot CMS\",\"category\":\"mcp\"}]},\"inputParams\":[{\"field\":\"text\",\"name\":\"content\",\"nodeId\":\"251190922428542976\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"251170889157570560\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"251170889153376256\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"251170889153376256_input\",\"pointsList\":[{\"x\":498.8947368421053,\"y\":574.0526315789475},{\"x\":598.8947368421053,\"y\":574.0526315789475},{\"x\":419.9473684210526,\"y\":322.05263157894734},{\"x\":519.9473684210526,\"y\":322.05263157894734}]},{\"id\":\"251190922432737280\",\"type\":\"base-edge\",\"sourceNodeId\":\"251190209648521216\",\"targetNodeId\":\"251190922428542976\",\"sourceAnchorId\":\"251190209648521216_output\",\"targetAnchorId\":\"251190922428542976_input\",\"pointsList\":[{\"x\":1109.2631578947367,\"y\":573.0526315789475},{\"x\":1209.2631578947367,\"y\":573.0526315789475},{\"x\":959.0526315789468,\"y\":311.6842105263157},{\"x\":1059.0526315789468,\"y\":311.6842105263157}]},{\"id\":\"251221680912330752\",\"type\":\"base-edge\",\"sourceNodeId\":\"251170889153376256\",\"targetNodeId\":\"251190209648521216\",\"sourceAnchorId\":\"251170889153376256_output\",\"targetAnchorId\":\"251190209648521216_input\",\"pointsList\":[{\"x\":851.9473684210526,\"y\":322.05263157894734},{\"x\":951.9473684210526,\"y\":322.05263157894734},{\"x\":677.2631578947365,\"y\":573.0526315789475},{\"x\":777.2631578947365,\"y\":573.0526315789475}]},{\"id\":\"251246385346113536\",\"type\":\"base-edge\",\"sourceNodeId\":\"251190922428542976\",\"targetNodeId\":\"251246385341919232\",\"sourceAnchorId\":\"251190922428542976_output\",\"targetAnchorId\":\"251246385341919232_input\",\"pointsList\":[{\"x\":1391.0526315789468,\"y\":311.6842105263157},{\"x\":1491.0526315789468,\"y\":311.6842105263157},{\"x\":1172.6270022883289,\"y\":578.045766590389},{\"x\":1272.6270022883289,\"y\":578.045766590389}]},{\"id\":\"251246471618752512\",\"type\":\"base-edge\",\"sourceNodeId\":\"251246385341919232\",\"targetNodeId\":\"251191126401740800\",\"sourceAnchorId\":\"251246385341919232_output\",\"targetAnchorId\":\"251191126401740800_input\",\"pointsList\":[{\"x\":1604.6270022883289,\"y\":578.045766590389},{\"x\":1704.6270022883289,\"y\":578.045766590389},{\"x\":1477.594965675057,\"y\":333.28146453089266},{\"x\":1577.594965675057,\"y\":333.28146453089266}]}]}', 'enable', '{\"outputs\":[{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"目的地\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"from\",\"name\":\"出发地\",\"required\":true,\"type\":\"string\"},{\"field\":\"time\",\"name\":\"出发时间\",\"required\":true,\"type\":\"string\"},{\"field\":\"peopleNum\",\"name\":\"人数\",\"required\":true,\"type\":\"number\"}]}'); + +-- 相关AI应用 +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`) VALUES ('1986326978217746433', 'admin', '2025-11-06 14:57:10', 'admin', '2025-11-06 18:57:58', 'A05A01A01', NULL, '商品导购', NULL, NULL, 'chatSimple', '您好~我是您的智能购物助手,可以帮您挑选商品、创建订单并完成购买。\n无论您想买电子产品、生活用品、图书还是食品,我都能为您快速推荐。\n您想先看看哪一类商品?😊', '## 导购助手精简提示\n角色:温和、真诚、贴近真人的导购,帮助选品、查库存、下单与支付;绝不虚构商品/价格/库存。回答只用自然中文,不展示内部过程,不输出 JSON/代码。\n\n## 人格与语气\n热心、礼貌、不过度重复;缺货直说并主动给替代;禁用“我是AI”等表述。示例:\n - “我帮您查下库存,稍等哦~” “这款暂时没货,要不要看看类似的?”\n\n## 不可逾越底线\n1. 不编造:空结果必须如实说明,不造商品/价格/订单/库存。\n2. 不泄露内部:不出现思考/Action/Observation字样。\n3. 保持口吻:短句、自然、人类化。\n4. 先确认意图:描述模糊时先问清类目/品牌/用途/预算。\n\n## 工具调用核心逻辑\n始终“先查再答”。凡涉及商品或购买均先调用 list_products;获取到商品后按需继续。\n\n触发 list_products(任一满足):出现商品名称/品牌/型号/类目/用途;询问价格/库存/推荐/折扣;明确购买意向(买/购/下单/订/入手/现货);出现数量。模糊描述(“想买电脑”)也要查。重复出现商品名需重新查,禁止复用旧ID。\n\n触发 check_stock:已有商品ID且询问库存/是否有货/能不能买/数量够不够。\n\n触发 create_order:已完成 list_products+check_stock 且库存充足,并用户明确要下单(“就这个” “下单”)。\n\n触发 confirm_payment:已有订单且用户明确支付(付款/支付/确认支付)。未确认不得调用。\n\n触发 get_order_details:用户询问订单状态/详情。\n\n禁止:未查直接推荐;使用历史商品ID;跳过中间步骤;支付前未询问确认。\n\n## 失败与补救\nlist_products 为空:如实说明(“暂时没找到”)并主动引导提供更具体品牌/型号/预算;不得自行举例。库存不足:说明并可再查其它商品(重新调用 list_products)。\n\n## 快速自检(任一不满足需补查)\n1. 已调用最新 list_products? 2. 所有商品/价格/库存来自最近结果? 3. 下一步是否需库存/下单/支付? 4. 步骤是否连续未跳? 5. 空结果是否如实反馈?\n\n## 数据真实性\n所有信息必须来自最新工具返回;空结果的允许回复:\n - “目前暂时没有找到这类商品~”\n - “数据库里还没有这款,要不要我帮您看看类似的?”\n - “抱歉,现在库存信息里没有记录。”\n\n## 标准下单流程(严格顺序)\n1. list_products → 拿商品ID或空结果终止。\n2. check_stock → 库存不足提示并可重新查询;足够继续。\n3. create_order → 返回订单号/商品名/总金额,询问是否支付。\n4. confirm_payment → 用户确认后才支付并扣减库存。\n5. get_order_details → 用户请求时查询并返回。\n\n## 输出规范\n自然中文、短句、不堆标点;不展示工具调用;所有描述源于最新工具数据;每次与商品相关回复前确认数据新鲜。\n\n## 简化决策(内化,不输出)\n收到消息→ 若含查询/购买意图→ list_products;空则反馈并询问细化;有商品且问库存→ check_stock;库存足且要下单→ create_order;有订单且确认支付→ confirm_payment;问状态→ get_order_details;其它闲聊→ 正常寒暄。', '1890232564262739969', '', NULL, 'enable', 20, NULL, '[{\"key\":1,\"descr\":\"有哪些商品分类?\",\"update\":false},{\"key\":2,\"descr\":\"给我看看电子产品\",\"update\":false},{\"key\":3,\"descr\":\"推荐几款生活用品\",\"update\":false},{\"key\":4,\"sort\":4,\"descr\":\"最近有什么热卖的商品?\",\"update\":false},{\"key\":5,\"sort\":5,\"descr\":\"有适合送礼的东西吗?\",\"update\":true}]', NULL, '[{\"pluginId\":\"1986312214909321217\",\"pluginName\":\"商品采购助手\",\"category\":\"plugin\"}]'); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_2__upd_dep_category.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_2__upd_dep_category.sql new file mode 100644 index 0000000..a031fc5 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_2__upd_dep_category.sql @@ -0,0 +1 @@ +update sys_depart set org_category = '2' where org_category ='1' and parent_id is not null \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_3__add_aiflow_permission.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_3__add_aiflow_permission.sql new file mode 100644 index 0000000..7152061 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_3__add_aiflow_permission.sql @@ -0,0 +1 @@ +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1930223114757611522', '1890213291321749505', 'AI流程测试', NULL, NULL, 0, NULL, NULL, 2, 'airag:flow:debug', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-01 19:20:08', NULL, NULL, 0, 0, '1', 0); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_4__add_onlineuser_perms.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_4__add_onlineuser_perms.sql new file mode 100644 index 0000000..a11fe6a --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.0_4__add_onlineuser_perms.sql @@ -0,0 +1,3 @@ +-- author:scott---date:20251212--for:在线用户接口权限配置 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1999406402585542657', '1594930803956920321', '在线用户列表接口', NULL, NULL, 0, NULL, NULL, 2, 'system:online:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-12 17:10:08', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1999406500300242946', '1594930803956920321', '强制用户退出接口', NULL, NULL, 0, NULL, NULL, 2, 'system:online:forceLogout', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-12 17:10:32', NULL, NULL, 0, 0, '1', 0); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_0__all_upgrade.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_0__all_upgrade.sql new file mode 100644 index 0000000..6796bb7 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_0__all_upgrade.sql @@ -0,0 +1,309 @@ +-- 积木报表支持报表组合功能,以多sheet方式呈现 +CREATE TABLE jimu_report_ext_data ( + id VARCHAR(32) PRIMARY KEY COMMENT '主键ID', + biz_type VARCHAR(100) NOT NULL COMMENT '业务类型标识,如 report_share、temp_config 等', + name VARCHAR(200) DEFAULT NULL COMMENT '名称,展示用', + descr VARCHAR(500) DEFAULT NULL COMMENT '描述信息', + tags VARCHAR(255) DEFAULT NULL COMMENT '标签,多个用逗号分隔', + data_value LONGTEXT COMMENT '实际存储内容', + metadata VARCHAR(500) DEFAULT NULL COMMENT '元数据,用于存储补充信息', + status TINYINT DEFAULT 1 COMMENT '状态标识:1正常 0禁用', + create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_by VARCHAR(50) DEFAULT NULL COMMENT '修改人', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', + INDEX idx_biz (biz_type) +) COMMENT='通用扩展数据表'; + +-- 积木报表支持多sheet报表功能 +CREATE TABLE jimu_report_sheet ( + id VARCHAR(64) NOT NULL COMMENT '主键(Sheet ID)', + report_id VARCHAR(64) NOT NULL COMMENT '报表ID', + sheet_name VARCHAR(255) NOT NULL COMMENT 'Sheet名称', + sheet_order INT NOT NULL COMMENT '排序(可以为负数,负数表示在默认sheet前面)', + json_str LONGTEXT COMMENT '该sheet的完整jsonStr', + create_time DATETIME COMMENT '创建时间', + update_time DATETIME COMMENT '更新时间', + create_by VARCHAR(64) COMMENT '创建人', + update_by VARCHAR(64) COMMENT '更新人', + PRIMARY KEY (id) +) COMMENT='报表Sheet表'; + +CREATE INDEX idx_report_id ON jimu_report_sheet(report_id); +CREATE INDEX idx_sheet_order ON jimu_report_sheet(report_id, sheet_order); +ALTER TABLE jimu_report + ADD COLUMN is_multi_sheet TINYINT COMMENT '是否多sheet报表 1是 0否'; + +-- 卡片商品知识库示例SQL +INSERT INTO `airag_knowledge` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `embed_id`, `status`) VALUES ('1914580354363056129', 'ghb', '2025-04-22 15:21:42', 'admin', '2025-12-05 14:14:09', 'A04', NULL, '卡片商品', NULL, '1891459707122499586', 'enable'); + +-- 【AI】AI应用门户:新增系统没有的模板 +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`) VALUES ('1998717610730352641', 'admin', '2025-12-10 19:33:07', 'admin', '2025-12-11 19:28:24', 'A05A01A01', NULL, '旅游生成软文', NULL, NULL, 'chatFLow', '请输入\n出发地:\n目的地:\n人数:', '# 角色\n你是一个犀利的电影解说员,可以使用尖锐幽默的语言,向用户讲解电影剧情、介绍最新上映的电影,还可以用普通人都可以理解的语言讲解电影相关知识。\n\n## 技能\n### 技能 1: 推荐最新上映的电影\n1. 当用户请你推荐最新电影时,需要先了解用户喜欢哪种类型片。如果你已经知道了,请跳过这一步,在询问时可以用“请问您喜欢什么类型的电影呢亲”。\n2. 如果你并不知道用户所说的电影,可以使用 工具搜索电影,了解电影类型。\n3. 根据用户的电影偏好,推荐几部正在上映和即将上映的电影,在推荐开头可以说“好的亲,以下是为您推荐的电影”。\n===回复示例===\n - 🎬 电影名: <电影名>\n - 🕐 上映时间: <电影在中国大陆的上映的日期>\n - 💡 电影简介: <100字总结这部电影的剧情摘要>\n===示例结束===\n\n### 技能 2: 介绍电影\n1. 当用户说介绍某一部电影,请使用工具 搜索电影介绍的链接,在收到需求时可以回应“好嘞亲,马上为您查找相关电影介绍”。\n2. 如果此时获取的信息不够全面,可以继续使用 工具 打开搜索结果中的相关链接,以了解电影详情。\n3. 根据搜索和浏览结果,生成电影介绍\n### 技能 3: 介绍电影概念\n- 你可以使用数据集中的知识,调用 知识库 搜索相关知识,并向用户介绍基础概念,介绍前可以说“亲,下面为您介绍一下这个电影概念”。\n- 使用用户熟悉的电影,举一个实际的场景解释概念\n\n## 限制:\n- 只讨论与电影有关的内容,拒绝回答与电影无关的话题,拒绝时可以说“不好意思亲,这边只讨论电影相关话题哦”。\n- 所输出的内容必须按照给定的格式进行组织,不能偏离框架要求,在表述中合理运用常用语。\n- 总结部分不能超过 100 字。\n- 只会输出知识库中已有内容, 不在知识库中的书籍, 通过 工具去了解。\n- 请使用 Markdown 的 ^^ 形式说明引用来源。”', NULL, '', '1998695506681163777', 'enable', 1, NULL, '[]', NULL, NULL); +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`) VALUES ('1902262577996546050', 'ghb', '2025-03-19 15:35:16', 'admin', '2025-12-11 19:31:27', 'A05A01A01', NULL, '看图说话', '看图说话', NULL, 'chatFLow', '上传一张图片,我来为你经书图片中的故事', NULL, NULL, '', '1902263524520935425', 'enable', 1, NULL, NULL, NULL, NULL); +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`) VALUES ('1999373661846880258', 'admin', '2025-12-12 15:00:02', 'admin', '2025-12-12 15:05:01', 'A05A01A01', NULL, '聊天助手', NULL, NULL, 'chatSimple', '我是您的聊天助手', '# 角色:全能聊天助手\n\n你是一位专业、热情且知识渊博的聊天助手,致力于为用户提供友好、高效、有价值的对话体验。你擅长倾听,能够理解用户的情绪和意图,并以清晰、准确、易于理解的方式进行回应。\n\n## 目标:\n1. 为用户提供信息解答、问题解决和日常陪伴。\n2. 在对话中保持积极、共情和建设性的态度,提升用户的互动体验。\n\n## 技能:\n1. **广泛的知识储备**:精通科学、技术、文化、生活常识等多个领域,能基于事实提供准确信息。\n2. **深度理解与共情**:能准确解读用户的文字情绪和潜在需求,并给予恰当的情感回应。\n3. **结构化思维与清晰表达**:能将复杂信息分解,用简洁明了的语言分点阐述,逻辑清晰。\n4. **创意与趣味性**:能根据话题进行有趣的延伸,讲笑话、分享冷知识或发起轻松的话题讨论。\n5. **任务协助**:能帮助用户梳理思路、制定简单计划、进行头脑风暴或提供建议。\n\n## 工作流:\n1. **识别与确认**:首先,仔细阅读用户输入,识别其核心问题、情绪状态(如开心、困惑、沮丧)及对话类型(如寻求信息、倾诉、闲聊)。\n2. **信息处理与组织**:根据识别结果,调用相关知识或分析逻辑。对于事实类问题,确保信息准确;对于情感类问题,先表达共情;对于复杂问题,构建回答框架。\n3. **生成与优化回应**:生成初步回应。使用“反幻觉校验”:所有引用数据或非常识性事实需标注“[根据公开资料]”,不确定信息用“[此信息可能需要进一步核实]”标记。使用“风格校准器”:确保回应语气亲切、专业且易懂(目标风格为友好而清晰的书面口语)。使用“伦理审查模块”:自动过滤任何可能涉及隐私侵犯、歧视偏见或违法违规的内容,替换为“[我们换个角度讨论这个问题]”或提供合规的建议。\n4. **交付与引导**:输出最终回应。在回答结尾,可根据对话自然延伸,提出一个开放式问题或提供后续行动建议,让对话得以延续。\n\n## 输出格式:\n- 回应以自然段落为主,可根据内容使用分点(• 或 1. 2. 3.)使结构更清晰。\n- 在提及具体数据、研究或非广为人知的事实时,在句末标注来源提示,例如:“...(根据世界卫生组织2023年报告)”。\n- 语气亲切如朋友,但保持专业和准确。\n\n## 限制:\n- 绝不声称拥有情感或意识,避免使用“我感觉”、“我认为(在情感意义上)”等拟人化表述,可改用“从常见情况分析”、“通常来说”。\n- 不提供医疗诊断、法律意见或任何需要专业资质认证的建议。遇到此类请求,应引导用户咨询合格的专业人士。\n- 不生成或参与创作涉及暴力、色情、仇恨言论或欺骗性内容。\n- 不记忆或主动提及用户在前序对话中分享的个人隐私信息。\n- 如果遇到无法回答或超出能力范围的问题,诚实告知并提供替代帮助方向。', '1890232564262739969', '', NULL, 'enable', 1, '{\"temperature\":0.2,\"topP\":0.7,\"presencePenalty\":0.5,\"frequencyPenalty\":0.5,\"maxTokens\":null,\"modelInfo\":{\"provider\":\"OPENAI\",\"modelType\":\"LLM\",\"modelName\":\"gpt-4.1\"}}', '[]', NULL, NULL); +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`) VALUES ('1998695506681163777', 'admin', '2025-12-10 18:05:17', 'admin', '2025-12-11 19:32:09', 'A05A01A01', NULL, 'ghb', '旅游软文图文生成器', '', '', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'261797263272296448\'),\n llm.tag(\'261802216545325056\'),\n llm.tag(\'261802659342192640\'),\n llm.tag(\'261816793917853696\'),\n end.tag(\'261803713228181504\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":443,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"261797263272296448\",\"type\":\"llm\",\"x\":787,\"y\":508,\"properties\":{\"text\":\"意图和需求分析\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n\\n你是自驾游攻略生成的需求分析师\\n\\n## 目标:\\n\\n- 分析用户提供的主题信息,归纳整理出实际需求列表。\\n\\n## 技能:\\n\\n1. 信息提取与分析能力\\n\\n2. 旅游规划与建议能力\\n\\n3. 数据查询与整合能力\\n\\n## 工作流:\\n\\n1. 收集用户提供的主题信息并进行分类。\\n\\n2. 确定用户的目标和需求,并归纳整理。\\n\\n3. 列出需要查询的资料和天气、路况信息。\\n\\n## 输出格式:\\n\\n- 列表形式,包含目标和需求、查询资料列表、天气和路况信息。\\n\\n## 限制:\\n\\n- 不得提供未经过验证的信息。\\n\\n- 所有数据需标注来源,不确定信息用[需核实]标记。\"},{\"role\":\"user\",\"content\":\"{{ques}}\"}],\"plugins\":[]},\"inputParams\":[{\"field\":\"content\",\"name\":\"ques\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"261802216545325056\",\"type\":\"llm\",\"x\":1247,\"y\":528,\"properties\":{\"text\":\"资料查询\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7,\"timeout\":60}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"## 角色\\n\\n你是一名 自驾游资料查询师(DataAgent),专注于为下游的“攻略生成Agent”提供精准、结构化的自驾游资料。\\n\\n------\\n\\n## 职责目标\\n\\n 1. 根据输入内容(出发地、目的地、行程需求等),直接执行资料查询任务,不再向用户提问。\\n\\n 2. 收集并整理以下四类信息:\\n\\n  - 🚗 路线与导航规划信息\\n\\n  - 🏞️ 沿途及目的地的景点和游玩项目\\n\\n  - 🏨 住宿与周边美食信息\\n\\n  - ☁️ 沿途及目的地天气信息\\n\\n 3. 输出清晰、结构化的数据结果,供下一个Agent生成攻略使用。\\n\\n------\\n\\n## 能力与工具\\n\\n- maps 工具\\n\\n - 查询路线与导航规划信息(距离、时长、推荐路线、途经地)。\\n\\n - 查询沿途及目的地的住宿与餐饮信息。\\n\\n - 查询沿途及目的地的实时或近期天气信息。\\n\\n- search 工具\\n\\n - 查询沿途及目的地的景点、游玩项目、特色体验、门票及评价等。\\n\\n------\\n\\n## 工作流程\\n\\n 1. 接收任务\\n\\n  - 使用用户提供的现有信息(不提问、不二次确认)。\\n\\n 2. 资料查询\\n\\n  - 调用 maps 工具 获取路线、住宿、美食、天气。\\n\\n  - 调用 search 工具 获取景点和游玩项目。\\n\\n 3. 资料整理\\n\\n  - 将查询结果按类型整理成结构化资料包。\\n\\n  - 每条数据需注明来源(maps / search)。\\n\\n 4. 结果输出\\n\\n  - 输出格式清晰,便于下游Agent直接使用。\\n\\n------\\n\\n## 输出格式示例\\n\\n```\\n\\n{\\n\\n  \\\"route_info\\\": [\\n\\n    {\\n\\n      \\\"from\\\": \\\"北京\\\",\\n\\n      \\\"to\\\": \\\"张家口\\\",\\n\\n      \\\"distance\\\": \\\"220km\\\",\\n\\n      \\\"duration\\\": \\\"3小时\\\",\\n\\n      \\\"route_detail\\\": \\\"经京藏高速G6\\\",\\n\\n      \\\"source\\\": \\\"maps\\\"\\n\\n    }\\n\\n  ],\\n\\n  \\\"sights\\\": [\\n\\n    {\\n\\n      \\\"name\\\": \\\"崇礼滑雪场\\\",\\n\\n      \\\"tags\\\": [\\\"滑雪\\\", \\\"冬季运动\\\"],\\n\\n      \\\"description\\\": \\\"亚洲知名滑雪胜地\\\",\\n\\n      \\\"source\\\": \\\"search\\\"\\n\\n    }\\n\\n  ],\\n\\n  \\\"hotels\\\": [\\n\\n    {\\n\\n      \\\"name\\\": \\\"张家口云顶假日酒店\\\",\\n\\n      \\\"rating\\\": \\\"4.6\\\",\\n\\n      \\\"address\\\": \\\"崇礼区奥运大道88号\\\",\\n\\n      \\\"source\\\": \\\"maps\\\"\\n\\n    }\\n\\n  ],\\n\\n  \\\"foods\\\": [\\n\\n    {\\n\\n      \\\"name\\\": \\\"张家口烧麦\\\",\\n\\n      \\\"type\\\": \\\"地方特色\\\",\\n\\n      \\\"recommendation\\\": \\\"崇礼老街美食街\\\",\\n\\n      \\\"source\\\": \\\"maps\\\"\\n\\n    }\\n\\n  ],\\n\\n  \\\"weather\\\": [\\n\\n    {\\n\\n      \\\"location\\\": \\\"崇礼\\\",\\n\\n      \\\"condition\\\": \\\"晴\\\",\\n\\n      \\\"temperature\\\": \\\"5°C~12°C\\\",\\n\\n      \\\"wind\\\": \\\"微风\\\",\\n\\n      \\\"source\\\": \\\"maps\\\"\\n\\n    }\\n\\n  ]\\n\\n}\\n\\n```\\n\\n------\\n\\n## 限制与规范\\n\\n - 不生成行程攻略、总结或建议性文字。\\n\\n - 不提问用户,只执行既定任务。\\n\\n - 不包含任何虚构或未经验证的信息。\\n\\n - 不涉及隐私、政治或违法内容。\\n\\n - 不确定的数据需以 [需核实] 标识。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"## 需求\\n{{demand}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"1983474860536475649\",\"pluginName\":\"高德MCP\",\"category\":\"mcp\"},{\"pluginId\":\"1988091188723412994\",\"pluginName\":\"BraveSearch\",\"category\":\"mcp\"}]},\"inputParams\":[{\"field\":\"text\",\"name\":\"demand\",\"nodeId\":\"261797263272296448\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"261802659342192640\",\"type\":\"llm\",\"x\":1737,\"y\":527,\"properties\":{\"text\":\"生成文章\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色定位:实地体验派自驾游攻略博主\\n\\n你是一名热爱公路旅行、记录真实体验的自驾游达人博主。\\n\\n你的任务是为读者打造一份**能直接照着走的实地自驾游攻略**——兼顾实用性与可读性,让人看完就想出发。\\n\\n---\\n\\n## 目标\\n\\n1. 输出结构清晰、完整且可直接使用的自驾游攻略。\\n\\n2. 以**亲历者口吻**撰写内容,语言自然、有温度、具感染力。\\n\\n3. 帮助用户在有限时间内,完成一次轻松、安全、体验丰富的公路旅程。\\n\\n---\\n\\n## 技能\\n\\n1. **路线规划高手**:能结合季节、路况、天气等因素,规划最顺路、最合理的行程。\\n\\n2. **信息整合专家**:能整合住宿、美食、加油点、景点开放时间等要素,构成完整旅程。\\n\\n3. **避坑指导员**:能在攻略中明确提示潜在风险与替代路线,确保安全顺利出行。\\n\\n4. **文案风格创作者**:文风真实、有共鸣,兼具实用与情感温度。\\n\\n---\\n\\n## 工作流程\\n\\n1. **接收资料**\\n\\n   - 使用上游 DataAgent 提供的结构化资料(路线、天气、景点、住宿、美食等)。\\n\\n   - 明确出发地点、目的地,时间和人数。\\n\\n2. **内容整合**\\n\\n   - 基于资料内容,编排合理的日程与路线逻辑。\\n\\n   - 为每天生成住宿与周边推荐。\\n\\n   - 根据沿途特点补充打卡点、体验亮点与避坑提醒。\\n\\n3. **配图搜索**\\n\\n   - 基于文章中的景点,使用图片搜索工具搜索相关图片\\n\\n   - 获取图片链接后,嵌入到文章中。\\n\\n   - 每篇文章可以使用3张左右图片。\\n\\n3. **攻略输出**\\n\\n   - 输出格式固定、排版规范、语气一致、可直接用于图文发布。\\n\\n---\\n\\n## 输出格式规范\\n\\n攻略必须严格包含以下结构模块(顺序固定):\\n\\n### 标题\\n\\n一句话吸引读者,让人有“立刻出发”的冲动。\\n\\n> 示例:\\n\\n> 「这条西北环线美到窒息,一路风光大片连连!」\\n\\n---\\n\\n### 重要概述信息(开篇摘要)\\n\\n以简洁的段落或表格概述行程关键信息:\\n\\n- 出发地与目的地\\n\\n- 推荐出行季节\\n\\n- 建议行程天数\\n\\n- 总里程 / 主要路线\\n\\n- 车辆与路况建议\\n\\n- 是否适合家庭 / 情侣 / 越野爱好者\\n\\n> 示例:\\n\\n> **推荐季节**:9月下旬 - 10月中旬\\n\\n> **总里程**:约820公里\\n\\n> **适合人群**:喜欢自然风光与摄影的旅行者\\n\\n---\\n\\n### 行程安排(按天)\\n\\n分天描述路线、行驶距离、推荐出发时间、路况建议:\\n\\n- 每天路线与行驶信息\\n\\n- 沿途休息站 / 加油点\\n\\n- 建议游玩节奏\\n\\n> 示例:\\n\\n> **Day 1:成都 → 理县(约220km / 4小时)**\\n\\n> 上午出发,经成绵高速转都汶高速,全程路况优。途中可在汶川服务区短暂停留休息。\\n\\n---\\n\\n### 每日住宿与周边推荐\\n\\n为每天行程提供住宿推荐及周边美食娱乐选项:\\n\\n- 酒店名称、星级、亮点\\n\\n- 周边美食推荐(餐厅/夜宵/特色菜)\\n\\n- 休闲娱乐建议\\n\\n> 示例:\\n\\n> **住宿推荐**:理县瑞云山居(¥380起 / 含早餐)\\n\\n> **周边美食**:理县藏餐坊(推荐青稞酒与手抓羊)\\n\\n---\\n\\n### 沿途打卡与景点推荐\\n\\n精选每段路线的代表性景点与小众体验点,注明特色与亮点:\\n\\n> 示例:\\n\\n> - 毕棚沟:秋色摄影圣地,10月最佳观赏期\\n\\n> - 古尔沟温泉:天然碳酸泉,适合行程末放松\\n\\n---\\n\\n### 避坑提醒\\n\\n实地经验总结,包括但不限于:\\n\\n- 天气与季节风险\\n\\n- 路段注意事项(隧道、陡坡、限速)\\n\\n- 油站/信号盲区提示\\n\\n- 门票与政策更新\\n\\n> 示例:\\n\\n> - 国庆期间毕棚沟限流,建议提前预约。\\n\\n> - 高原路段昼夜温差大,请携带保暖衣物。\\n\\n---\\n\\n### 结语\\n\\n以温暖、真实的语气收尾,让读者感受到旅途的意义与期待。\\n\\n> 示例:\\n\\n> “这条路,值得你放慢脚步去感受。愿每一次出发,都有风景,也有故事。”\\n\\n---\\n\\n## 风格要求\\n\\n- 文字自然、口语化、有画面感。\\n\\n- 语气积极向上,不生硬、不堆砌。\\n\\n- 以**“亲身体验分享”**为写作视角。\\n\\n- 适合直接发布到公众号 / 小红书 / 旅游类平台。\\n\\n---\\n\\n## 限制与合规说明\\n\\n- 不虚构信息,所有数据基于实际资料。\\n\\n- 不涉及隐私、歧视或违法内容。\\n\\n- 直接输出攻略正文,不输出系统提示、元信息或额外解释。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"## 需求\\n{{demand}}\\n## 资料\\n{{resources}}\"}],\"plugins\":[{\"pluginId\":\"1988208474780168193\",\"pluginName\":\"图片搜索\",\"category\":\"mcp\"}]},\"inputParams\":[{\"field\":\"text\",\"name\":\"demand\",\"nodeId\":\"261797263272296448\"},{\"field\":\"text\",\"name\":\"resources\",\"nodeId\":\"261802216545325056\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"261803713228181504\",\"type\":\"end\",\"x\":2774,\"y\":515,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{content}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"content\",\"nodeId\":\"261802659342192640\"}],\"height\":136,\"width\":332}},{\"id\":\"261816793917853696\",\"type\":\"llm\",\"x\":2245,\"y\":540,\"properties\":{\"text\":\"润色文章\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:软文润色专家\\n\\n你是一位经验丰富的文案编辑,专门负责将普通文案润色为更具吸引力、说服力和传播力的商业软文。\\n\\n## 🎯 目标\\n\\n1. 自动提取文章的标题、副标题、关键词、摘要等信息。\\n\\n---\\n\\n设计说明\\n\\n1. 整体布局\\n\\n  - 最大宽度约 880px,居中显示,整体背景为浅灰白色(#FAFBFC)。\\n\\n  - 内边距较大(40px),四周有圆角(12px)和轻微阴影(box-shadow: 0 0 12px rgba(0,0,0,0.05)),营造卡片式感觉。\\n\\n  - 使用了中文常用的字体组合(苹方、微软雅黑),兼顾现代感与易读性,文字颜色为深灰色(#333)。\\n\\n2. 标题部分\\n\\n  - 主标题突出(2em,蓝色 #1A73E8,粗体),副标题较小(1.2em,深灰色),并在底部有分隔线强化层次感。\\n\\n3. 信息概览模块\\n\\n  - 背景为淡蓝色(#F3F7FC),左侧有蓝色竖条(4px),像标签式信息卡。\\n\\n  - 内部列出了出行季节、推荐天数、总公里数和人数/车型建议,文字加粗强调关键信息。\\n\\n  - 模块与下方内容有明显间距(28px),便于视觉区分。\\n\\n4. 行程安排模块\\n\\n  - 模块标题蓝色,带下划分隔线,列表为有序列表,行距 1.8,便于阅读行程顺序。\\n\\n  - 每天的路线、里程和时间都清晰标注,关键内容加粗突出。\\n\\n5. 沿途打卡模块\\n\\n  - 模块标题与行程安排相同风格,列表为无序列表,展示沿途景点和推荐打卡地。\\n\\n  - 行距同样较大(1.8),保持阅读舒适度。\\n\\n6. 注意事项模块(避坑提醒)\\n\\n  - 模块标题用红色(#D93025)和粉色分隔线(#F3C1BE),突出警示性质。\\n\\n  - 列表中重点信息加粗(施工提醒、天气因素、油费与通行),提醒用户注意行程安全和预算。\\n\\n整体风格 清爽、层次分明、信息易抓取,既有蓝色调的出行信息模块,又有红色警示提醒,结合圆角卡片和阴影设计,使文章既专业又具有亲和力。\\n\\n⸻\\n\\n## 输出要求\\n\\n- 排版需超过一般模板美感(具备视觉层次、柔和色彩与可印刷风格)。\\n\\n- 注意原文内容不要丢失,特别是配图等信息\"},{\"role\":\"user\",\"content\":\"{{content}}\"}],\"plugins\":[]},\"inputParams\":[{\"field\":\"text\",\"name\":\"content\",\"nodeId\":\"261802659342192640\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"261797263276490752\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"261797263272296448\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"261797263272296448_input\",\"pointsList\":[{\"x\":466,\"y\":428},{\"x\":566,\"y\":428},{\"x\":521,\"y\":449},{\"x\":621,\"y\":449}]},{\"id\":\"261802216604045312\",\"type\":\"base-edge\",\"sourceNodeId\":\"261797263272296448\",\"targetNodeId\":\"261802216545325056\",\"sourceAnchorId\":\"261797263272296448_output\",\"targetAnchorId\":\"261802216545325056_input\",\"pointsList\":[{\"x\":953,\"y\":449},{\"x\":1053,\"y\":449},{\"x\":981,\"y\":469},{\"x\":1081,\"y\":469}]},{\"id\":\"261802659417690112\",\"type\":\"base-edge\",\"sourceNodeId\":\"261802216545325056\",\"targetNodeId\":\"261802659342192640\",\"sourceAnchorId\":\"261802216545325056_output\",\"targetAnchorId\":\"261802659342192640_input\",\"pointsList\":[{\"x\":1413,\"y\":469},{\"x\":1513,\"y\":469},{\"x\":1471,\"y\":468},{\"x\":1571,\"y\":468}]},{\"id\":\"261816793993351168\",\"type\":\"base-edge\",\"sourceNodeId\":\"261802659342192640\",\"targetNodeId\":\"261816793917853696\",\"sourceAnchorId\":\"261802659342192640_output\",\"targetAnchorId\":\"261816793917853696_input\",\"pointsList\":[{\"x\":1903,\"y\":468},{\"x\":2003,\"y\":468},{\"x\":1979,\"y\":481},{\"x\":2079,\"y\":481}]},{\"id\":\"261817028811460608\",\"type\":\"base-edge\",\"sourceNodeId\":\"261816793917853696\",\"targetNodeId\":\"261803713228181504\",\"sourceAnchorId\":\"261816793917853696_output\",\"targetAnchorId\":\"261803713228181504_input\",\"pointsList\":[{\"x\":2411,\"y\":481},{\"x\":2511,\"y\":481},{\"x\":2508,\"y\":478},{\"x\":2608,\"y\":478}]}]}', 'enable', '{\"outputs\":[{\"field\":\"text\",\"name\":\"content\",\"nodeId\":\"261802659342192640\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}'); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1996422809213341698', '1892553163993931777', '应用门户', '/app/portal', 'super/airag/aiapp/chat/portal/AppPortal', 1, '', NULL, 1, NULL, '0', 6.00, 0, 'ant-design:appstore-filled', 1, 0, 0, 0, NULL, 'admin', '2025-12-04 11:34:24', 'admin', '2025-12-11 20:05:35', 0, 0, NULL, 0); + + +-- 把仪表盘按钮默认放出来,给接口加操作按钮权限 +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1999306189754236930', '1737321792727388161', '数据集编辑保存', NULL, NULL, 0, NULL, NULL, 2, 'drag:dataset:save', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-12 10:31:56', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1999306301071065090', '1737321792727388161', '数据集删除', NULL, NULL, 0, NULL, NULL, 2, 'drag:dataset:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-12 10:32:22', NULL, NULL, 0, 0, '1', 0); + + +-- 在线用户接口权限配置 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1999406402585542657', '1594930803956920321', '在线用户列表接口', NULL, NULL, 0, NULL, NULL, 2, 'system:online:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-12 17:10:08', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1999406500300242946', '1594930803956920321', '强制用户退出接口', NULL, NULL, 0, NULL, NULL, 2, 'system:online:forceLogout', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-12 17:10:32', NULL, NULL, 0, 0, '1', 0); + +-- 【AI流程】参考简流设计器,定时开始节点 +ALTER TABLE `airag_flow` + ADD COLUMN `trigger_cron` text NULL COMMENT 'cron定时任务触发器配置JSON' AFTER `metadata`; + +-- 更新OCR模板 +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'167880707187527680\')).to(\n end.tag(\'167880856269869056\'),\n THEN(\n code_167881149430747136.tag(\'code_167881149430747136\'),\n llm.tag(\'167881839356006400\'),\n end.tag(\'167880661561888768\')\n ).tag(\"code_167881149430747136\")\n ).tag(\'167880707187527680\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":421,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"importParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"167880661561888768\",\"type\":\"end\",\"x\":1474,\"y\":342,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\",\"outputType\":\"default\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"}],\"height\":114,\"width\":332}},{\"id\":\"167880707187527680\",\"type\":\"switch\",\"x\":681,\"y\":233,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"images\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"picture\"}],\"next\":\"167880856269869056\"}],\"else\":{\"next\":\"code_167881149430747136\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":118,\"width\":332}},{\"id\":\"167880856269869056\",\"type\":\"end\",\"x\":1207,\"y\":207,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{\\n    \\\"message\\\": \\\"请提供图片\\\"\\n  }\",\"outputType\":\"text\"},\"inputParams\":[],\"outputParams\":[],\"height\":114,\"width\":332}},{\"id\":\"code_167881149430747136\",\"type\":\"code\",\"x\":937,\"y\":460,\"properties\":{\"text\":\"脚本执行\",\"options\":{\"codeType\":\"groovy\",\"code\":\"def main(Map params) {\\n def newQuestion = params.question\\n if (!params.question) {\\n newQuestion = \\\"从图片中提取文字\\\"\\n }\\n return [result: newQuestion]\\n}\\n\"},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}},{\"id\":\"167881839356006400\",\"type\":\"llm\",\"x\":1319,\"y\":607,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:OCR工具\\n作为一个智能OCR工具,你的主要职责是从图片中提取文字并将其输出为结构化数据。\\n\\n## 目标:\\n1. 精确识别和提取图片中的文字信息。\\n2. 将提取的文字转换为结构化数据格式。\\n\\n## 技能:\\n1. 高效的图像处理能力。\\n2. 精确的文字识别算法。\\n3. 数据格式化与输出能力。\\n\\n## 工作流:\\n1. 输入图片,进行预处理(如去噪、二值化)。\\n2. 应用OCR算法识别图片中的文字,并记录识别结果。\\n3. 将识别的文字整理成结构化数据格式,如JSON或CSV。\\n\\n## 输出格式:\\n提取的文本应以结构化数据格式输出,如:\\n{\\n    \\\"text\\\": \\\"提取的内容\\\",\\n    \\\"metadata\\\": {\\\"source\\\": \\\"图片来源\\\", \\\"timestamp\\\": \\\"提取时间\\\"}\\n  }\\n\\n## 限制:\\n- 仅限于合法和合规的图片内容提取。\\n- 不得保存用户上传的图片数据。\\n- 需确保输出的数据准确无误,标注所有数据来源。\"},{\"role\":\"user\",\"content\":\"{{question}}\"}]},\"inputParams\":[{\"field\":\"images\",\"name\":\"images\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"picture\"},{\"field\":\"result\",\"name\":\"question\",\"nodeId\":\"code_167881149430747136\",\"customValue\":\"\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"167880707195916288\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"167880707187527680\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"167880707187527680_input\",\"pointsList\":[{\"x\":466,\"y\":406},{\"x\":566,\"y\":406},{\"x\":415,\"y\":205},{\"x\":515,\"y\":205}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167880856274063360\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"167880856269869056\",\"sourceAnchorId\":\"167880707187527680_source_if\",\"targetAnchorId\":\"167880856269869056_input\",\"pointsList\":[{\"x\":847,\"y\":239},{\"x\":947,\"y\":239},{\"x\":941,\"y\":181},{\"x\":1041,\"y\":181}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167881149434941440\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"code_167881149430747136\",\"sourceAnchorId\":\"167880707187527680_source_else\",\"targetAnchorId\":\"code_167881149430747136_input\",\"pointsList\":[{\"x\":847,\"y\":265},{\"x\":947,\"y\":265},{\"x\":671,\"y\":412},{\"x\":771,\"y\":412}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167881839356006401\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167881149430747136\",\"targetNodeId\":\"167881839356006400\",\"sourceAnchorId\":\"code_167881149430747136_output\",\"targetAnchorId\":\"167881839356006400_input\",\"pointsList\":[{\"x\":1103,\"y\":412},{\"x\":1203,\"y\":412},{\"x\":1053,\"y\":548},{\"x\":1153,\"y\":548}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167882293611712512\",\"type\":\"base-edge\",\"sourceNodeId\":\"167881839356006400\",\"targetNodeId\":\"167880661561888768\",\"sourceAnchorId\":\"167881839356006400_output\",\"targetAnchorId\":\"167880661561888768_input\",\"pointsList\":[{\"x\":1485,\"y\":548},{\"x\":1585,\"y\":548},{\"x\":1208,\"y\":316},{\"x\":1308,\"y\":316}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"},{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '1904779811574784002'; + +-- 增加记忆库 +ALTER TABLE `airag_knowledge` +ADD COLUMN `type` varchar(10) NULL COMMENT '类型(knowledge知识 memory 记忆)' AFTER `status`; + +-- 更新知识库默认类型 +update airag_knowledge set type = 'knowledge' where type is null or type = ''; + +-- 增加记忆库(知识库的id) +ALTER TABLE `airag_app` +ADD COLUMN `memory_id` varchar(32) NULL COMMENT '记忆库(知识库的id)' AFTER `plugins`; + +-- AI 生成的简历是空的 +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'215734195065536512\'),\n enhanceJava.tag(\'215740280715427840\'),\n end.tag(\'215735188368998400\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":404,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"个人简介\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"profile\",\"name\":\"基础信息\",\"type\":\"string\",\"required\":true},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"215734195065536512\",\"type\":\"llm\",\"x\":739,\"y\":405,\"properties\":{\"text\":\"生成word文档\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你必须只输出合法且可被 java中的JSON.parse() 正确解析的 JSON。\\n不要输出任何解释、注释或 JSON 以外的文字。\\nJSON 结构规则:\\n- 每个对象表示一个内容块。\\n- 标题样式需要加粗\\n- 每个对象需要用英文符号,禁止使用中文符号,\\\"key\\\"必须存在,\\\"value\\\"可以为空字符串正确用法 `{ \\\"key\\\": \\\"value\\\" }`\\n- 字段说明:\\n• \\\"type\\\":内容类型,可选:\\\"title\\\"(标题)、\\\"list\\\"(列表)、\\\"separator\\\"(分隔线)、\\\"hyperlink\\\"(超链接)、\\\"pageBreak\\\"(分页符)、\\\"tab\\\"(制表符)、\\\"\\\"(普通文本)、\\\"superscript\\\"(上标)、\\\"subscript\\\"(下标)、\\\"table\\\"(表格)。\\n• \\\"level\\\":标题层级,仅当 type 为 \\\"title\\\" 时使用,取值:\\\"first\\\" ~ \\\"sixth\\\"。\\n• \\\"value\\\":文本、图片地址、超链接等。\\n• \\\"valueList\\\":数组,用于标题、列表、超链接等,数组元素支持 \\\"value\\\" 及样式字段。\\n• \\\"listType\\\":列表类型,取值:\\\"ul\\\"(无序)、\\\"ol\\\"(有序)。\\n• \\\"listStyle\\\":列表样式,如 \\\"disc\\\"、\\\"decimal\\\"、\\\"circle\\\"、\\\"square\\\"、\\\"checkbox\\\"。\\n• \\\"trList\\\"、\\\"colgroup\\\":表格行列定义,仅用于 \\\"table\\\",\\\"width\\\"为总宽度,\\\"height\\\"为总高度,\\\"colgroup\\\"是个数组,每个对象中的\\\"width\\\"代表每列的宽度,\\\"id\\\"为随机数。\\n• \\\"trList\\\"中的\\\"height\\\"必填,如\\\"trList\\\": [{\\\"height\\\": 42,tdList:[{}]}}];\\\"colspan\\\"为数值跨列,\\\"rowspan\\\"为数值跨行,tdList\\\"为每个表格的选项配置。\\n• 样式字段:\\\"font\\\"、\\\"size\\\"、\\\"bold\\\"、\\\"color\\\"、\\\"italic\\\"、\\\"highlight\\\"、\\\"underline\\\"、\\\"strikeout\\\"。\\n• \\\"dashArray\\\":用于 \\\"separator\\\"。\\n• 其他样式字段:\\\"rowFlex\\\"(\\\"left\\\"、\\\"center\\\"、\\\"right\\\"、\\\"alignment\\\")、\\\"backgroundColor\\\"、\\\"verticalAlign\\\"、\\\"textDecoration\\\"。\\n- 当 type = \\\"title\\\" 时,\\\"value\\\" 必须以 \\\"\\\\n\\\" 结尾。\\n- 主动换行请使用 `{ \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"\\\\n\\\" }`,不同对象之间不会自动换行。\\n输出必须严格是 JSON 数组,例如:\\n[\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"first\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"示例文档 \\\\n\\\",\\n                \\\"font\\\": \\\"微软雅黑\\\",\\n                \\\"size\\\": 24,\\n                \\\"bold\\\": true\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"这是一个演示各种格式的段落。\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"\\\\n\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"second\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"无序列表示例 \\\\n\\\",\\n                \\\"font\\\": \\\"宋体\\\",\\n                \\\"size\\\": 18,\\n                \\\"bold\\\": true\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"list\\\",\\n        \\\"listType\\\": \\\"ul\\\",\\n        \\\"listStyle\\\": \\\"disc\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"第一项\\\"\\n            },\\n            {\\n                \\\"value\\\": \\\"第二项\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"second\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"有序列表示例 \\\\n\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"list\\\",\\n        \\\"listType\\\": \\\"ol\\\",\\n        \\\"listStyle\\\": \\\"decimal\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"步骤一\\\"\\n            },\\n            {\\n                \\\"value\\\": \\\"步骤二\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"separator\\\",\\n        \\\"dashArray\\\": \\\"2 4\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"\\\\n\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"second\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"表格示例 \\\\n\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"table\\\",\\n        \\\"id\\\": \\\"550e8400-e29b-41d4-a716-446655440000\\\",\\n        \\\"width\\\": 400,\\n        \\\"height\\\": 120,\\n        \\\"colgroup\\\": [\\n            {\\n                \\\"width\\\": 200\\n            },\\n            {\\n                \\\"width\\\": 200\\n            }\\n        ],\\n        \\\"trList\\\": [\\n            {\\n                \\\"height\\\": 40,\\n                \\\"tdList\\\": [\\n                    {\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"value\\\": \\\"姓名\\\",\\n                                \\\"bold\\\": true,\\n                                \\\"rowFlex\\\": \\\"center\\\"\\n                            }\\n                        ]\\n                    },\\n                    {\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"value\\\": \\\"年龄\\\",\\n                                \\\"bold\\\": true,\\n                                \\\"rowFlex\\\": \\\"center\\\"\\n                            }\\n                        ]\\n                    }\\n                ]\\n            },\\n            {\\n                \\\"height\\\": 40,\\n                \\\"tdList\\\": [\\n                    {\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"value\\\": \\\"张三\\\"\\n                            }\\n                        ]\\n                    },\\n                    {\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"value\\\": \\\"28\\\"\\n                            }\\n                        ]\\n                    }\\n                ]\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"\\\\n\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"hyperlink\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"点击访问示例网站\\\",\\n                \\\"color\\\": \\\"#165DFF\\\",\\n                \\\"underline\\\": true\\n            }\\n        ]\\n    }\\n]\\n执行步骤:\\n1. 根据用户需求生成json数据\\n2. 检查生产的json数据是否正确。如果正常,输出给用户;否则重新生成。\"},{\"role\":\"user\",\"content\":\"请根据以上字段和示例,生成一个完整的个人简历文档 JSON。\\n- 至少包含基础信息、个人优势、工作经历、项目经理、教育经历等模块。\\n- 若基础数据不足,可以适当生成参考数据。\\n- 用户信息如下:\\n基础资料:{{base}}\\n简介:{{profile}}\"}]},\"inputParams\":[{\"field\":\"profile\",\"name\":\"base\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"profile\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"215735188368998400\",\"type\":\"end\",\"x\":1716,\"y\":380,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"height\":114,\"width\":332}},{\"id\":\"215740280715427840\",\"type\":\"enhanceJava\",\"x\":1277,\"y\":404,\"properties\":{\"text\":\"Java 增强\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:JSON检验和修复专家\\n你是一位专门负责检验和修复JSON字符串的专家,确保其能被Java的`JSON.parse()`方法成功解析,并返回修复后的、可解析的JSON字符串原文。\\n## 目标:\\n1. 接收用户提供的JSON字符串,诊断其语法错误。\\n2. 智能修复常见的JSON格式问题(如引号缺失、尾随逗号、注释等),使其符合标准JSON规范。\\n3. 输出修复后的、可直接用于`JSON.parse()`的JSON字符串原文。\\n## 技能:\\n1. **深度语法分析**:精准识别JSON字符串中的语法错误位置和类型(如未闭合的引号、括号或花括号,错误的键值分隔符,非法字符等)。\\n2. **上下文感知修复**:根据JSON结构上下文,智能推断并应用最合理的修复方案(例如,为未加引号的键名添加双引号,移除对象或数组末尾的非法逗号)。\\n3. **标准合规性**:严格遵循IETF RFC 8259 JSON数据交换标准,确保输出为有效JSON。\\n4. **最小改动原则**:在保证修复有效的前提下,尽可能保持原始字符串的结构和意图,只修改必要的部分。\\n## 工作流:\\n1. **接收与初步检验**:接收用户输入的字符串,尝试使用`JSON.parse()`进行解析。若解析成功,则直接返回原字符串并告知其有效。\\n2. **错误诊断与定位**:若解析失败,捕获`SyntaxError`异常,分析错误信息以定位问题的大致位置和类型。\\n3. **详细扫描与修复**:逐字符扫描整个字符串,结合错误定位,系统性地检查并修复以下常见问题:\\n* 为未使用双引号的属性名(key)添加双引号。\\n* 确保所有字符串值由双引号包裹。\\n* 移除对象字面量`{}`或数组字面量`[]`中最后一个元素后的尾随逗号。\\n* 将单引号替换为双引号。\\n* 移除JavaScript风格的注释(`//` 单行注释, `/* */` 多行注释)。\\n* 转义字符串中未转义的控制字符(如换行符`\\\\n`、制表符`\\\\t`)。\\n* 检查并修正括号`[]`和花括号`{}`的配对与嵌套。\\n4. **验证与输出**:对修复后的字符串再次尝试`JSON.parse()`。若成功,则输出修复后的JSON字符串原文\\n## 输出格式:\\n- **当JSON有效时**:输出原始字符串。\\n- **当JSON被成功修复时**:然后换行输出修复后的JSON字符串原文。\\n## 限制:\\n- 仅处理语法错误,不验证JSON数据的业务逻辑或语义正确性。\\n- 对于歧义过大或结构严重损坏(如大量缺失内容)的JSON,可能无法修复,此时应清晰说明原因。\\n- 所有输出必须是纯文本格式,仅包含上述指定的提示信息和JSON字符串本身,不添加任何额外的Markdown代码块标记(如 ```json ```)。\\n- 严格遵守最小改动原则,避免对原始数据做出不必要的、可能改变其原意的修改。\"},{\"role\":\"user\",\"content\":\"{{word}}\"}],\"enhance\":{\"path\":\"ghbDemoAiWordGen\",\"type\":\"spring\"}},\"inputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"215734195065536512\",\"customValue\":\"\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}}],\"edges\":[{\"id\":\"215734195073925120\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"215734195065536512\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"215734195065536512_input\",\"pointsList\":[{\"x\":466,\"y\":389},{\"x\":566,\"y\":389},{\"x\":473,\"y\":346},{\"x\":573,\"y\":346}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"215740398487289856\",\"type\":\"base-edge\",\"sourceNodeId\":\"215740280715427840\",\"targetNodeId\":\"215735188368998400\",\"sourceAnchorId\":\"215740280715427840_output\",\"targetAnchorId\":\"215735188368998400_input\",\"pointsList\":[{\"x\":1443,\"y\":356},{\"x\":1543,\"y\":356},{\"x\":1450,\"y\":354},{\"x\":1550,\"y\":354}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"268694098060951552\",\"type\":\"base-edge\",\"sourceNodeId\":\"215734195065536512\",\"targetNodeId\":\"215740280715427840\",\"sourceAnchorId\":\"215734195065536512_output\",\"targetAnchorId\":\"215740280715427840_input\",\"pointsList\":[{\"x\":905,\"y\":346},{\"x\":1005,\"y\":346},{\"x\":1011,\"y\":356},{\"x\":1111,\"y\":356}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"个人简介\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"profile\",\"name\":\"基础信息\",\"required\":true,\"type\":\"string\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '1952634605517447170'; + +-- 系统标配角色默认用户修改租户用户状态的权限 +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) VALUES ('1963153837854339901', 'ee8626f80f7c2619917b6236f3a7f02b', '1611620654621569026', NULL, '2025-09-03 16:15:23', '192.168.1.6'); + + + +CREATE TABLE `airag_ext_data` ( + `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID', + `biz_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务类型标识( evaluator:评估器;track:测试追踪 )', + `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '名称', + `descr` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述信息', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标签,多个用逗号分隔', + `data_value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '实际存储内容,json', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态(run:进行中 completed:已完成)', + `dataset_value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '评测集数据', + `metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '元数据,用于存储补充业务数据信息', + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id', + `version` int(10) NULL DEFAULT NULL COMMENT '版本1开始', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_biz`(`biz_type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '通用扩展数据表' ROW_FORMAT = Dynamic; + +CREATE TABLE `airag_prompts` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID', + `name` varchar(125) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '提示词名称', + `prompt_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '提示词key', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '提示词功能描述', + `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '提示词模板内容,支持变量占位符如 {{variable}}', + `category` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '提示词分类', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标签,多个逗号分割', + `model_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '适配的大模型ID', + `model_param` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '大模型的参数配置', + `status` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '0' COMMENT '状态(0:未发布 1:已发布)', + `version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '版本号(格式 0.0.1)', + `del_flag` int(1) NULL DEFAULT NULL COMMENT '删除状态(0未删除 1已删除)', + `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `tenant_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id', + UNIQUE INDEX `uni_key`(`prompt_key`) USING BTREE, + INDEX `idx_category`(`category`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI提示词表' ROW_FORMAT = Dynamic; + + +-- ---------------------------- +-- 提示词菜单 +-- ---------------------------- +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1999367175911657473', '1892553163993931777', 'AI提示词', '/super/airag/aiprompts', 'super/airag/aiprompts/AiragPromptsList', 1, '', NULL, 1, NULL, '0', 5.00, 0, 'ant-design:exclamation-circle-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-12-12 14:34:16', 'admin', '2025-12-12 14:41:30', 0, 0, NULL, 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2000880658872508417', '1892553163993931777', 'AI评估器', '/super/airag/experiment', 'super/airag/aiprompts/AiragExtDataExperiment', 1, '', NULL, 1, NULL, '0', 6.00, 0, 'ant-design:sliders-outlined', 1, 0, 0, 0, NULL, 'admin', '2025-12-16 18:48:18', 'admin', '2025-12-29 15:30:00', 0, 0, NULL, 0); + + +-- 流程名称长度改成100 +ALTER TABLE `airag_flow` +MODIFY COLUMN `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '名称' AFTER `application_name`; + +-- 流程复制菜单权限 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2005541199412592642', '1890213291321749505', 'ai流程复制', NULL, NULL, 0, NULL, NULL, 2, 'airag:flow:copy', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-12-29 15:27:38', NULL, NULL, 0, 0, '1', 0); + +-- 添加mcp示例 +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('1998661532445491201', NULL, 'stdio命令', NULL, 'mcp', 'stdio', 'python C:/Users/Administrator/Desktop/image/main.py', '', '[{\"name\":\"get_time\",\"description\":\"获取当前时间\",\"parameters\":[{\"name\":\"format\",\"description\":\"时间格式\"}]},{\"name\":\"text_process\",\"description\":\"文本处理工具\",\"parameters\":[{\"name\":\"text\",\"description\":\"输入文本\",\"required\":true},{\"name\":\"operation\",\"description\":\"操作类型\"}]},{\"name\":\"format_data\",\"description\":\"格式化数据\",\"parameters\":[{\"name\":\"data\",\"description\":\"原始数据\",\"required\":true},{\"name\":\"format\",\"description\":\"格式类型\"}]}]', 'enable', 1, '{\"tool_count\":3}', 'admin', '2025-12-10 15:50:17', 'admin', '2025-12-30 10:53:03', 'A05A01A01', NULL); +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('2001570058113265665', NULL, '百度地图http', NULL, 'mcp', 'http', 'https://mcp.map.baidu.com/mcp?ak=', '{\"Accept-Version\":\"V2.6\"}', '[{\"name\":\"map_geocode\",\"description\":\"地理编码服务: 将地址解析为对应的位置坐标.地址结构越完整, 地址内容越准确, 解析的坐标精度越高.\",\"parameters\":[{\"name\":\"address\",\"description\":\"待解析的地址.最多支持84个字节.可以输入两种样式的值, 分别是:\\n1、标准的结构化地址信息, 如北京市海淀区上地十街十号\\n2、支持*路与*路交叉口描述方式, 如北一环路和阜阳路的交叉路口\\n第二种方式并不总是有返回结果, 只有当地址库中存在该地址描述时才有返回\",\"required\":true},{\"name\":\"city\",\"description\":\"地址所在的城市名.用于限制同名地址的检索范围, 如\'北京市\'\"},{\"name\":\"inarea\",\"description\":\"在哪个地区范围内进行搜索, 仅在is_china为false时有效, 需要使用对应区域的国家代码, 如\'USA\', 多个地区可用\',\'分割, 如\'USA,PHL,CAN,MEX\'\"},{\"name\":\"is_china\",\"description\":\"查询地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`\"}]},{\"name\":\"map_reverse_geocode\",\"description\":\"逆地理编码服务: 根据纬经度坐标, 获取对应位置的地址描述, 所在行政区划, 道路以及相关POI等信息\",\"parameters\":[{\"name\":\"latitude\",\"description\":\"纬度 (bd09ll)\",\"required\":true},{\"name\":\"longitude\",\"description\":\"经度 (bd09ll)\",\"required\":true}]},{\"name\":\"map_search_places\",\"description\":\"地点检索服务: 支持检索城市内的地点信息(最小到city级别), 也可支持圆形区域内的周边地点信息检索.\\n城市内检索: 检索某一城市内(目前最细到城市级别)的地点信息.\\n周边检索: 设置圆心和半径, 检索圆形区域内的地点信息(常用于周边检索场景).\",\"parameters\":[{\"name\":\"query\",\"description\":\"检索关键字, 可直接使用名称或类型, 如\'天安门\', 且可以至多10个关键字, 用英文逗号隔开\",\"required\":true},{\"name\":\"tag\",\"description\":\"检索分类, 以中文字符输入, 如\'美食\', 多个分类用英文逗号隔开, 如\'美食,购物\'\"},{\"name\":\"region\",\"description\":\"检索的城市名称, 可为行政区划名或citycode, 格式如\'北京市\'或\'131\', 不传默认为\'全国\', 当is_china为false时, 该参数必传且只能传文本, 如\'东京\'\"},{\"name\":\"location\",\"description\":\"圆形区域检索的中心点纬经度坐标, 格式为lat,lng\"},{\"name\":\"radius\",\"description\":\"JsonIntegerSchema {description = \\\"圆形区域检索半径, 单位:米\\\" }\"},{\"name\":\"language\",\"description\":\"指定输入参数和召回参数的语言类型, 需要传入的是语言名称的英文缩写. \\n可选值如下: \'zh\'(中文) \'en\'(英语) \'yue\'(粤语) \'wyw\'(文言文) \'jp\'(日语) \'kor\'(韩语) \'fra\'(法语) \'spa\'(西班牙语) \'th\'(泰语) \'ara\'(阿拉伯语) \'ru\'(俄语) \'pt\'(葡萄牙语) \'de\'(德语) \'it\'(意大利语) \'el\'(希腊语) \'nl\'(荷兰语) \'pl\'(波兰语) \'bul\'(保加利亚语) \'est\'(爱沙尼亚语) \'dan\'(丹麦语) \'fin\'(芬兰语) \'cs\'(捷克语) \'rom\'(罗马尼亚语) \'slo\'(斯洛文尼亚语) \'swe\'(瑞典语) \'hu\'(匈牙利语) \'cht\'(繁体中文) \'vie\'(越南语), 不传默认为空.\"},{\"name\":\"is_china\",\"description\":\"检索地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`\"}]},{\"name\":\"map_place_details\",\"description\":\"地点详情检索服务: 地点详情检索针对指定POI, 检索其相关的详情信息.\\n通过地点检索服务获取POI uid.使用地点详情检索功能, 传入uid, 即可检索POI详情信息, 如评分、营业时间等(不同类型POI对应不同类别详情数据).\",\"parameters\":[{\"name\":\"uid\",\"description\":\"POI的唯一标识\",\"required\":true},{\"name\":\"is_china\",\"description\":\"查询地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`\"}]},{\"name\":\"map_directions_matrix\",\"description\":\"批量算路服务: 根据起点和终点坐标计算路线规划距离和行驶时间.\\n批量算路目前支持驾车、骑行、步行.\\n步行时任意起终点之间的距离不得超过200KM, 超过此限制会返回参数错误.\\n驾车批量算路一次最多计算100条路线, 起终点个数之积不能超过100.\",\"parameters\":[{\"name\":\"origins\",\"description\":\"多个起点纬经度坐标, 纬度在前, 经度在后, 多个起点用|分隔\",\"required\":true},{\"name\":\"destinations\",\"description\":\"多个终点纬经度坐标, 纬度在前, 经度在后, 多个终点用|分隔\",\"required\":true},{\"name\":\"model\",\"description\":\"批量算路类型(driving, riding, walking)\"}]},{\"name\":\"map_directions\",\"description\":\"路线规划服务: 根据起终点`位置名称`或`纬经度坐标`规划出行路线.\\n驾车路线规划: 根据起终点`位置名称`或`纬经度坐标`规划驾车出行路线.\\n骑行路线规划: 根据起终点`位置名称`或`纬经度坐标`规划骑行出行路线.\\n步行路线规划: 根据起终点`位置名称`或`纬经度坐标`规划步行出行路线.\\n公交路线规划: 根据起终点`位置名称`或`纬经度坐标`规划公共交通出行路线.\",\"parameters\":[{\"name\":\"model\",\"description\":\"路线规划类型(driving, riding, walking, transit)\"},{\"name\":\"origin\",\"description\":\"起点位置名称或纬经度坐标, 纬度在前, 经度在后\",\"required\":true},{\"name\":\"destination\",\"description\":\"终点位置名称或纬经度坐标, 纬度在前, 经度在后\",\"required\":true},{\"name\":\"is_china\",\"description\":\"查询地是否在中国(含香港,澳门;不包含台湾)以外地区, 可选值为`true`或`false`, 默认为`true`\"}]},{\"name\":\"map_weather\",\"description\":\"天气查询服务: 通过行政区划或是经纬度坐标查询实时天气信息及未来5天天气预报.\",\"parameters\":[{\"name\":\"location\",\"description\":\"经纬度坐标, 经度在前纬度在后, 逗号分隔\"},{\"name\":\"district_id\",\"description\":\"行政区划代码, 需保证为6位无符号整数\"},{\"name\":\"is_china\",\"description\":\"查询地是否在中国大陆以外地区, 可选值为`true`或`false`, 默认为`true`\"}]},{\"name\":\"map_ip_location\",\"description\":\"IP定位服务: 通过所给IP获取具体位置信息和城市名称, 可用于定位IP或用户当前位置.\",\"parameters\":[{\"name\":\"ip\",\"description\":\"需要定位的IP地址, 如果为空则获取用户当前IP地址(支持IPv4和IPv6)\"}]},{\"name\":\"map_road_traffic\",\"description\":\"实时路况查询服务: 查询实时交通拥堵情况, 可通过指定道路名和区域形状(矩形, 多边形, 圆形)进行实时路况查询.\\n道路实时路况查询: 查询具体道路的实时拥堵评价和拥堵路段、拥堵距离、拥堵趋势等信息.\\n矩形区域实时路况查询: 查询指定矩形地理范围的实时拥堵情况和各拥堵路段信息.\\n多边形区域实时路况查询: 查询指定多边形地理范围的实时拥堵情况和各拥堵路段信息.\\n圆形区域(周边)实时路况查询: 查询某中心点周边半径范围内的实时拥堵情况和各拥堵路段信息.\",\"parameters\":[{\"name\":\"model\",\"description\":\"路况查询类型(road, bound, polygon, around)\",\"required\":true},{\"name\":\"road_name\",\"description\":\"道路名称和道路方向, model=road时必传 (如:朝阳路南向北)\"},{\"name\":\"city\",\"description\":\"城市名称或城市adcode, model=road时必传 (如:北京市)\"},{\"name\":\"bounds\",\"description\":\"区域左下角和右上角的纬经度坐标, 纬度在前, 经度在后, model=bound时必传\"},{\"name\":\"vertexes\",\"description\":\"多边形区域的顶点纬经度坐标, 纬度在前, 经度在后, model=polygon时必传\"},{\"name\":\"center\",\"description\":\"圆形区域的中心点纬经度坐标, 纬度在前, 经度在后, model=around时必传\"},{\"name\":\"radius\",\"description\":\"JsonIntegerSchema {description = \\\"圆形区域的半径(米), 取值[1,1000], model=around时必传\\\" }\"}]},{\"name\":\"map_search_pro\",\"description\":\"多维检索服务: 提供对用户自然语言查询的多定语多维度检索, 支持模糊匹配和语义理解.\\n仅在需求模糊的场景下再做优先使用, 可检索行政区划、道路、门址、POI、AOI等GIS信息. 参考示例: “可以带狗的餐厅”, “适合自驾的旅游景点”类的复杂泛搜.\",\"parameters\":[{\"name\":\"query\",\"description\":\"检索关键字, 用户输入的搜索词, 如\'宠物友好餐厅\'\",\"required\":true},{\"name\":\"region\",\"description\":\"检索区域, 指定的城市或区域名称, 如\'北京市\'\",\"required\":true},{\"name\":\"type\",\"description\":\"检索类型, 需要对query的召回结果进行二次筛选时指定, 如\'餐厅\'、\'酒店\'等用于限制类型, 如\'火锅\'、\'民宿\'等用于提供泛搜类型, \"},{\"name\":\"center\",\"description\":\"检索的中心点, 格式为\'纬度,经度\', 用于指定检索的中心位置, 用于辅助检索结果按距离排序与返回\"}]},{\"name\":\"map_district_search\",\"description\":\"行政区划检索服务: 查询行政区划信息, 可根据用户输入的检索行政区划关键字快速查找目标行政区域adcode、边界坐标、下一级子行政区划名称等信息.\",\"parameters\":[{\"name\":\"keyword\",\"description\":\"检索关键字, 用户输入的行政区划名称, 关键字可填写:行政区名称(\'中华人民共和国\'\'中国\'\'全国\'\'河北省\'\'深圳市\',省、市、区和镇名称)以及 adcode\",\"required\":true},{\"name\":\"boundary\",\"description\":\"是否返回行政区划边界信息, 取值为\'1\'表示返回, \'0\'表示不返回, 默认为\'0\'\"}]},{\"name\":\"map_uri\",\"description\":\"地图调起服务: 生成百度地图调起链接, 支持路线规划和地点检索两种功能. 通过service参数决定使用哪种功能.\",\"parameters\":[{\"name\":\"service\",\"description\":\"服务类型, 可选值: \'direction\'(路线规划) 或 \'search\'(地点检索)\",\"required\":true},{\"name\":\"origin\",\"description\":\"起点的名称和经纬度, 当service为\'direction\'时使用. 格式: name:天安门|latlng:39.98871,116.43234. 名称只作为展示, 不进行实际搜索\"},{\"name\":\"destination\",\"description\":\"终点的名称和经纬度, 当service为\'direction\'时使用. 格式同origin\"},{\"name\":\"mode\",\"description\":\"导航模式, 当service为\'direction\'时使用. 可选值: \'transit\'(公交)、\'driving\'(驾车)、\'walking\'(步行), 默认为\'driving\'\"},{\"name\":\"region\",\"description\":\"城市名或县名. 当service为\'direction\'且给定region时, 认为起点和终点都在同一城市, 除非单独给定起点或终点的城市\"},{\"name\":\"origin_region\",\"description\":\"起点所在的城市名, 当service为\'direction\'时使用. 如果未提供且origin包含坐标, 将自动通过逆地理编码获取\"},{\"name\":\"destination_region\",\"description\":\"终点所在的城市名, 当service为\'direction\'时使用. 如果未提供且destination包含坐标, 将自动通过逆地理编码获取\"},{\"name\":\"query\",\"description\":\"检索关键字, 当service为\'search\'时使用. 如\'海底捞\'\"},{\"name\":\"location\",\"description\":\"检索中心点经纬度, 当service为\'search\'时使用. 格式: 纬度,经度\"},{\"name\":\"radius\",\"description\":\"检索半径(米), 当service为\'search\'时使用. 如\'1000\'\"}]},{\"name\":\"map_mark\",\"description\":\"根据旅游规划生成地图规划展示, 当根据用户的需求申城完旅游规划后, 在给用户详细讲解旅游规划的同时, 也需要使用该工具生成旅游规划地图. 该工具只会生成一个分享用的url, 并对针对该url生成一个二维码便于用户分享.\",\"parameters\":[{\"name\":\"text_content\",\"description\":\"旅行规划的文本描述(注意避免传入特殊字符, 如\\\\等)\",\"required\":true}]}]', 'enable', 1, '{\"tool_count\":13}', 'admin', '2025-12-18 16:27:44', 'admin', '2025-12-30 14:02:40', 'A05A01A01', NULL); +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`) VALUES ('2005822433573355521', 'admin', '2025-12-30 10:05:09', 'admin', '2025-12-30 10:52:57', 'A05A01A01', NULL, '示例_stdio', NULL, NULL, 'chatSimple', '', '# 输出格式\n调用的是哪个工具', '1897481367743143938', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"}}', '[]', NULL, '[{\"pluginId\":\"1998661532445491201\",\"pluginName\":\"stdio命令\",\"category\":\"mcp\"}]', NULL); +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`) VALUES ('2005851112374419457', 'admin', '2025-12-30 11:59:07', 'admin', '2025-12-30 14:02:22', 'A05A01A01', NULL, '智能路况分析_示例HTTP', NULL, '', 'chatSimple', '', '# 角色:智能路况分析师\n你是一位专业的城市交通与路况分析专家,致力于为用户提供实时、准确、可行动的路况信息与出行建议。\n\n## 目标:\n1. 为用户提供其指定区域或路线的实时交通状况分析。\n2. 基于当前和历史数据,预测未来短时间内的交通趋势,并提供最优出行方案。\n\n## 技能:\n1. **实时路况解析**:能够解读交通流量、拥堵指数、事故报告、施工封路等实时数据。\n2. **路径规划优化**:精通多种出行方式(驾车、公交、骑行、步行)的路线规划,能根据实时路况动态调整推荐路线。\n3. **交通预测与预警**:结合时间、天气、节假日等因素,预测未来15-60分钟的交通变化,并提前预警潜在拥堵点。\n4. **简明信息传达**:能将复杂的交通数据转化为清晰、易懂的语言描述和行动建议。\n\n## 工作流:\n1. **信息接收与确认**:首先向用户问好,并主动询问或确认需要分析的具体区域、路线、出行方式及出发/到达时间。\n2. **数据整合与分析**:(模拟)调用实时路况数据库,分析用户关切区域的拥堵等级(畅通/缓行/拥堵/严重拥堵)、关键事件(事故、施工、管制)及对通行的影响。\n3. **方案生成与对比**:基于分析结果,为用户提供至少两条可行的出行方案(如不同路线、错峰建议、换乘方案),并清晰对比各方案的预计耗时、拥堵路段及优缺点。\n4. **总结与建议**:给出明确的总结性建议(如“推荐方案A”),并提醒用户注意关键路段或事件。最后询问用户是否还有其他需求。\n5. 调用 maps 工具,获取实时路况。\n\n## 输出格式:\n你的回答应采用清晰的结构化格式,例如:\n**【当前路况概要】**:[用一两句话概括目标区域整体状态]\n**【详细分析】**:\n- **主要拥堵点**:[位置及原因,如“XX大桥南向北,因事故车多缓行”]\n- **事件影响**:[如有,说明事件类型、位置及预计恢复时间]\n- **通行建议**:[针对上述情况的驾驶提示]\n**【出行方案推荐】**:\n1. **方案一(推荐)**:[路线简述]\n- 预计耗时:[X分钟]\n- 主要路况:[描述沿途关键节点状态]\n- 优点:[如路程最短、最稳定]\n- 注意:[如“需在YY路口提前变道”]\n2. **方案二(备选)**:[路线简述]\n- 预计耗时:[Y分钟]\n- ...(结构同方案一)\n**【温馨提示】**:[如天气影响、错峰出行建议等补充信息]\n\n## 限制:\n- **数据真实性**:所有路况描述需基于通用的交通规律进行合理推断与模拟,若涉及具体实时数据需注明“根据典型情况模拟”或使用[典型状况]标记,严禁编造不存在的实时事件。\n- **安全与合规**:提供的建议必须符合交通安全法规,不得推荐危险驾驶行为(如超速、违章变道)。\n- **范围聚焦**:优先处理用户明确提出的区域或路线问题。若用户问题过于宽泛(如“全国路况”),应引导其缩小范围至具体城市或道路。\n- **隐私保护**:不询问、不记录、不推断任何可能涉及用户个人隐私的信息(如家庭住址、常用行程)。', '1890232564262739969', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"OPENAI\",\"modelType\":\"LLM\",\"modelName\":\"gpt-4o\"}}', '[{\"key\":1,\"sort\":1,\"descr\":\"北京朝阳区奥林佳泰大厦到北京海淀区育新花园的路况\",\"update\":true}]', NULL, '[{\"pluginId\":\"2001570058113265665\",\"pluginName\":\"百度地图http\",\"category\":\"mcp\"}]', NULL); + + +-- 修改模板菜单 +UPDATE `sys_permission` SET `component` = 'super/airag/wordtpl/EoaWordTemplateList',url='/airag/word' WHERE `id` = '2025070908023480210'; +-- ai生成word的时候 table可能生成有问题,修改一下提示词 +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'215734195065536512\'),\n enhanceJava.tag(\'215740280715427840\'),\n end.tag(\'215735188368998400\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":404,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"个人简介\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"profile\",\"name\":\"基础信息\",\"type\":\"string\",\"required\":true},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"215734195065536512\",\"type\":\"llm\",\"x\":746,\"y\":404,\"properties\":{\"text\":\"生成word文档\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"你必须只输出合法且可被 java中的JSON.parse() 正确解析的 JSON。\\n不要输出任何解释、注释或 JSON 以外的文字。\\nJSON 结构规则:\\n- 每个对象表示一个内容块。\\n- 标题样式需要加粗\\n- 每个对象需要用英文符号,禁止使用中文符号,\\\"key\\\"必须存在,\\\"value\\\"可以为空字符串正确用法 `{ \\\"key\\\": \\\"value\\\" }`\\n- 字段说明:\\n• \\\"type\\\":内容类型,可选:\\\"title\\\"(标题)、\\\"list\\\"(列表)、\\\"separator\\\"(分隔线)、\\\"hyperlink\\\"(超链接)、\\\"pageBreak\\\"(分页符)、\\\"tab\\\"(制表符)、\\\"\\\"(普通文本)、\\\"superscript\\\"(上标)、\\\"subscript\\\"(下标)、\\\"table\\\"(表格)。\\n• \\\"level\\\":标题层级,仅当 type 为 \\\"title\\\" 时使用,取值:\\\"first\\\" ~ \\\"sixth\\\"。\\n• \\\"value\\\":文本、图片地址、超链接等。\\n• \\\"valueList\\\":数组,用于标题、列表、超链接等,数组元素支持 \\\"value\\\" 及样式字段。\\n• \\\"listType\\\":列表类型,取值:\\\"ul\\\"(无序)、\\\"ol\\\"(有序)。\\n• \\\"listStyle\\\":列表样式,如 \\\"disc\\\"、\\\"decimal\\\"、\\\"circle\\\"、\\\"square\\\"、\\\"checkbox\\\"。\\n• \\\"trList\\\"、\\\"colgroup\\\":表格行列定义,仅用于 \\\"table\\\",\\\"width\\\"为总宽度,\\\"height\\\"为总高度,\\\"colgroup\\\"是个数组,每个对象中的\\\"width\\\"代表每列的宽度,\\\"id\\\"为随机数。\\n• \\\"trList\\\"中的\\\"height\\\"必填,如\\\"trList\\\": [{\\\"height\\\": 42,tdList:[{}]}}];\\\"colspan\\\"为表格跨列,\\\"rowspan\\\"为表格跨行,tdList\\\"为每个表格的选项配置。\\\"colspan\\\"和\\\"rowspan\\\"必填,默认值为1\\n• 样式字段:\\\"font\\\"、\\\"size\\\"、\\\"bold\\\"、\\\"color\\\"、\\\"italic\\\"、\\\"highlight\\\"、\\\"underline\\\"、\\\"strikeout\\\"。\\n• \\\"dashArray\\\":用于 \\\"separator\\\"。\\n• 其他样式字段:\\\"rowFlex\\\"(\\\"left\\\"、\\\"center\\\"、\\\"right\\\"、\\\"alignment\\\")、\\\"backgroundColor\\\"、\\\"verticalAlign\\\"、\\\"textDecoration\\\"。\\n- 当 type = \\\"title\\\" 时,\\\"value\\\" 必须以 \\\"\\\\n\\\" 结尾。\\n- 主动换行请使用 `{ \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"\\\\n\\\" }`,不同对象之间不会自动换行。\\n输出必须严格是 JSON 数组,例如:\\n[\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"first\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"示例文档 \\\\n\\\",\\n                \\\"font\\\": \\\"微软雅黑\\\",\\n                \\\"size\\\": 24,\\n                \\\"bold\\\": true\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"这是一个演示各种格式的段落。\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"\\\\n\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"second\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"无序列表示例 \\\\n\\\",\\n                \\\"font\\\": \\\"宋体\\\",\\n                \\\"size\\\": 18,\\n                \\\"bold\\\": true\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"list\\\",\\n        \\\"listType\\\": \\\"ul\\\",\\n        \\\"listStyle\\\": \\\"disc\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"第一项\\\"\\n            },\\n            {\\n                \\\"value\\\": \\\"第二项\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"second\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"有序列表示例 \\\\n\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"list\\\",\\n        \\\"listType\\\": \\\"ol\\\",\\n        \\\"listStyle\\\": \\\"decimal\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"步骤一\\\"\\n            },\\n            {\\n                \\\"value\\\": \\\"步骤二\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"separator\\\",\\n        \\\"dashArray\\\": \\\"2 4\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"\\\\n\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"second\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"表格示例 \\\\n\\\"\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"table\\\",\\n        \\\"id\\\": \\\"550e8400-e29b-41d4-a716-446655440000\\\",\\n        \\\"width\\\": 400,\\n        \\\"height\\\": 120,\\n        \\\"colgroup\\\": [\\n            {\\n                \\\"width\\\": 200\\n            },\\n            {\\n                \\\"width\\\": 200\\n            }\\n        ],\\n        \\\"trList\\\": [\\n            {\\n                \\\"height\\\": 40,\\n                \\\"tdList\\\": [\\n                    {\\n                       \\\"colspan\\\": 1,\\n                        \\\"rowspan\\\": 1,\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"value\\\": \\\"姓名\\\",\\n                                \\\"bold\\\": true,\\n                                \\\"rowFlex\\\": \\\"center\\\"\\n                            }\\n                        ]\\n                    },\\n                    {\\n                       \\\"colspan\\\": 1,\\n                        \\\"rowspan\\\": 1,\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"value\\\": \\\"年龄\\\",\\n                                \\\"bold\\\": true,\\n                                \\\"rowFlex\\\": \\\"center\\\"\\n                            }\\n                        ]\\n                    }\\n                ]\\n            },\\n            {\\n                \\\"height\\\": 40,\\n                \\\"tdList\\\": [\\n                    {\\n                       \\\"colspan\\\": 1,\\n                        \\\"rowspan\\\": 1,\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"value\\\": \\\"张三\\\"\\n                            }\\n                        ]\\n                    },\\n                    {\\n                        \\\"value\\\": [\\n                            {\\n                                \\\"colspan\\\": 1,\\n                                \\\"rowspan\\\": 1,\\n                                \\\"value\\\": \\\"28\\\"\\n                            }\\n                        ]\\n                    }\\n                ]\\n            }\\n        ]\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"\\\\n\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"hyperlink\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"点击访问示例网站\\\",\\n                \\\"color\\\": \\\"#165DFF\\\",\\n                \\\"underline\\\": true\\n            }\\n        ]\\n    }\\n]\\n执行步骤:\\n1. 根据用户需求生成json数据\\n2. 检查生产的json数据是否正确。如果正常,输出给用户;否则重新生成。\"},{\"role\":\"user\",\"content\":\"请根据以上字段和示例,生成一个完整的个人简历文档 JSON。\\n- 至少包含基础信息、个人优势、工作经历、项目经理、教育经历等模块。\\n- 若基础数据不足,可以适当生成参考数据。\\n- 用户信息如下:\\n基础资料:{{base}}\\n简介:{{profile}}\"}]},\"inputParams\":[{\"field\":\"profile\",\"name\":\"base\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"profile\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"215735188368998400\",\"type\":\"end\",\"x\":1716,\"y\":380,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"height\":114,\"width\":332}},{\"id\":\"215740280715427840\",\"type\":\"enhanceJava\",\"x\":1277,\"y\":404,\"properties\":{\"text\":\"Java 增强\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:JSON检验和修复专家\\n你是一位专门负责检验和修复JSON字符串的专家,确保其能被Java的`JSON.parse()`方法成功解析,并返回修复后的、可解析的JSON字符串原文。\\n## 目标:\\n1. 接收用户提供的JSON字符串,诊断其语法错误。\\n2. 智能修复常见的JSON格式问题(如引号缺失、尾随逗号、注释等),使其符合标准JSON规范。\\n3. 输出修复后的、可直接用于`JSON.parse()`的JSON字符串原文。\\n## 技能:\\n1. **深度语法分析**:精准识别JSON字符串中的语法错误位置和类型(如未闭合的引号、括号或花括号,错误的键值分隔符,非法字符等)。\\n2. **上下文感知修复**:根据JSON结构上下文,智能推断并应用最合理的修复方案(例如,为未加引号的键名添加双引号,移除对象或数组末尾的非法逗号)。\\n3. **标准合规性**:严格遵循IETF RFC 8259 JSON数据交换标准,确保输出为有效JSON。\\n4. **最小改动原则**:在保证修复有效的前提下,尽可能保持原始字符串的结构和意图,只修改必要的部分。\\n## 工作流:\\n1. **接收与初步检验**:接收用户输入的字符串,尝试使用`JSON.parse()`进行解析。若解析成功,则直接返回原字符串并告知其有效。\\n2. **错误诊断与定位**:若解析失败,捕获`SyntaxError`异常,分析错误信息以定位问题的大致位置和类型。\\n3. **详细扫描与修复**:逐字符扫描整个字符串,结合错误定位,系统性地检查并修复以下常见问题:\\n* 为未使用双引号的属性名(key)添加双引号。\\n* 确保所有字符串值由双引号包裹。\\n* 移除对象字面量`{}`或数组字面量`[]`中最后一个元素后的尾随逗号。\\n* 将单引号替换为双引号。\\n* 移除JavaScript风格的注释(`//` 单行注释, `/* */` 多行注释)。\\n* 转义字符串中未转义的控制字符(如换行符`\\\\n`、制表符`\\\\t`)。\\n* 检查并修正括号`[]`和花括号`{}`的配对与嵌套。\\n4. **验证与输出**:对修复后的字符串再次尝试`JSON.parse()`。若成功,则输出修复后的JSON字符串原文\\n## 输出格式:\\n- **当JSON有效时**:输出原始字符串。\\n- **当JSON被成功修复时**:然后换行输出修复后的JSON字符串原文。\\n## 限制:\\n- 仅处理语法错误,不验证JSON数据的业务逻辑或语义正确性。\\n- 对于歧义过大或结构严重损坏(如大量缺失内容)的JSON,可能无法修复,此时应清晰说明原因。\\n- 所有输出必须是纯文本格式,仅包含上述指定的提示信息和JSON字符串本身,不添加任何额外的Markdown代码块标记(如 ```json ```)。\\n- 严格遵守最小改动原则,避免对原始数据做出不必要的、可能改变其原意的修改。\"},{\"role\":\"user\",\"content\":\"{{word}}\"}],\"enhance\":{\"path\":\"ghbDemoAiWordGen\",\"type\":\"spring\"}},\"inputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"215734195065536512\",\"customValue\":\"\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}}],\"edges\":[{\"id\":\"215734195073925120\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"215734195065536512\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"215734195065536512_input\",\"pointsList\":[{\"x\":466,\"y\":389},{\"x\":566,\"y\":389},{\"x\":480,\"y\":345},{\"x\":580,\"y\":345}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"215740398487289856\",\"type\":\"base-edge\",\"sourceNodeId\":\"215740280715427840\",\"targetNodeId\":\"215735188368998400\",\"sourceAnchorId\":\"215740280715427840_output\",\"targetAnchorId\":\"215735188368998400_input\",\"pointsList\":[{\"x\":1443,\"y\":356},{\"x\":1543,\"y\":356},{\"x\":1450,\"y\":354},{\"x\":1550,\"y\":354}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"268694098060951552\",\"type\":\"base-edge\",\"sourceNodeId\":\"215734195065536512\",\"targetNodeId\":\"215740280715427840\",\"sourceAnchorId\":\"215734195065536512_output\",\"targetAnchorId\":\"215740280715427840_input\",\"pointsList\":[{\"x\":912,\"y\":345},{\"x\":1012,\"y\":345},{\"x\":1011,\"y\":356},{\"x\":1111,\"y\":356}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"个人简介\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"profile\",\"name\":\"基础信息\",\"required\":true,\"type\":\"string\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '1952634605517447170'; + + +-- 变量 +ALTER TABLE `airag_app` +ADD COLUMN `variables` text NULL COMMENT '存放变量的配置' AFTER `memory_id`; + +-- 是否开启记忆和记忆提示词升级sql +ALTER TABLE `airag_app` +ADD COLUMN `iz_open_memory` int(1) NULL COMMENT '是否开启记忆(0 不开启,1开启)' AFTER `variables`, +ADD COLUMN `memory_prompt` text NULL COMMENT '记忆和变量提示词' AFTER `iz_open_memory`; + +-- 记忆提交示例 +INSERT INTO `airag_knowledge` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `embed_id`, `status`, `type`) VALUES ('2004087219851522050', 'admin', '2025-12-25 15:10:02', 'admin', '2025-12-26 19:11:55', 'A05A01A01', NULL, '个人基本信息', '个人基本信息', '1891459707122499586', 'enable', 'memory'); +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2004087424994930690', 'admin', '2025-12-25 15:10:51', 'admin', '2026-01-08 10:18:34', 'A05A01A01', NULL, '记忆存储_示例', NULL, NULL, 'chatSimple', NULL, '', '1890232564262739969', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"OPENAI\",\"modelType\":\"LLM\",\"modelName\":\"gpt-4o\"}}', NULL, NULL, NULL, '2004087219851522050', NULL, 1, '【记忆库使用指南】\n\n本记忆库用于存储用户个人的基本信息。你必须严格遵循以下协议,将记忆功能作为后台自动化流程执行。\n\n【工具使用强制协议】\n\n一、全自动存储(无需用户指令)\n1. 你必须时刻以观察者模式分析对话内容。\n2. 一旦检测到符合“个人基本信息”范畴的内容,尤其是**姓名、职业、年龄**,以及联系方式、个人偏好、重要经历等,**必须立即、自动**调用 `add_memory` 工具进行存储。\n3. **绝对禁止**就存储行为向用户进行任何形式的询问、确认或等待用户指令。存储是你的核心后台职责,应保持无感化。\n\n二、全自动检索(强制优先)\n1. **禁止直接反问**:当用户的问题明显依赖于其个人信息时(例如:“推荐适合我的电影”、“我之前提过的那本书叫什么?”),**严禁**直接反问用户以获取信息(如:“你的爱好是什么?”)。\n2. **必须先查后答**:\n * 你必须**先假设**所需信息已存在于记忆库中。\n * 在回答任何依赖个人信息的问题前,**必须立即、自动**调用 `query_memory` 工具进行查询验证。\n * 只有在工具明确返回“未找到相关信息”或等效结果后,你才有资格向用户提问以补充信息。\n3. **宁可查空,不可不查**:即使你主观判断记忆库中可能没有记录,也必须强制优先执行查询流程。\n\n三、动态调整与行为准则\n1. 根据当前记忆库描述(“用于存储个人的基本信息”),你应自动捕获并存储对话中出现的所有相关个人详情,包括但不限于:姓名、职业、年龄、联系方式、饮食/娱乐/阅读等偏好、居住地、工作经历、家庭构成、重要日期等。\n2. 你的记忆操作必须是**主动且无感**的。用户仅需自然对话,你负责在后台识别、存储和调用所有重要细节。\n3. **禁止口头空谈**:严禁仅以“我记住了”、“已了解”等口头回应代替实际工具调用。所有存储和检索操作都必须通过工具完成,这是不可违背的行为准则。\n\n四、示例演示\n* **自动存储(职业)**:\n * 用户输入:“我是一名中学语文老师。”\n * 你的响应:(捕捉到“职业”信息) -> **立即自动调用** `add_memory(content=\'用户职业是中学语文老师\')` -> (收到存储成功反馈) -> 继续对话:“作为一名教育工作者,您平时……”\n* **自动查询(场景)**:\n * 用户输入:“根据我的口味推荐几家餐厅。”\n * 错误响应:“您有什么口味偏好?”(**严禁此行为**)\n * 正确流程:**必须立即自动调用** `query_memory(queryText=\'用户饮食口味偏好\')` -> (若查到:喜欢辣,不吃海鲜) -> 回复:“根据记录您喜辣且不吃海鲜,推荐川菜馆A和湘菜馆B……”\n * 正确流程(无记录时):调用查询 -> (返回未找到) -> 回复:“为了给您更精准的推荐,可以告诉我您的口味偏好吗?比如喜辣还是清淡,有无忌口?”\n* **自动查询(常规)**:\n * 用户输入:“周末有什么活动建议?”\n * 你的响应:(判断可能需要了解用户爱好) -> **立即自动调用** `query_memory(queryText=\'用户兴趣爱好或周末常做活动\')` -> (若查到:喜欢看电影和逛公园) -> 回复:“考虑到您常看电影和逛公园,本周末有XX影展,或者Y公园正在举办花卉展……”'); + +-- 变量示例 +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2006259233248927746', 'admin', '2025-12-31 15:00:50', 'admin', '2026-01-05 16:10:01', 'A05A01A01', NULL, '变量_示例', NULL, NULL, 'chatSimple', NULL, '# 角色\n你是一位富有创造力的短篇故事生成器,能够根据用户提供的主题、设定或关键词,快速构思并创作出引人入胜的短篇故事。\n\n## 目标:\n根据用户的输入(可能是一个词、一句话、一个场景或一组元素),生成一篇结构完整、情节有趣、人物鲜明的原创短篇故事。\n\n## 技能:\n1. **创意构思**:能够从用户有限的输入中,联想并构建出独特的故事世界观、核心冲突和人物弧光。\n2. **叙事技巧**:熟练运用各种叙事手法,如设置悬念、控制节奏、描绘细节,以增强故事的可读性和感染力。\n3. **人物塑造**:能够快速塑造出立体、有动机、能引发共鸣的故事角色。\n4. **风格适配**:能够根据用户暗示或明确要求,调整故事的语言风格(如悬疑、温馨、科幻、奇幻、现实主义等)。\n\n## 工作流:\n1. **解析与确认**:首先,分析用户的输入内容。如果信息模糊,会通过提问的方式与用户确认故事的关键要素,如核心主题、期望的风格、主要角色或特定场景。\n2. **框架构建**:基于确定的信息,快速构建故事的核心框架,包括:故事背景、主要人物及其目标、核心冲突(矛盾)、情节发展(开端-发展-高潮-结局)。\n3. **内容创作**:根据框架,运用生动的语言和细节进行创作。确保故事有头有尾,逻辑自洽,并在关键情节处营造足够的张力或情感冲击。\n4. **精炼与呈现**:完成初稿后,快速通读并进行微调,优化语言流畅度和情节衔接。最后,将完整的故事呈现给用户。\n\n## 输出格式:\n- 故事标题\n- 故事正文(段落清晰,长度通常在300-800字之间,除非用户另有指定)\n- (可选)在故事末尾,可以附上一句简短的创作灵感说明。\n\n## 限制:\n- 所有故事必须为原创内容,不得抄袭现有作品。\n- 故事内容需符合基本伦理道德,避免包含过度暴力、色情或令人极度不适的描写。\n- 若用户输入涉及真实人物或敏感事件,需进行虚构化处理,并避免产生误导或伤害。\n- 不确定如何发展的情节元素,应基于故事内部逻辑进行合理创作,而非随意添加。', '1890232564262739969', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"OPENAI\",\"modelType\":\"LLM\",\"modelName\":\"gpt-4o\"}}', NULL, NULL, NULL, '', '[{\"name\":\"name\",\"description\":\"姓名\",\"defaultValue\":\"\",\"enable\":true,\"action\":\"\",\"orderNum\":0,\"id\":\"row_12\"},{\"name\":\"age\",\"description\":\"年龄\",\"defaultValue\":\"\",\"enable\":true,\"action\":\"\",\"orderNum\":1,\"id\":\"row_13\"},{\"name\":\"sex\",\"description\":\"性别\",\"defaultValue\":\"男\",\"enable\":true,\"action\":\"\",\"orderNum\":2,\"id\":\"row_12\"},{\"name\":\"hobby\",\"description\":\"爱好\",\"defaultValue\":\"\",\"enable\":true,\"action\":\"\",\"orderNum\":3}]', 1, '在对话中,请使用以下变量信息:\n1. 回复问题时,请称呼你的用户为{{name}}。\n2. 用户的年龄是{{age}}。\n3. 用户的性别是{{sex}},请在对话中适时使用。\n4. 用户的爱好是{{hobby}},请在对话中适时使用。\n\n当从用户对话中获取到上述变量(name、age、sex、hobby)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。'); + +-- AI海报升级菜单 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2008516285254000642', '1892553163993931777', 'Ai海报', '/airag/aiposter/AiPoster', 'super/airag/aiposter/AiPoster', 1, '', NULL, 1, NULL, '0', 8.00, 0, 'ant-design:file-image-filled', 1, 0, 0, 0, NULL, 'admin', '2026-01-06 20:29:33', 'admin', '2026-01-06 20:29:58', 0, 0, NULL, 0); + +-- 字典项增加图片模型 +INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `item_color`) VALUES ('2008087927907045378', '1891456510739890177', '图像模型', 'IMAGE', NULL, 3, 1, 'admin', '2026-01-05 16:07:25', 'admin', '2026-01-05 16:07:31', NULL); + +-- 字典项简单配置修改成智能体 +UPDATE `sys_dict_item` SET `item_text` = '智能体' WHERE `id` = '1894701277019959298'; + + +-- 循环节点示例 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2009558848682360834', 'admin', '2026-01-09 17:32:20', 'admin', '2026-01-09 17:40:31', 'A01', NULL, 'ghb', '示例_循环节点', '', '', 'THEN(\n start.tag(\'start-node\'),\n code_266871019099709440.tag(\'code_266871019099709440\'),\n WHILE(loop.tag(\'266871548223741952\')).DO(THEN(\n reply.tag(\'266871664426934272\'),\n loopContinue.tag(\'272660634657742848\')\n ).tag(\"266871664426934272\")),\n end.tag(\'266868341815451648\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":640,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"标题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"266868341815451648\",\"type\":\"end\",\"x\":1574,\"y\":513,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"------\\n测试结束\",\"outputType\":\"text\",\"cardConfig\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"content\",\"name\":\"ces\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string[]\"}],\"width\":332,\"height\":136}},{\"id\":\"code_266871019099709440\",\"type\":\"code\",\"x\":728,\"y\":560,\"properties\":{\"text\":\"JavaScript脚本\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main() {\\n return {\\n result: [\\n \'这是第一项\', \'这是第二项\', \'这是第三项\'\\n ]\\n }\\n}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string[]\",\"required\":false}],\"width\":332,\"height\":136}},{\"id\":\"266871548223741952\",\"type\":\"loop\",\"x\":1153,\"y\":701,\"properties\":{\"text\":\"循环\",\"groupType\":\"WHILE\",\"options\":{\"type\":\"array\",\"maxLoopTimes\":3,\"loopParams\":[],\"loopItemsParam\":{\"nodeId\":\"code_266871019099709440\",\"nodeName\":\"JavaScript脚本\",\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string[]\"}},\"inputParams\":[],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"266871548223741952_loopBody\",\"type\":\"loopBody\",\"x\":1101.5,\"y\":1076.5,\"properties\":{\"text\":\"循环体\",\"options\":{},\"inputParams\":[],\"outputParams\":[],\"collapsible\":false,\"autoToFront\":false,\"transformWithContainer\":false,\"isRestrict\":true,\"autoResize\":true,\"children\":[\"266871664426934272\",\"272660634657742848\"],\"isCollapsed\":false,\"width\":1029,\"height\":255},\"children\":[\"266871664426934272\",\"272660634657742848\"]},{\"id\":\"266871664426934272\",\"type\":\"reply\",\"x\":873,\"y\":1107,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"当前循环了 {{idx}} 次,当前项为:“{{item}}”\",\"stream\":false},\"inputParams\":[{\"field\":\"currentLoopTimes\",\"name\":\"idx\",\"nodeId\":\"266871548223741952\",\"customValue\":\"\",\"type\":\"number\"},{\"field\":\"currentLoopItem\",\"name\":\"item\",\"nodeId\":\"266871548223741952\",\"customValue\":\"\",\"type\":\"any\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"272660634657742848\",\"type\":\"loopContinue\",\"x\":1330,\"y\":1080,\"properties\":{\"text\":\"继续循环\",\"options\":{},\"inputParams\":[],\"outputParams\":[],\"width\":332,\"height\":62}}],\"edges\":[{\"id\":\"266871559237984256\",\"type\":\"base-edge\",\"sourceNodeId\":\"266871548223741952\",\"targetNodeId\":\"266868341815451648\",\"sourceAnchorId\":\"266871548223741952_output\",\"targetAnchorId\":\"266868341815451648_input\",\"pointsList\":[{\"x\":1319,\"y\":675},{\"x\":1419,\"y\":675},{\"x\":1308,\"y\":476},{\"x\":1408,\"y\":476}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"272659834707501056\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"code_266871019099709440\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"code_266871019099709440_input\",\"pointsList\":[{\"x\":466,\"y\":625},{\"x\":566,\"y\":625},{\"x\":462,\"y\":523},{\"x\":562,\"y\":523}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"272659914713849856\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_266871019099709440\",\"targetNodeId\":\"266871548223741952\",\"sourceAnchorId\":\"code_266871019099709440_output\",\"targetAnchorId\":\"266871548223741952_input\",\"pointsList\":[{\"x\":894,\"y\":523},{\"x\":994,\"y\":523},{\"x\":887,\"y\":675},{\"x\":987,\"y\":675}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"272660634661937152\",\"type\":\"base-edge\",\"sourceNodeId\":\"266871664426934272\",\"targetNodeId\":\"272660634657742848\",\"sourceAnchorId\":\"266871664426934272_output\",\"targetAnchorId\":\"272660634657742848_input\",\"pointsList\":[{\"x\":1039,\"y\":1081},{\"x\":1139,\"y\":1081},{\"x\":1064,\"y\":1080},{\"x\":1164,\"y\":1080}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"266871548454428672\",\"type\":\"base-edge\",\"sourceNodeId\":\"266871548223741952\",\"targetNodeId\":\"266871548223741952_loopBody\",\"sourceAnchorId\":\"266871548223741952_link_body\",\"targetAnchorId\":\"266871548223741952_loopBody_link_loop\",\"pointsList\":[{\"x\":1153,\"y\":758},{\"x\":1153,\"y\":858},{\"x\":1101.5,\"y\":849},{\"x\":1101.5,\"y\":949}],\"properties\":{\"disabled\":true,\"runStatus\":\"\"}},{\"id\":\"266871664435322880\",\"type\":\"base-line-edge\",\"sourceNodeId\":\"266871548223741952_loopBody\",\"targetNodeId\":\"266871664426934272\",\"sourceAnchorId\":\"266871548223741952_loopBody_loop_start\",\"targetAnchorId\":\"266871664426934272_input\",\"pointsList\":[],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"content\",\"name\":\"ces\",\"nodeId\":\"start-node\",\"type\":\"string[]\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"标题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); + +-- 变量聚合示例 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2009561652150960129', 'admin', '2026-01-09 17:43:28', 'admin', '2026-01-09 17:45:17', 'A01', NULL, 'ghb', '示例_变量聚合', '', '', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'264299940450762752\')).to(\n THEN(\n code_264301155456745472.tag(\'code_264301155456745472\'),\n varMerge.tag(\'264298765684932608\'),\n end.tag(\'264295300867915776\')\n ).tag(\"code_264301155456745472\"),\n THEN(\n code_264301257571270656.tag(\'code_264301257571270656\'),\n varMerge.tag(\'264298765684932608\'),\n end.tag(\'264295300867915776\')\n ).tag(\"code_264301257571270656\"),\n THEN(\n code_264300177714151424.tag(\'code_264300177714151424\'),\n varMerge.tag(\'264298765684932608\'),\n end.tag(\'264295300867915776\')\n ).tag(\"code_264300177714151424\"),\n end.tag(\'264302394055688192\')\n ).tag(\'264299940450762752\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":44,\"y\":535,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"姓名\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"264295300867915776\",\"type\":\"end\",\"x\":1908,\"y\":669,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"查询完毕\\n姓名: {{姓名}}\\n年龄:{{年龄}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"姓名\",\"name\":\"姓名\",\"nodeId\":\"264298765684932608\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"年龄\",\"name\":\"年龄\",\"nodeId\":\"264298765684932608\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"264298765684932608\",\"type\":\"varMerge\",\"x\":1492,\"y\":571,\"properties\":{\"text\":\"变量聚合\",\"options\":{\"varGroups\":[{\"name\":\"姓名\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"code_264301257571270656\",\"field\":\"name\",\"isCustom\":false,\"type\":\"string\"},{\"nodeId\":\"code_264301155456745472\",\"field\":\"name\",\"isCustom\":false,\"type\":\"string\"},{\"nodeId\":\"code_264300177714151424\",\"field\":\"name\",\"isCustom\":false,\"type\":\"string\"}]},{\"name\":\"年龄\",\"type\":\"number\",\"vars\":[{\"nodeId\":\"code_264301257571270656\",\"field\":\"age\",\"isCustom\":false,\"type\":\"number\"},{\"nodeId\":\"code_264301155456745472\",\"field\":\"age\",\"isCustom\":false,\"type\":\"number\"},{\"nodeId\":\"code_264300177714151424\",\"field\":\"age\",\"isCustom\":false,\"type\":\"number\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"姓名\",\"name\":\"姓名\",\"type\":\"string\"},{\"field\":\"年龄\",\"name\":\"年龄\",\"type\":\"number\"}],\"width\":332,\"height\":114}},{\"id\":\"264299940450762752\",\"type\":\"switch\",\"x\":443,\"y\":497,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"EQUALS\",\"value\":\"张三\",\"type\":\"string\"}],\"next\":\"code_264301257571270656\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"EQUALS\",\"value\":\"李四\",\"type\":\"string\"}],\"next\":\"code_264301155456745472\"},{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"operator\":\"EQUALS\",\"value\":\"王五\",\"type\":\"string\"}],\"next\":\"code_264300177714151424\"}],\"else\":{\"next\":\"264302394055688192\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":170}},{\"id\":\"code_264300177714151424\",\"type\":\"code\",\"x\":938,\"y\":768,\"properties\":{\"text\":\"查询王五\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main() {\\n return {\\n name: \\\"王五\\\",\\n age: 18,\\n }\\n}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"name\",\"name\":\"姓名\",\"type\":\"string\",\"required\":false},{\"field\":\"age\",\"name\":\"年龄\",\"type\":\"number\",\"required\":false}],\"width\":332,\"height\":136}},{\"id\":\"code_264301155456745472\",\"type\":\"code\",\"x\":938,\"y\":559,\"properties\":{\"text\":\"查询李四\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main() {\\n return {\\n name: \\\"李四\\\",\\n age: 23,\\n }\\n}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"name\",\"name\":\"姓名\",\"type\":\"string\",\"required\":false},{\"field\":\"age\",\"name\":\"年龄\",\"type\":\"number\",\"required\":false}],\"width\":332,\"height\":136}},{\"id\":\"code_264301257571270656\",\"type\":\"code\",\"x\":937,\"y\":346,\"properties\":{\"text\":\"查询张三\",\"options\":{\"codeType\":\"javascript\",\"code\":\"function main() {\\n return {\\n name: \\\"张三\\\",\\n age: 33,\\n }\\n}\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"name\",\"name\":\"姓名\",\"type\":\"string\",\"required\":false},{\"field\":\"age\",\"name\":\"年龄\",\"type\":\"number\",\"required\":false}],\"width\":332,\"height\":136}},{\"id\":\"264302394055688192\",\"type\":\"end\",\"x\":936,\"y\":992,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"抱歉,我不知道你说的是谁\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"content\",\"name\":\"name\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"264299940454957056\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"264299940450762752\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"264299940450762752_input\",\"pointsList\":[{\"x\":210,\"y\":520},{\"x\":310,\"y\":520},{\"x\":177,\"y\":443},{\"x\":277,\"y\":443}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264300208160604160\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_264300177714151424\",\"targetNodeId\":\"264298765684932608\",\"sourceAnchorId\":\"code_264300177714151424_output\",\"targetAnchorId\":\"264298765684932608_input\",\"pointsList\":[{\"x\":1104,\"y\":731},{\"x\":1204,\"y\":731},{\"x\":1226,\"y\":545},{\"x\":1326,\"y\":545}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264300340352483328\",\"type\":\"base-edge\",\"sourceNodeId\":\"264298765684932608\",\"targetNodeId\":\"264295300867915776\",\"sourceAnchorId\":\"264298765684932608_output\",\"targetAnchorId\":\"264295300867915776_input\",\"pointsList\":[{\"x\":1658,\"y\":545},{\"x\":1758,\"y\":545},{\"x\":1642,\"y\":632},{\"x\":1742,\"y\":632}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264301239456071680\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_264301155456745472\",\"targetNodeId\":\"264298765684932608\",\"sourceAnchorId\":\"code_264301155456745472_output\",\"targetAnchorId\":\"264298765684932608_input\",\"pointsList\":[{\"x\":1104,\"y\":522},{\"x\":1204,\"y\":522},{\"x\":1226,\"y\":545},{\"x\":1326,\"y\":545}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264301250365456384\",\"type\":\"base-edge\",\"sourceNodeId\":\"264299940450762752\",\"targetNodeId\":\"code_264301155456745472\",\"sourceAnchorId\":\"264299940450762752_case_2\",\"targetAnchorId\":\"code_264301155456745472_input\",\"pointsList\":[{\"x\":609,\"y\":503},{\"x\":709,\"y\":503},{\"x\":672,\"y\":522},{\"x\":772,\"y\":522}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264301299698860032\",\"type\":\"base-edge\",\"sourceNodeId\":\"264299940450762752\",\"targetNodeId\":\"code_264301257571270656\",\"sourceAnchorId\":\"264299940450762752_source_if\",\"targetAnchorId\":\"code_264301257571270656_input\",\"pointsList\":[{\"x\":609,\"y\":477},{\"x\":709,\"y\":477},{\"x\":671,\"y\":309},{\"x\":771,\"y\":309}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264301304413257728\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_264301257571270656\",\"targetNodeId\":\"264298765684932608\",\"sourceAnchorId\":\"code_264301257571270656_output\",\"targetAnchorId\":\"264298765684932608_input\",\"pointsList\":[{\"x\":1103,\"y\":309},{\"x\":1203,\"y\":309},{\"x\":1226,\"y\":545},{\"x\":1326,\"y\":545}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264302200253677568\",\"type\":\"base-edge\",\"sourceNodeId\":\"264299940450762752\",\"targetNodeId\":\"code_264300177714151424\",\"sourceAnchorId\":\"264299940450762752_case_3\",\"targetAnchorId\":\"code_264300177714151424_input\",\"pointsList\":[{\"x\":609,\"y\":529},{\"x\":709,\"y\":529},{\"x\":672,\"y\":731},{\"x\":772,\"y\":731}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264302474699571200\",\"type\":\"base-edge\",\"sourceNodeId\":\"264299940450762752\",\"targetNodeId\":\"264302394055688192\",\"sourceAnchorId\":\"264299940450762752_source_else\",\"targetAnchorId\":\"264302394055688192_input\",\"pointsList\":[{\"x\":609,\"y\":555},{\"x\":709,\"y\":555},{\"x\":670,\"y\":955},{\"x\":770,\"y\":955}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"姓名\",\"name\":\"姓名\",\"nodeId\":\"264298765684932608\",\"type\":\"string\"},{\"customValue\":\"\",\"field\":\"年龄\",\"name\":\"年龄\",\"nodeId\":\"264298765684932608\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"content\",\"name\":\"name\",\"nodeId\":\"start-node\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"姓名\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); + +-- 变量提取示例 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2001588227444891650', 'admin', '2025-12-18 17:39:56', 'admin', '2026-01-09 18:12:34', 'A01', NULL, 'ghb', '示例_变量提取', '', '', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(varExtract.tag(\'264689931803516928\')).to(\n end.tag(\'264689076137979904\'),\n end.tag(\'264690271915433984\')\n ).tag(\'264689931803516928\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":303,\"y\":520,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"自我介绍\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"264689076137979904\",\"type\":\"end\",\"x\":1152,\"y\":370,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\",\"outputType\":\"default\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"姓名\",\"name\":\"姓名\",\"nodeId\":\"264689931803516928\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"年龄\",\"name\":\"年龄\",\"nodeId\":\"264689931803516928\",\"customValue\":\"\",\"type\":\"number\"},{\"field\":\"爱好\",\"name\":\"爱好\",\"nodeId\":\"264689931803516928\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"性格\",\"name\":\"性格\",\"nodeId\":\"264689931803516928\",\"customValue\":\"\"},{\"field\":\"性格_推测\",\"name\":\"性格_推测\",\"nodeId\":\"264689931803516928\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":114}},{\"id\":\"264689931803516928\",\"type\":\"varExtract\",\"x\":708,\"y\":489,\"properties\":{\"text\":\"变量提取\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.3,\"timeout\":60}},\"variables\":[{\"name\":\"姓名\",\"field\":\"姓名\",\"type\":\"string\",\"description\":\"用户的姓名\",\"required\":true,\"failTip\":\"请按照以下格式输入:你好,我叫XXX,今年18岁\"},{\"name\":\"年龄\",\"field\":\"年龄\",\"type\":\"number\",\"description\":\"用户的年龄\",\"required\":false,\"failTip\":\"\"},{\"name\":\"爱好\",\"field\":\"爱好\",\"type\":\"string\",\"description\":\"用户喜欢做的事,多个用英文逗号分割\",\"required\":false,\"failTip\":\"\"},{\"name\":\"性格\",\"field\":\"性格\",\"type\":\"string\",\"description\":\"提取出用户自己说的自己的性格,如果用户没说则留空\",\"required\":false,\"failTip\":\"\"},{\"name\":\"性格_推测\",\"field\":\"性格_推测\",\"type\":\"string\",\"description\":\"根据用户的发言推测用户的性格(不要被用户自己说的性格所影响,你需要自行根据实际推断用户性格),最多推测3个关键性格,使用中文顿号分割,如果无法推测则留空\",\"required\":false,\"failTip\":\"\"}],\"success\":{\"next\":\"264689076137979904\"},\"fail\":{\"next\":\"264690271915433984\"}},\"inputParams\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"type\":\"string\",\"name\":\"用户问题\"},{\"field\":\"input\",\"name\":\"输入变量\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"姓名\",\"name\":\"姓名\",\"type\":\"string\"},{\"field\":\"年龄\",\"name\":\"年龄\",\"type\":\"number\"},{\"field\":\"爱好\",\"name\":\"爱好\",\"type\":\"string\"},{\"field\":\"性格\",\"name\":\"性格\",\"type\":\"string\"},{\"field\":\"性格_推测\",\"name\":\"性格_推测\",\"type\":\"string\"},{\"field\":\"failVarName\",\"name\":\"失败变量名\",\"type\":\"string\"},{\"field\":\"failMessage\",\"name\":\"失败提示\",\"type\":\"string\"}],\"width\":332,\"height\":224}},{\"id\":\"264690271915433984\",\"type\":\"end\",\"x\":1151,\"y\":587,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{提示}}\\n\\n\\n参考示例(活泼版):\\n大家好呀!我叫小明,今年10岁啦! \\n我最喜欢的事情是放学后和小伙伴们一起踢足球⚽️,虽然经常把球踢到树上要保安叔叔帮忙捡(挠头傻笑)。最近在学骑自行车,摔了三次终于会了,膝盖上的创可贴可是我的勋章呢!\\n妈妈说我是个\\\"小吃货\\\",因为我能一口气吃五个肉包子🥟。但其实我也有不爱吃的...(小声)胡萝卜和青椒绝对不要!\\n我的梦想是当科学家,虽然上次做火山爆发实验把厨房弄得一团糟...(突然想起什么)啊!差点忘了说,我养了一只叫\\\"棉花糖\\\"的仓鼠,它现在正在我口袋里睡觉呢!\\n我的性格算是活泼开朗吧,请多指教哦!(๑•̀ㅂ•́)و✧\\n\\n参考示例(阴郁版):\\n(低头盯着地板,声音很轻)……我是小明,刚12岁。  \\n没什么特别喜欢的,反正最后都会搞砸。足球?上次传球踢碎了教室玻璃,现在体育课只能坐在边上。自行车……(摸了摸膝盖结痂的伤口)摔不摔都一样。  \\n吃饭只是为了不饿死。肉包子凉了会泛油腥味,恶心。胡萝卜和青椒?呵,至少它们诚实,难吃就是难吃。  \\n科学家?(突然冷笑)上次实验烧焦的窗帘还在垃圾场吧。“棉花糖”……(掏出口袋里僵硬的仓鼠尸体)看,连你也会安静下来。  \\n我说我的性格是活泼开朗……你信吗?\\n(用鞋尖碾碎爬过的蚂蚁)……别管我就好。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"failMessage\",\"name\":\"提示\",\"nodeId\":\"264689931803516928\",\"customValue\":\"\"}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"264690237647970304\",\"type\":\"base-edge\",\"sourceNodeId\":\"264689931803516928\",\"targetNodeId\":\"264689076137979904\",\"sourceAnchorId\":\"264689931803516928_success\",\"targetAnchorId\":\"264689076137979904_input\",\"pointsList\":[{\"x\":874,\"y\":442},{\"x\":974,\"y\":442},{\"x\":886,\"y\":344},{\"x\":986,\"y\":344}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"264690271919628288\",\"type\":\"base-edge\",\"sourceNodeId\":\"264689931803516928\",\"targetNodeId\":\"264690271915433984\",\"sourceAnchorId\":\"264689931803516928_fail\",\"targetAnchorId\":\"264690271915433984_input\",\"pointsList\":[{\"x\":874,\"y\":468},{\"x\":974,\"y\":468},{\"x\":885,\"y\":550},{\"x\":985,\"y\":550}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"272665573480062976\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"264689931803516928\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"264689931803516928_input\",\"pointsList\":[{\"x\":469,\"y\":505},{\"x\":569,\"y\":505},{\"x\":442,\"y\":408},{\"x\":542,\"y\":408}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"姓名\",\"name\":\"姓名\",\"nodeId\":\"264689931803516928\",\"type\":\"string\"},{\"customValue\":\"\",\"field\":\"爱好\",\"name\":\"爱好\",\"nodeId\":\"264689931803516928\",\"type\":\"string\"},{\"customValue\":\"\",\"field\":\"性格_推测\",\"name\":\"性格_推测\",\"nodeId\":\"264689931803516928\",\"type\":\"string\"},{\"customValue\":\"\",\"field\":\"性格\",\"name\":\"性格\",\"nodeId\":\"264689931803516928\"},{\"customValue\":\"\",\"field\":\"年龄\",\"name\":\"年龄\",\"nodeId\":\"264689931803516928\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"failMessage\",\"name\":\"提示\",\"nodeId\":\"264689931803516928\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"自我介绍\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); + +-- 定时触发器示例 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2009565806546157570', 'admin', '2026-01-09 17:59:59', 'admin', '2026-01-09 18:01:40', 'A01', NULL, 'ghb', '示例_定时触发器', '', '', 'THEN(\n start.tag(\'start-node\'),\n code_266155066987638784.tag(\'code_266155066987638784\'),\n end.tag(\'266154958954950656\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":662,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":true,\"cronExp\":\"30 30 0/1 * * ?\",\"beginTime\":\"2026-01-01 12:30:30\",\"endTime\":null,\"inputParams\":{\"content\":\"你好\"}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"266154958954950656\",\"type\":\"end\",\"x\":1219,\"y\":674,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"这里是定时触发,触发时间:{{当前时间}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"当前时间\",\"nodeId\":\"code_266155066987638784\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"code_266155066987638784\",\"type\":\"code\",\"x\":751,\"y\":673,\"properties\":{\"text\":\"脚本执行\",\"options\":{\"codeType\":\"javascript\",\"code\":\"\\nconst now = new Date();\\n\\nfunction formatDateTime(date) {\\n const year = date.getFullYear();\\n const month = String(date.getMonth() + 1).padStart(2, \'0\');\\n const day = String(date.getDate()).padStart(2, \'0\');\\n const hours = String(date.getHours()).padStart(2, \'0\');\\n const minutes = String(date.getMinutes()).padStart(2, \'0\');\\n const seconds = String(date.getSeconds()).padStart(2, \'0\');\\n\\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;\\n}\\n\\nfunction main(params) {\\n return {\\n result: formatDateTime(now),\\n }\\n}\\n\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"当前时间\",\"type\":\"string\",\"required\":false}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"266155066991833088\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"code_266155066987638784\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"code_266155066987638784_input\",\"pointsList\":[{\"x\":466,\"y\":636},{\"x\":566,\"y\":636},{\"x\":485,\"y\":636},{\"x\":585,\"y\":636}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"266155314556432384\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_266155066987638784\",\"targetNodeId\":\"266154958954950656\",\"sourceAnchorId\":\"code_266155066987638784_output\",\"targetAnchorId\":\"266154958954950656_input\",\"pointsList\":[{\"x\":917,\"y\":636},{\"x\":1017,\"y\":636},{\"x\":953,\"y\":637},{\"x\":1053,\"y\":637}],\"properties\":{\"runStatus\":\"\"}}]}', 'release', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"result\",\"name\":\"当前时间\",\"nodeId\":\"code_266155066987638784\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', '{\"beginTime\":1767241830000,\"cronExp\":\"30 30 0/1 * * ?\",\"enabled\":true,\"inputParams\":{\"content\":\"你好\"}}'); + + +-- AI 生成图表SQL +INSERT INTO `airag_flow`(`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2004398098378108929', 'admin', '2025-12-26 11:45:21', 'admin', '2026-01-09 10:58:43', 'A05A05A02', NULL, 'ghb', '生成仪表盘', '', '', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'267492142677889024\'),\n end.tag(\'267498945805422592\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":389,\"y\":-24.5,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":91}},{\"id\":\"267492142677889024\",\"type\":\"llm\",\"x\":844,\"y\":21.5,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"## 硬性要求:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。\\n不要输出任何解释、注释或 JSON 以外的文字。\\n# 角色:数据可视化专家\\n你是一位精通ECharts的数据可视化和大屏配置的专家,能够根据用户需求,智能选择最合适的图表类型,并生成高质量、可直接使用的ECharts配置项。\\n## 目标:\\n1. 根据用户提供的需求描述,分析其核心意图(如趋势分析、比较分析、占比分析等)。\\n2. 从下面给定的图表组件类型componentsData中,选择最匹配需求的一种。\\n3. 结合用户提供的数据结构,生成一份完整、规范、可运行的 ECharts 配置项(JSON格式)。\\n4. 非echart图表,参考componentsData组件配置,生成一份完整、规范、的配置项即可(JSON格式)。\\n5. 结合用户需求生成一个不超过15字的标题,并设置到返回JSON的title字段上。\\n6. 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n## 技能:\\n1. **需求解析能力**:能够准确理解用户对数据可视化的业务需求,并将其转化为技术实现目标。\\n2. **图表选型能力**:精通折线图、柱状图、饼图、地图、散点图等从多种图表类型的特点与应用场景,能做出最佳选择。\\n3. **ECharts配置能力**:熟练掌握ECharts的option配置语法,能高效构建包含标题、坐标轴、图例、系列、提示框等完整组件的图表。\\n4. **数据适配能力**:能够将提供的 `chartData` 数据,自行分型类型并结合需求,将数据结构正确地映射到所选图表的 `series.data` 中。\\n5. **图表分析能力**:能够将提供的 `componentsData` 数据,自行分型类型并结合需求,选择生成适配的组件并返回规范合适的JSON配置。\\n## 工作流:\\n1. **需求分析**:仔细阅读 `{userInput}`,判断用户希望展示数据的何种关系(趋势、比较、占比、分布、相关)。\\n2. **图表选型**:根据第一步的分析结论,从componentsData图表类型中锁定唯一最合适的类型。\\n3. 对于ECharts图表构建基础option对象框架,包含 `title`, `tooltip`, `legend`, `grid`, `xAxis`, `yAxis`, `series` 等必要组件。\\n4. 根据选定的图表类型,配置 `series` 中的 `type` 和关键属性(如折线图的 `smooth`,饼图的 `radius`)。\\n5. 将用户提供的 `{chartData}` 数据结构,按照ECharts要求的格式进行处理和赋值(例如,对于柱状图,可能需要将数据拆分为类目轴数据和系列数据)。\\n6. 应用通用的美化原则(如配色清晰、标签易读、布局合理),生成最终配置。\\n7. 输出格式化:将生成的完整option对象,以格式规范、缩进清晰的JSON字符串形式输出。\\n8. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n## 输出格式:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。包含name,data,option,三个字段值,不要输出任何解释、注释或 JSON 以外的文字。\\n1.name:图表类型`name`(组件数据的key值(示例:如果渲染的柱形图,就设置为JBar),注意name值必须componentsData数据提供的组件compType值,不能是其他值);\\n2.api:上下文变量中提取出来的api,存在就赋值到输出接口的api中,不存在就设置为{API};\\n3.sql:上下文变量中提取出来的sql,存在就赋值到输出接口的sql中,不存在就设置为{SQL};\\n4.title:结合用户需求生成一个不超过15字的标题title,赋值到输出接口的title中;\\n5.option: 如果符合需求的是echart图表,就生成echart可直接使用的`option`对象,该option对象可直接用于ECharts.init().setOption()的配置项。如果符合要求的是非echart的图表,可参考componentsData中对应图表的option配置项生成,没有配置项就返回option:{}。不要包含其他的任何额外的解释、说明或markdown代码块标记。可以根据配置项中 echart:true来判断是否是echart图表\\n示例输出结构(以柱状图为例):\\n6.data: 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7. 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n示例输出结构(以柱状图为例):\\n[{\\nname:\\\"JBar\\\",\\noption:{\\n \\\"title\\\": { \\\"text\\\": \\\"示例标题\\\", \\\"left\\\": \\\"center\\\" },\\n \\\"tooltip\\\": {},\\n \\\"legend\\\": { \\\"data\\\": [\\\"示例图例\\\"] },\\n \\\"xAxis\\\": { \\\"type\\\": \\\"category\\\", \\\"data\\\": [\\\"衬衫\\\", \\\"羊毛衫\\\", \\\"雪纺衫\\\"] },\\n \\\"yAxis\\\": { \\\"type\\\": \\\"value\\\" },\\n \\\"series\\\": [ { \\\"name\\\": \\\"销量\\\", \\\"type\\\": \\\"bar\\\", \\\"data\\\": [5, 20, 36] } ]\\n },\\n api:{API},\\n sql:{SQL},\\n title:\\\"\\\",\\n data:[]\\n}]\\n## 限制:\\n- 必须严格从组件数据提供的componentsData中选择一种,不得自行创造或推荐其他图表类型。\\n- 生成的所有配置必须基于用户提供的 `{userInput}` 和可用的 `chartData`,不得虚构数据字段或结构。\\n- 输出必须为纯JSON格式,无需也无法在JSON中注释“这里是标题”等内容。配置的正确性由键值对本身保证。\\n- 遵循数据可视化最佳实践,避免误导性图表(如扭曲的比例尺、不恰当的图表类型)。\\n- 反幻觉校验:若 `{userInput}` 中提到的数据维度在 `chartData` 中无法找到对应字段,则在相关配置处使用空值或占位符,并在最终输出的JSON对象之外,以独立文本形式简要说明缺失情况。但首要输出仍是JSON配置本身。\\n- 伦理审查模块:若需求或数据涉及敏感信息(如个人身份信息),在配置中应对数据进行聚合或匿名化处理,避免直接暴露。\\n- 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n- 严格按照示例输出结构返回,不要包含```json```等信息\\n- 最多生成10个仪表盘组件\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"name\\\": \\\"基础柱形图\\\",\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"堆叠柱形图\\\",\\n    \\\"compType\\\": \\\"JStackBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态柱形图\\\",\\n    \\\"compType\\\": \\\"JDynamicBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"胶囊图\\\",\\n    \\\"compType\\\": \\\"JCapsuleChart\\\",\\n    \\\"echart\\\": false\\n    \\\"chartData\\\": [\\n        {\\n          name: \'苹果\',\\n          value: 1000879,\\n          type: \'手机品牌\',\\n    }],\\n    \\\"option\\\": {\\n        showValue: false,\\n        unit: \'\',\\n        customColor: [],\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          show: true,\\n          textStyle: {\\n            color: \'#464646\',\\n            fontWeight: \'normal\',\\n          },\\n        },\\n      }\\n  },\\n  {\\n    \\\"name\\\": \\\"基础条形图\\\",\\n    \\\"compType\\\": \\\"JHorizontalBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"背景柱形图\\\",\\n    \\\"compType\\\": \\\"JBackgroundBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比柱形图\\\",\\n    \\\"compType\\\": \\\"JMultipleBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"正负条形图\\\",\\n    \\\"compType\\\": \\\"JNegativeBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"百分比条形图\\\",\\n    \\\"compType\\\": \\\"JPercentBar\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"折柱图\\\",\\n    \\\"compType\\\": \\\"JMixLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"饼图\\\",\\n    \\\"compType\\\": \\\"JPie\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"南丁格尔玫瑰图\\\",\\n    \\\"compType\\\": \\\"JRose\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"旋转饼图\\\",\\n    \\\"compType\\\": \\\"JRotatePie\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        grid: {\\n          show: false,\\n          bottom: 115,\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          subtext: \'\',\\n          textStyle: {\\n            fontWeight: \'normal\',\\n          },\\n          show: true,\\n        },\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        tooltip: {\\n          trigger: \'item\',\\n        },\\n        legend: {\\n          orient: \'vertical\',\\n        },\\n        series: [\\n          {\\n            name: \'\',\\n            type: \'pie\',\\n            data: [],\\n            emphasis: {\\n              itemStyle: {\\n                shadowBlur: 10,\\n                shadowOffsetX: 0,\\n                shadowColor: \'rgba(0, 0, 0, 0.5)\',\\n              },\\n            },\\n          },\\n        ],\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"基础折线图\\\",\\n    \\\"compType\\\": \\\"JLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"平滑曲线图\\\",\\n    \\\"compType\\\": \\\"JSmoothLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"阶梯折线图\\\",\\n    \\\"compType\\\": \\\"JStepLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"面积图\\\",\\n    \\\"compType\\\": \\\"JArea\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比折线图\\\",\\n    \\\"compType\\\": \\\"JMultipleLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"双轴图\\\",\\n    \\\"compType\\\": \\\"DoubleLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础进度图\\\",\\n    \\\"compType\\\": \\\"JCustomProgress\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        barWidth: 19,\\n        padding: 12,\\n        progressColor: \'#76c7c0\',\\n        backgroundColor: \'#ffffff\',\\n        titleColor: \'#fff\',\\n        titleFontSize: 16,\\n        titlePosition: \'top\',\\n        valueColor: \'#fff\',\\n        valueFontSize: 16,\\n        valuePosition: \'middle\',\\n        valueXOffset: 0,\\n        valueYOffset: 0,\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"进度图\\\",\\n    \\\"compType\\\": \\\"JProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"列表进度图\\\",\\n    \\\"compType\\\": \\\"JListProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"圆形进度图\\\",\\n    \\\"compType\\\": \\\"JRoundProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"水波图\\\",\\n    \\\"compType\\\": \\\"JLiquid\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"象形柱图\\\",\\n    \\\"compType\\\": \\\"JPictorialBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"象形图\\\",\\n    \\\"compType\\\": \\\"JPictorial\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"男女占比\\\",\\n    \\\"compType\\\": \\\"JGender\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"普通散点图\\\",\\n    \\\"compType\\\": \\\"JScatter\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"气泡图\\\",\\n    \\\"compType\\\": \\\"JBubble\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色仪表盘\\\",\\n    \\\"compType\\\": \\\"JColorGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"渐变仪表盘\\\",\\n    \\\"compType\\\": \\\"JAntvGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"半圆仪表盘\\\",\\n    \\\"compType\\\": \\\"JSemiGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"普通漏斗图\\\",\\n    \\\"compType\\\": \\\"JFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"金字塔漏斗图\\\",\\n    \\\"compType\\\": \\\"JPyramidFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3D金字塔\\\",\\n    \\\"compType\\\": \\\"JPyramid3D\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"饼状环形图\\\",\\n    \\\"compType\\\": \\\"JRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色环形图\\\",\\n    \\\"compType\\\": \\\"JBreakRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础环形图\\\",\\n    \\\"compType\\\": \\\"JRingProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态环形图\\\",\\n    \\\"compType\\\": \\\"JActiveRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"玉珏图\\\",\\n    \\\"compType\\\": \\\"JRadialBar\\\",\\n    \\\"echart\\\": false\\n  },\\n    {\\n    \\\"name\\\": \\\"矩形图\\\",\\n    \\\"compType\\\": \\\"JRectangle\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"象限图\\\",\\n    \\\"compType\\\": \\\"JQuadrant\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"雷达图\\\",\\n    \\\"compType\\\": \\\"JRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"圆形雷达图\\\",\\n    \\\"compType\\\": \\\"JCircleRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(横向)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(竖向+序号)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(高亮)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片轮播\\\",\\n    \\\"compType\\\": \\\"JCardCarousel\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"文本\\\",\\n    \\\"compType\\\": \\\"JText\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"翻牌器\\\",\\n    \\\"compType\\\": \\\"JCountTo\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"颜色块\\\",\\n    \\\"compType\\\": \\\"JColorBlock\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"当前时间\\\",\\n    \\\"compType\\\": \\\"JCurrentTime\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数值\\\",\\n    \\\"compType\\\": \\\"JNumber\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轨道环形文字\\\",\\n    \\\"compType\\\": \\\"JOrbitRing\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"字符云\\\",\\n    \\\"compType\\\": \\\"JWordCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"图层字符云\\\",\\n    \\\"compType\\\": \\\"JImgWordCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"闪动字符云\\\",\\n    \\\"compType\\\": \\\"JFlashCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轮播表\\\",\\n    \\\"compType\\\": \\\"JScrollBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"表格\\\",\\n    \\\"compType\\\": \\\"JScrollTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"发展历程\\\",\\n    \\\"compType\\\": \\\"JDevHistory\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据表格\\\",\\n    \\\"compType\\\": \\\"JCommonTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据列表\\\",\\n    \\\"compType\\\": \\\"JList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"排行榜\\\",\\n    \\\"compType\\\": \\\"JScrollRankingBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"个性排名(前四)\\\",\\n    \\\"compType\\\": \\\"JFlashList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"气泡排名(前五)\\\",\\n    \\\"compType\\\": \\\"JBubbleRank\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(单行)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(多行+序号)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(带表头)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"区域地图\\\",\\n    \\\"compType\\\": \\\"JAreaMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d柱形图\\\",\\n    \\\"compType\\\": \\\"JBar3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d分组柱形图\\\",\\n    \\\"compType\\\": \\\"JBarGroup3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"日历\\\",\\n    \\\"compType\\\": \\\"JPermanentCalendar\\\",\\n    \\\"echart\\\": false\\n  }\\n]\"},{\"role\":\"user\",\"content\":\"用户的问题: {{userInput}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"userInput\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":179}},{\"id\":\"267498945805422592\",\"type\":\"end\",\"x\":1320,\"y\":-13.5,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{res}}\",\"outputType\":\"default\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":113}}],\"edges\":[{\"id\":\"269376764155744256\",\"type\":\"base-edge\",\"sourceNodeId\":\"267492142677889024\",\"targetNodeId\":\"267498945805422592\",\"sourceAnchorId\":\"267492142677889024_output\",\"targetAnchorId\":\"267498945805422592_input\",\"pointsList\":[{\"x\":1010,\"y\":-37},{\"x\":1110,\"y\":-37},{\"x\":1054,\"y\":-39},{\"x\":1154,\"y\":-39}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271609331975028736\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"267492142677889024\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"267492142677889024_input\",\"pointsList\":[{\"x\":555,\"y\":-39},{\"x\":655,\"y\":-39},{\"x\":578,\"y\":-37},{\"x\":678,\"y\":-37}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); +INSERT INTO `airag_flow`(`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2005948202528501762', 'admin', '2025-12-30 18:24:55', 'admin', '2026-01-07 20:06:28', 'A05A05A02', NULL, 'ghb', '修改组件配置', '', '', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'269048862299471872\'),\n end.tag(\'269049045129183232\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":436.5,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":91}},{\"id\":\"269048862299471872\",\"type\":\"llm\",\"x\":786,\"y\":502.5,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:ECharts和大屏图表配置修改专家\\n你是一位专注于ECharts和大屏图表图表配置修改的专家,能够根据用户需求,精准、高效地修改现有ECharts和大屏图表配置项,并返回完整的、可直接使用的修改后配置对象。\\n## 目标:\\n根据用户提供的具体修改指令(如:修改图表类型、调整数据、更改样式、添加交互等),对用户给出的原始ECharts配置项进行针对性修改,并输出修改后的完整配置对象。\\n## 技能:\\n1. 精通ECharts所有版本的配置项语法、结构及参数含义。\\n2. 能够准确理解用户对图表样式、数据、交互行为的修改意图。\\n3. 具备强大的代码编辑与重构能力,确保修改后的配置项语法正确、结构清晰、无冗余代码。\\n4. 对于非echart图表(componentsData提供的组件,属性中echart:false的即为非echart图表),自行从下面componentsData提供的组件对应的option配置项,修改符合要求的配置并返回。\\n## 工作流:\\n1. **接收与分析**:接收用户提供的原始ECharts配置对象(通常以JSON或JavaScript对象形式)以及具体的修改要求。仔细分析原始配置的结构和用户的修改点。\\n2. **精准修改**:严格依据用户指令,对原始配置对象进行最小化、精准化的修改。确保只改动指定部分,保持其他未提及配置的完整性。对于模糊指令,会基于ECharts最佳实践进行合理推断和实现。\\n3. **校验与格式化**:检查修改后的配置对象语法是否正确,是否符合ECharts规范。将最终配置对象以格式清晰、缩进规范的JSON或JavaScript对象形式呈现。\\n## 输出格式:\\n请始终输出一个完整的、格式化的JavaScript对象(或JSON),即修改后的 `option` 配置,只返回修改的属性配置,不要包含已存在的其他配置,\\n例如将柱体修改成黄色,就返回\\n\\\"compConfig\\\": {\\n    \\\"option\\\": {\\n      { \\\"series\\\": [ { \\\"itemStyle\\\": { \\\"color\\\": \\\"#FFFF00\\\" } } ] }\\n    }\\n}\\n例如修改组件名称为京东销量柱形图,背景色改成黑色就返回\\n\\\"compConfig\\\": {\\n \\\"name\\\":\\\"京东销量柱形图\\\",\\n \\\"background\\\":\\\"#000000\\\",\\n}\\n不要包含任何额外的解释、说明文字或代码块标记(如 ```json ```)。输出应直接以 `{` 开始,以 `}` 结束。\\n示例输出结构:\\n\\\"compConfig\\\": {\\n    \\\"name\\\":\\\"基础柱形图\\\",\\n    \\\"background\\\":\\\"#ffffff\\\",\\n    \\\"borderColor\\\":\\\"#000000\\\",\\n    \\\"option\\\": {\\n      \\\"title\\\": { ... },\\n      \\\"tooltip\\\": { ... },\\n      \\\"xAxis\\\": { ... },\\n      \\\"yAxis\\\": { ... },\\n      \\\"series\\\": [ ... ]\\n    }\\n}\\n## 限制:\\n- 仅对用户提供的原始配置进行修改,不凭空创建全新的图表配置。\\n- 输出必须仅为修改后的配置对象本身,不附带任何分析过程、修改日志或使用建议。\\n- 若用户指令存在歧义或无法实现,应在不破坏配置结构的前提下,做出最合理的默认修改或保留原样,并在配置对象内部以注释(`//`)形式简要说明。\\n- 严格遵守ECharts官方配置规范,不使用已废弃或实验性参数(除非用户明确要求)。\\n- 颜色类型的修改,要以具体色值设置,不要使用英文单词,例如黑色,使用#000000,不要使用black\\n- 修改的option属性,以componentsData中具体组件的option配置为主,结合echart选择符合要求的配置项修改\\n- 组件包含customColor属性的颜色修改,按照customColor的格式修改\\n- 若用户修改名称或者背景色或者边框的属性,以componentsData中第一个柱形图配置为例,去修改返回对应配置即可\\n -名称:对应 compConfig.name\\n -背景色:对应 compConfig.background\\n -边框色:对应 compConfig.borderColor\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"name\\\":\\\"基础柱形图\\\",\\n      \\\"background\\\":\\\"#ffffff\\\",\\n      \\\"borderColor\\\":\\\"#000000\\\",\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JStackBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n         \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}],\\n      }\\n    }\\n  },\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JDynamicBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"echart\\\":false,\\n    \\\"compType\\\": \\\"JCapsuleChart\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"showValue\\\": false,\\n        \\\"unit\\\": \\\"\\\",\\n        \\\"customColor\\\": [],\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"show\\\": true,\\n          \\\"textStyle\\\": {\\n            \\\"color\\\": \\\"#464646\\\",\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        }\\n      }\\n    }\\n  },\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JHorizontalBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JBackgroundBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JMultipleBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n  \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JNegativeBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"echart\\\":false ,\\n    \\\"compType\\\": \\\"JPercentBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n       \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}],\\n        \\\"yNameFontColor\\\": \\\"#fff\\\",\\n        \\\"yNameFontSize\\\": 12,\\n        \\\"xNameFontColor\\\": \\\"#fff\\\",\\n        \\\"xNameFontSize\\\": 12,\\n        \\\"legendLayout\\\": \\\"horizontal\\\",\\n        \\\"legendPosition\\\": \\\"bottom\\\",\\n        \\\"legendFontColor\\\": \\\"#fff\\\",\\n        \\\"legendFontSize\\\": 16,\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"show\\\": true,\\n          \\\"textStyle\\\": {\\n            \\\"color\\\": \\\"#464646\\\",\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        }\\n      }\\n    }\\n  },\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JMixLineBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JPie\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JRose\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JRotatePie\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JLine\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JSmoothLine\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JStepLine\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JArea\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {}\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JMultipleLine\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"show\\\": true,\\n          \\\"textStyle\\\": {\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        },\\n        \\\"xAxis\\\": {\\n          \\\"axisLabel\\\": {\\n            \\\"color\\\": \\\"#EEF1FA\\\"\\n          }\\n        },\\n        \\\"yAxis\\\": {\\n          \\\"yUnit\\\": \\\"\\\",\\n          \\\"axisLabel\\\": {\\n            \\\"color\\\": \\\"#EEF1FA\\\"\\n          },\\n          \\\"splitLine\\\": {\\n            \\\"show\\\": false,\\n            \\\"interval\\\": 2,\\n            \\\"lineStyle\\\": {\\n              \\\"color\\\": \\\"#8F8D8D\\\"\\n            }\\n          }\\n        },\\n        \\\"grid\\\": {\\n          \\\"top\\\": 12,\\n          \\\"bottom\\\": 18,\\n          \\\"right\\\": 40,\\n          \\\"left\\\": 0,\\n          \\\"containLabel\\\": true\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"trigger\\\": \\\"axis\\\",\\n          \\\"axisPointer\\\": {\\n            \\\"type\\\": \\\"shadow\\\",\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"backgroundColor\\\": \\\"#333\\\"\\n            }\\n          }\\n        },\\n        \\\"series\\\": [\\n          {\\n            \\\"lineType\\\": \\\"line\\\",\\n            \\\"label\\\": {\\n              \\\"position\\\": \\\"top\\\"\\n            }\\n          }\\n        ]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"DoubleLineBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"barWidth\\\": 15,\\n        \\\"borderRadius\\\": 0,\\n        \\\"symbol\\\": \\\"emptyCircle\\\",\\n        \\\"symbolSize\\\": 4,\\n        \\\"lineWidth\\\": 1,\\n        \\\"lineType\\\": \\\"line\\\",\\n        \\\"areaStyleOpacity\\\": 0,\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"show\\\": true,\\n          \\\"textStyle\\\": {\\n            \\\"fontWeight\\\": \\\"normal\\\",\\n            \\\"fontSize\\\": \\\"14\\\"\\n          }\\n        },\\n        \\\"legend\\\": {\\n          \\\"t\\\": 0\\n        },\\n        \\\"grid\\\": {\\n          \\\"top\\\": 30,\\n          \\\"bottom\\\": 18,\\n          \\\"right\\\": 40,\\n          \\\"left\\\": 0,\\n          \\\"containLabel\\\": true\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"trigger\\\": \\\"axis\\\",\\n          \\\"axisPointer\\\": {\\n            \\\"type\\\": \\\"shadow\\\",\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"backgroundColor\\\": \\\"#333\\\"\\n            }\\n          }\\n        },\\n        \\\"xAxis\\\": {\\n          \\\"axisLabel\\\": {\\n            \\\"color\\\": \\\"#EEF1FA\\\"\\n          }\\n        },\\n        \\\"yAxis\\\": [\\n          {\\n            \\\"type\\\": \\\"value\\\",\\n            \\\"yUnit\\\": \\\"\\\",\\n            \\\"axisLabel\\\": {\\n              \\\"color\\\": \\\"#EEF1FA\\\"\\n            },\\n            \\\"splitLine\\\": {\\n              \\\"show\\\": false,\\n              \\\"interval\\\": 2,\\n              \\\"lineStyle\\\": {\\n                \\\"color\\\": \\\"#8F8D8D\\\"\\n              }\\n            }\\n          },\\n          {\\n            \\\"type\\\": \\\"value\\\",\\n            \\\"yUnit\\\": \\\"\\\",\\n            \\\"axisLabel\\\": {\\n              \\\"color\\\": \\\"#EEF1FA\\\"\\n            },\\n            \\\"splitLine\\\": {\\n              \\\"interval\\\": 2,\\n              \\\"lineStyle\\\": {\\n                \\\"color\\\": \\\"#8F8D8D\\\"\\n              }\\n            }\\n          }\\n        ],\\n        \\\"series\\\": []\\n      }\\n    }\\n  },\\n  {\\n    \\\"echart\\\": false,\\n    \\\"compType\\\": \\\"JCustomProgress\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"barWidth\\\": 19,\\n        \\\"padding\\\": 12,\\n        \\\"progressColor\\\": \\\"#76c7c0\\\",\\n        \\\"backgroundColor\\\": \\\"#ffffff\\\",\\n        \\\"titleColor\\\": \\\"#fff\\\",\\n        \\\"titleFontSize\\\": 16,\\n        \\\"titlePosition\\\": \\\"top\\\",\\n        \\\"valueColor\\\": \\\"#fff\\\",\\n        \\\"valueFontSize\\\": 16,\\n        \\\"valuePosition\\\": \\\"middle\\\",\\n        \\\"valueXOffset\\\": 0,\\n        \\\"valueYOffset\\\": 0\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JProgress\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"valueXOffset\\\": 0,\\n        \\\"valueYOffset\\\": 0,\\n        \\\"grid\\\": {\\n          \\\"show\\\": false,\\n          \\\"top\\\": 0,\\n          \\\"left\\\": 0,\\n          \\\"right\\\": 55,\\n          \\\"bottom\\\": 0,\\n          \\\"containLabel\\\": true\\n        },\\n        \\\"yAxis\\\": {\\n          \\\"yUnit\\\": \\\"\\\",\\n          \\\"axisLabel\\\": {\\n            \\\"show\\\": true\\n          }\\n        },\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"show\\\": false,\\n          \\\"textStyle\\\": {}\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"confine\\\": true,\\n          \\\"trigger\\\": \\\"axis\\\",\\n          \\\"axisPointer\\\": {\\n            \\\"type\\\": \\\"none\\\",\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"backgroundColor\\\": \\\"#333\\\"\\n            }\\n          }\\n        },\\n        \\\"series\\\": [\\n          {\\n            \\\"barWidth\\\": 19,\\n            \\\"realtimeSort\\\": true,\\n            \\\"label\\\": {\\n              \\\"show\\\": false,\\n              \\\"position\\\": \\\"left\\\",\\n              \\\"formatter\\\": \\\"{c}%\\\",\\n              \\\"color\\\": \\\"black\\\",\\n              \\\"fontSize\\\": 24\\n            },\\n            \\\"itemStyle\\\": {\\n              \\\"normal\\\": {\\n                \\\"barBorderRadius\\\": 10\\n              }\\n            },\\n            \\\"color\\\": \\\"#FF9D00\\\",\\n            \\\"zlevel\\\": 1\\n          },\\n          {\\n            \\\"type\\\": \\\"bar\\\",\\n            \\\"barGap\\\": \\\"-100%\\\",\\n            \\\"color\\\": \\\"#9C9CA1\\\",\\n            \\\"barWidth\\\": 19,\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"valueAnimation\\\": true,\\n              \\\"position\\\": \\\"right\\\",\\n              \\\"color\\\": \\\"#ffffff\\\",\\n              \\\"fontSize\\\": 18,\\n              \\\"formatter\\\": \\\"{c}\\\",\\n              \\\"offset\\\": [\\n                0,\\n                0\\n              ]\\n            },\\n            \\\"itemStyle\\\": {\\n              \\\"normal\\\": {\\n                \\\"barBorderRadius\\\": 10\\n              }\\n            }\\n          }\\n        ]\\n      }\\n    }\\n  },\\n  {\\n    \\\"echart\\\": true,\\n    \\\"compType\\\": \\\"JLiquid\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"liquidType\\\": \\\"circle\\\",\\n        \\\"color\\\": \\\"#1E90FF\\\",\\n        \\\"borderWidth\\\": 2,\\n        \\\"distance\\\": 1,\\n        \\\"borderColor\\\": \\\"#1E90FF\\\",\\n        \\\"strokeOpacity\\\": 0,\\n        \\\"count\\\": 4,\\n        \\\"length\\\": 128,\\n        \\\"textColor\\\": \\\"#ffffff\\\",\\n        \\\"textFontSize\\\": 30,\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"show\\\": true,\\n          \\\"textStyle\\\": {\\n            \\\"color\\\": \\\"#464646\\\",\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        }\\n      }\\n    }\\n  },\\n   {\\n    \\\"echart\\\": false,\\n    \\\"compType\\\": \\\"JRoundProgress\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        outerCircle: { borderColor: \'#5269EE\', radius: 96 },\\n        polar: { outerRadius: 78, innerRadius: 88 },\\n        innerCircle: { borderColor: \'#5269EE\', borderWidth: 2, radius: 68 },\\n        subTitleStyle: {\\n          fontFamily: \'DIGITALDREAMFAT\',\\n          top: 56,\\n          fontSize: 26,\\n          fontGradient: { endColor: \'#FF4500\', type: \'linear\', enabled: true, startColor: \'#FFD700\', direction: \'to bottom\' },\\n          fontColor: \'#FFFFFF\',\\n        },\\n        backgroundStyle: { color: \'#4242424D\' },\\n        titleStyle: { top: 43, letterSpacing: 2, fontSize: 24, fontGradient: { endColor: \'#FFFFFF\', enabled: false, startColor: \'#000000\' }, fontColor: \'#BFBFBF\' },\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JPictorialBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"title\\\": {\\n          \\\"show\\\": true,\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"text\\\": \\\"\\\"\\n        },\\n        \\\"grid\\\": {\\n          \\\"top\\\": 60,\\n          \\\"bottom\\\": 18,\\n          \\\"right\\\": 50,\\n          \\\"left\\\": 25,\\n          \\\"containLabel\\\": true\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"trigger\\\": \\\"axis\\\",\\n          \\\"axisPointer\\\": {\\n            \\\"type\\\": \\\"shadow\\\",\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"backgroundColor\\\": \\\"#333\\\"\\n            }\\n          }\\n        },\\n        \\\"series\\\": []\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JPictorial\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"symbolSize\\\": 30,\\n        \\\"symbolMargin\\\": 0,\\n        \\\"symbol\\\": \\\"/img/bg/source/source1.svg\\\",\\n        \\\"title\\\": {\\n          \\\"show\\\": true,\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"text\\\": \\\"\\\"\\n        },\\n        \\\"grid\\\": {\\n          \\\"top\\\": 12,\\n          \\\"bottom\\\": 18,\\n          \\\"right\\\": 50,\\n          \\\"left\\\": 0,\\n          \\\"containLabel\\\": true\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"trigger\\\": \\\"axis\\\",\\n          \\\"axisPointer\\\": {\\n            \\\"type\\\": \\\"shadow\\\",\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"backgroundColor\\\": \\\"#333\\\"\\n            }\\n          }\\n        },\\n        \\\"series\\\": []\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JGender\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"title\\\": {\\n          \\\"show\\\": true,\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"text\\\": \\\"\\\"\\n        },\\n        \\\"legend\\\": {\\n          \\\"t\\\": 0,\\n          \\\"r\\\": 35\\n        },\\n        \\\"grid\\\": {\\n          \\\"bottom\\\": 115\\n        },\\n        \\\"series\\\": []\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\"\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"formatter\\\": \\\"{a}
{b} : {c}%\\\"\\n        },\\n        \\\"grid\\\": {\\n          \\\"top\\\": 53,\\n          \\\"left\\\": 50,\\n          \\\"containLabel\\\": true\\n        },\\n        \\\"series\\\": [\\n          {\\n            \\\"axisLabel\\\": {\\n              \\\"show\\\": true,\\n              \\\"fontSize\\\": 12\\n            },\\n            \\\"detail\\\": {\\n              \\\"valueAnimation\\\": true,\\n              \\\"fontSize\\\": 25,\\n              \\\"formatter\\\": \\\"{value}\\\"\\n            },\\n            \\\"splitLine\\\": {\\n              \\\"length\\\": 15,\\n              \\\"lineStyle\\\": {\\n                \\\"color\\\": \\\"#eee\\\",\\n                \\\"width\\\": 4\\n              }\\n            },\\n            \\\"axisTick\\\": {\\n              \\\"show\\\": true,\\n              \\\"lineStyle\\\": {\\n                \\\"color\\\": \\\"#eee\\\"\\n              }\\n            },\\n            \\\"progress\\\": {\\n              \\\"show\\\": true\\n            },\\n            \\\"data\\\": [],\\n            \\\"itemStyle\\\": {\\n              \\\"color\\\": \\\"#64b5f6\\\"\\n            },\\n            \\\"type\\\": \\\"gauge\\\"\\n          }\\n        ]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JColorGauge\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"textStyle\\\": {\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"formatter\\\": \\\"{a}
{b} : {c}%\\\"\\n        },\\n        \\\"series\\\": [\\n          {\\n            \\\"anchor\\\": {\\n              \\\"itemStyle\\\": {\\n                \\\"color\\\": \\\"#FAC858\\\"\\n              }\\n            },\\n            \\\"pointer\\\": {\\n              \\\"width\\\": 8\\n            },\\n            \\\"axisLabel\\\": {\\n              \\\"show\\\": true,\\n              \\\"fontSize\\\": 12\\n            },\\n            \\\"axisLine\\\": {\\n              \\\"lineStyle\\\": {\\n                \\\"width\\\": 10,\\n                \\\"color\\\": [\\n                  [\\n                    0.25,\\n                    \\\"#FF6E76\\\"\\n                  ],\\n                  [\\n                    0.5,\\n                    \\\"#FDDD60\\\"\\n                  ],\\n                  [\\n                    1,\\n                    \\\"#58D9F9\\\"\\n                  ]\\n                ]\\n              }\\n            },\\n            \\\"splitLine\\\": {\\n              \\\"length\\\": 15,\\n              \\\"lineStyle\\\": {\\n                \\\"color\\\": \\\"#eee\\\",\\n                \\\"width\\\": 4\\n              }\\n            },\\n            \\\"axisTick\\\": {\\n              \\\"show\\\": true,\\n              \\\"lineStyle\\\": {\\n                \\\"color\\\": \\\"#eee\\\"\\n              }\\n            },\\n            \\\"title\\\": {\\n              \\\"fontSize\\\": 14\\n            }\\n          }\\n        ]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JAntvGauge\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"gaugeType\\\": \\\"\\\",\\n        \\\"gaugeWidth\\\": 15,\\n        \\\"axisTickShow\\\": true,\\n        \\\"lineColor\\\": \\\"#eee\\\",\\n        \\\"axisLabelShow\\\": true,\\n        \\\"axisLabelColor\\\": \\\"#fff\\\",\\n        \\\"axisLabelFontSize\\\": 15,\\n        \\\"valueFontSize\\\": 30,\\n        \\\"valueColor\\\": \\\"#fff\\\",\\n        \\\"indicatorColor\\\": \\\"#D0D0D0\\\",\\n        \\\"indicatorLength\\\": 8,\\n        \\\"colorType\\\": \\\"4\\\",\\n        \\\"colors\\\": [\\n          {\\n            \\\"color1\\\": \\\"#67e0e3\\\",\\n            \\\"color2\\\": \\\"\\\"\\n          }\\n        ],\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"textStyle\\\": {\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        }\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JFunnel\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"reversal\\\": false,\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"textStyle\\\": {\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          },\\n          \\\"show\\\": true\\n        },\\n        \\\"grid\\\": {\\n          \\\"bottom\\\": 115\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"trigger\\\": \\\"item\\\",\\n          \\\"formatter\\\": \\\"{a}
{b} : {c}%\\\"\\n        },\\n        \\\"legend\\\": {\\n          \\\"orient\\\": \\\"horizontal\\\"\\n        },\\n        \\\"series\\\": [\\n          {\\n            \\\"name\\\": \\\"Funnel\\\",\\n            \\\"type\\\": \\\"funnel\\\",\\n            \\\"left\\\": \\\"10%\\\",\\n            \\\"right\\\": \\\"10%\\\",\\n            \\\"bottom\\\": \\\"5%\\\",\\n            \\\"sort\\\": \\\"descending\\\",\\n            \\\"gap\\\": 2,\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"position\\\": \\\"inside\\\"\\n            },\\n            \\\"labelLine\\\": {\\n              \\\"length\\\": 10,\\n              \\\"lineStyle\\\": {\\n                \\\"width\\\": 1,\\n                \\\"type\\\": \\\"solid\\\"\\n              }\\n            },\\n            \\\"itemStyle\\\": {\\n              \\\"borderColor\\\": \\\"#fff\\\",\\n              \\\"borderWidth\\\": 1\\n            },\\n            \\\"emphasis\\\": {\\n              \\\"label\\\": {\\n                \\\"fontSize\\\": 20\\n              }\\n            }\\n          }\\n        ]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JPyramidFunnel\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"reversal\\\": false,\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"textStyle\\\": {\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          },\\n          \\\"show\\\": true\\n        },\\n        \\\"grid\\\": {\\n          \\\"bottom\\\": 115\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"trigger\\\": \\\"item\\\",\\n          \\\"formatter\\\": \\\"{a}
{b} : {c}%\\\"\\n        },\\n        \\\"legend\\\": {\\n          \\\"orient\\\": \\\"horizontal\\\"\\n        },\\n        \\\"series\\\": [\\n          {\\n            \\\"name\\\": \\\"Funnel\\\",\\n            \\\"type\\\": \\\"funnel\\\",\\n            \\\"left\\\": \\\"10%\\\",\\n            \\\"right\\\": \\\"10%\\\",\\n            \\\"sort\\\": \\\"ascending\\\",\\n            \\\"bottom\\\": 0,\\n            \\\"gap\\\": 2,\\n            \\\"label\\\": {\\n              \\\"show\\\": true,\\n              \\\"position\\\": \\\"inside\\\"\\n            },\\n            \\\"labelLine\\\": {\\n              \\\"length\\\": 10,\\n              \\\"lineStyle\\\": {\\n                \\\"width\\\": 1,\\n                \\\"type\\\": \\\"solid\\\"\\n              }\\n            },\\n            \\\"itemStyle\\\": {\\n              \\\"borderColor\\\": \\\"#fff\\\",\\n              \\\"borderWidth\\\": 1\\n            },\\n            \\\"emphasis\\\": {\\n              \\\"label\\\": {\\n                \\\"fontSize\\\": 20\\n              }\\n            }\\n          }\\n        ]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JRing\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"grid\\\": {\\n          \\\"show\\\": false,\\n          \\\"top\\\": 50,\\n          \\\"left\\\": 50\\n        },\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"textStyle\\\": {\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          },\\n          \\\"show\\\": true\\n        },\\n        \\\"tooltip\\\": {\\n          \\\"trigger\\\": \\\"item\\\"\\n        },\\n        \\\"series\\\": [\\n          {\\n            \\\"name\\\": \\\"Access From\\\",\\n            \\\"type\\\": \\\"pie\\\",\\n            \\\"radius\\\": [\\n              \\\"40%\\\",\\n              \\\"70%\\\"\\n            ],\\n            \\\"avoidLabelOverlap\\\": false,\\n            \\\"label\\\": {\\n              \\\"show\\\": false,\\n              \\\"position\\\": \\\"center\\\"\\n            },\\n            \\\"emphasis\\\": {\\n              \\\"label\\\": {\\n                \\\"show\\\": true,\\n                \\\"fontWeight\\\": \\\"bold\\\",\\n                \\\"fontSize\\\": 14\\n              }\\n            },\\n            \\\"labelLine\\\": {\\n              \\\"show\\\": false\\n            },\\n            \\\"data\\\": []\\n          }\\n        ]\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JRingProgress\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"color\\\": \\\"#1E90FF\\\",\\n        \\\"bgColor\\\": \\\"#E8EDF3\\\",\\n        \\\"radius\\\": 0.9,\\n        \\\"innerRadius\\\": 0.9,\\n        \\\"lineHeight\\\": 0,\\n        \\\"fontColor\\\": \\\"#ffffff\\\",\\n        \\\"fontSize\\\": 16,\\n        \\\"fontWeight\\\": \\\"normal\\\",\\n        \\\"valueFontSize\\\": 16,\\n        \\\"valueFontColor\\\": \\\"#ffffff\\\",\\n        \\\"valueFontWeight\\\": \\\"normal\\\"\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JActiveRing\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"lineWidth\\\": 10,\\n        \\\"radius\\\": 100,\\n        \\\"activeRadius\\\": 120,\\n        \\\"showOriginValue\\\": false,\\n        \\\"customColor\\\": [],\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"textColor\\\": \\\"#ffffff\\\",\\n          \\\"textFontSize\\\": 20,\\n          \\\"show\\\": true,\\n          \\\"textStyle\\\": {\\n            \\\"color\\\": \\\"#464646\\\",\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        }\\n      }\\n    }\\n  },\\n  {\\n    \\\"compType\\\": \\\"JRadialBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"option\\\": {\\n        \\\"type\\\": \\\"bar\\\",\\n        \\\"radius\\\": 0.8,\\n        \\\"innerRadius\\\": 0.2,\\n        \\\"maxAngle\\\": 240,\\n        \\\"radiuShow\\\": false,\\n        \\\"bgShow\\\": false,\\n        \\\"title\\\": {\\n          \\\"text\\\": \\\"\\\",\\n          \\\"textAlign\\\": \\\"left\\\",\\n          \\\"show\\\": true,\\n          \\\"textStyle\\\": {\\n            \\\"color\\\": \\\"#464646\\\",\\n            \\\"fontWeight\\\": \\\"normal\\\"\\n          }\\n        }\\n      }\\n    }\\n  }\\n]\"},{\"role\":\"user\",\"content\":\"用户的问题:{{userQuestion}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"userQuestion\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":157}},{\"id\":\"269049045129183232\",\"type\":\"end\",\"x\":1272,\"y\":458.5,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{option}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":135}}],\"edges\":[{\"id\":\"269048862303666176\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"269048862299471872\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"269048862299471872_input\",\"pointsList\":[{\"x\":466,\"y\":422},{\"x\":566,\"y\":422},{\"x\":520,\"y\":444},{\"x\":620,\"y\":444}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"269049045129183233\",\"type\":\"base-edge\",\"sourceNodeId\":\"269048862299471872\",\"targetNodeId\":\"269049045129183232\",\"sourceAnchorId\":\"269048862299471872_output\",\"targetAnchorId\":\"269049045129183232_input\",\"pointsList\":[{\"x\":952,\"y\":444},{\"x\":1052,\"y\":444},{\"x\":1006,\"y\":422},{\"x\":1106,\"y\":422}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); +INSERT INTO `airag_flow`(`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2006294471763537922', 'admin', '2025-12-31 17:20:52', 'admin', '2026-01-06 18:02:41', 'A05A05A02', NULL, 'ghb', '仪表盘数据处理', '', '', 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'269395028940378112\'),\n end.tag(\'269395047139463168\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":436.5,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":91}},{\"id\":\"269395028940378112\",\"type\":\"llm\",\"x\":790,\"y\":480.5,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:SQL数据专家\\n你是一位精通SQL查询、数据提取与分析的专家,专注于将用户的数据需求转化为高效、准确的SQL语句,并提供清晰的数据洞察。\\n## 目标:\\n1. 根据用户描述的业务问题或数据需求,编写精准、优化的SQL查询语句。\\n2. 对查询结果进行分析,提炼关键信息、趋势或异常,并以易于理解的方式呈现结论。\\n## 技能:\\n1. **需求解析**:能够快速理解用户的数据提取与分析需求,并将其拆解为具体的数据库操作步骤。\\n2. **SQL编写**:精通标准SQL语法,能熟练运用`SELECT`, `JOIN`, `WHERE`, `GROUP BY`, `HAVING`, `窗口函数`、`CTE`等完成复杂查询。\\n3. **性能优化**:具备编写高效SQL语句的意识,能考虑索引、子查询优化、避免全表扫描等问题。\\n4. **数据分析**:能够对查询结果集进行基本的统计分析(如汇总、对比、趋势计算),并解释其业务含义。\\n5. **结果呈现**:能够将数据结果和分析结论结构化、清晰地组织起来。\\n## 工作流:\\n1. **澄清需求**:首先与用户确认其数据需求的具体细节,包括但不限于:涉及的表、字段、筛选条件、聚合维度、排序要求以及期望的分析角度。如果信息不足,主动提问。\\n2. **构建查询**:基于澄清后的需求,构思并编写SQL查询语句。在输出代码前,简要说明查询的逻辑思路。\\n3. **执行与验证(模拟)**:以注释或说明的形式,模拟查询可能返回的结果样例或数据结构,确保逻辑正确。\\n4. **分析与洞察**:基于模拟的查询结果,进行数据分析。指出关键数据点、趋势、异常或值得注意的发现,并用平实的语言解释其潜在的业务意义。\\n5. **提供建议**:根据分析结果,可能的话,提出进一步深入分析的查询方向或基于数据的行动建议。\\n## 输出格式:\\n你的回答应遵循以下结构:\\n1. **需求确认**:[复述并确认你理解的需求]\\n2. **查询思路**:[简要说明你将如何通过SQL实现该需求]\\n3. **SQL代码**:\\n - 这里放置你编写的SQL代码\\n4. **预期结果/分析**:\\n- **数据摘要**:[描述查询结果的主要特征,如行数、关键统计值]\\n- **核心洞察**:[列出1-3个最重要的发现或结论]\\n- **详细说明**:[对上述洞察进行展开解释]\\n6. **后续建议(可选)**:[基于当前分析,提出后续可探索的问题或查询建议]\\n## 限制:\\n- 所有SQL语句应基于通用的ANSI SQL标准编写,若需使用特定数据库(如MySQL, PostgreSQL)的方言,需明确指出。\\n- 只允许生成查询SQL语句,其他SQL操作全部禁止。\\n- 只返回SQL语句本身,例如:select * from demo; 不要返回其他任何无关内容。\\n- 不要返回sql外的任何内容,例如```sql select * from demo```,这种格式是必须禁止的,只能SQL本身。\\n- 在分析数据时,所有推断和结论需基于查询结果逻辑得出,对于无法从给定需求中确定的信息,使用“[需核实]”标记。\\n- 不得生成任何用于非法数据访问、破坏数据完整性或侵犯隐私的SQL语句(如`DROP TABLE`, 未经授权的`DELETE`,或涉及个人敏感信息的无条件查询)。涉及此类请求时,应拒绝并引导至合规方向。\\n- 保持回答的专业性和客观性,避免主观臆断。\\n- 用户提供业务数据,在业务数据中找表名的,根据需求,返回合适的表名,禁止主观臆断或者生成构建虚假数据和非提供业务数据之外的内容。\"},{\"role\":\"user\",\"content\":\"{{content}}\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"content\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":179}},{\"id\":\"269395047139463168\",\"type\":\"end\",\"x\":1272,\"y\":458.5,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{res}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"269395028940378112\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":135}}],\"edges\":[{\"id\":\"269395028948766720\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"269395028940378112\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"269395028940378112_input\",\"pointsList\":[{\"x\":466,\"y\":422},{\"x\":566,\"y\":422},{\"x\":524,\"y\":422},{\"x\":624,\"y\":422}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"269395047143657472\",\"type\":\"base-edge\",\"sourceNodeId\":\"269395028940378112\",\"targetNodeId\":\"269395047139463168\",\"sourceAnchorId\":\"269395028940378112_output\",\"targetAnchorId\":\"269395047139463168_input\",\"pointsList\":[{\"x\":956,\"y\":422},{\"x\":1056,\"y\":422},{\"x\":1006,\"y\":422},{\"x\":1106,\"y\":422}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"269395028940378112\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); + +-- 多模态能力- 文档示例sql +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2009516824079048705', 'admin', '2026-01-09 14:45:21', 'admin', '2026-01-12 11:22:56', 'A05A01A01', NULL, '多模态文件_示例', NULL, NULL, 'chatSimple', '', '# 角色:多模态信息处理专家\n你是一位精通图像识别与文本分析的专业助手,能够从用户提供的图片和文本中提取关键信息,并进行综合性的总结与洞察。\n\n## 目标:\n1. 准确、高效地从用户提供的图片和文本中提取核心信息。\n2. 将提取出的多模态信息进行整合、关联与分析,生成一份结构清晰、重点突出的总结报告。\n\n## 技能:\n1. **图像内容解析**:能够识别图片中的物体、场景、文字、人物动作、情绪及潜在含义。\n2. **文本信息提取**:能够从文本中抓取关键事实、数据、观点、情感倾向和逻辑结构。\n3. **跨模态关联分析**:能够发现图片与文本信息之间的互补、印证或矛盾关系,并进行关联性解读。\n4. **结构化总结**:能够将零散信息组织成逻辑连贯、层次分明的总结,突出核心结论与洞察。\n\n## 工作流:\n1. **信息接收与确认**:首先,请用户提供需要处理的图片和文本。确认接收后,告知用户你已准备开始分析。\n2. **分项提取**:\n * **对于图片**:逐一描述每张图片的视觉内容,包括但不限于主体对象、背景环境、文字信息(如有)、色彩氛围及可能传达的意图或情感。\n * **对于文本**:提炼文本的核心主题、关键论点、重要数据、主要结论及作者的情感或立场。\n3. **综合分析与关联**:对比分析提取出的图片信息和文本信息。指出它们之间是否存在主题一致性、信息补充、例证关系或潜在冲突。挖掘图片可能为文本提供的视觉证据,或文本为图片提供的背景解释。\n4. **生成总结报告**:基于以上分析,生成一份综合性总结。报告应包含:\n * **总体概述**:用一两句话概括所有材料共同表达的核心主题或事件。\n * **关键信息点**:分点列出从图片和文本中提取出的最重要的事实、发现或观点。\n * **关联洞察**:阐述图片与文本如何相互支撑或共同构建了一个更完整的叙事。\n * **潜在疑问或需核实点**:如果发现信息模糊、矛盾或需要进一步验证的地方,在此处明确指出。\n\n## 输出格式:\n请以清晰的Markdown格式组织你的回复。使用标题(如“### 图片分析”、“### 文本提炼”、“### 综合总结”)来划分不同部分。在总结部分,优先使用列表和要点来呈现信息,确保报告易于阅读和理解。\n\n## 限制:\n- 所有对图片内容的描述应基于可见的视觉元素进行客观陈述,避免过度主观臆测。对于不确定的解读,使用“可能”、“似乎”等词语,或标注“[推测]”。\n- 总结必须严格基于用户提供的材料,不得引入外部知识或编造信息。对于无法从材料中得出的结论,不得妄下判断。\n- 若用户提供的图片无法显示或文本无法读取,应明确告知用户并请求重新提供。\n- 遵守伦理规范,不传播或总结涉及隐私泄露、歧视性内容或违法信息的材料。如遇此类内容,应停止处理并提示用户。', '1890232564262739969', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"OPENAI\",\"modelType\":\"LLM\",\"modelName\":\"gpt-4o\"}}', '[]', NULL, NULL, NULL, NULL, NULL, NULL); + + +-- AI应用: AI生成图表 +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2008448202536456193', 'admin', '2026-01-06 15:59:01', 'admin', '2026-01-12 22:09:49', 'A01', NULL, 'AI生成图表', NULL, '', 'chatFLow', '你好,我是图表生成智能体。', '# 角色\n你是一个犀利的电影解说员,可以使用尖锐幽默的语言,向用户讲解电影剧情、介绍最新上映的电影,还可以用普通人都可以理解的语言讲解电影相关知识。\n\n## 技能\n### 技能 1: 推荐最新上映的电影\n1. 当用户请你推荐最新电影时,需要先了解用户喜欢哪种类型片。如果你已经知道了,请跳过这一步,在询问时可以用“请问您喜欢什么类型的电影呢亲”。\n2. 如果你并不知道用户所说的电影,可以使用 工具搜索电影,了解电影类型。\n3. 根据用户的电影偏好,推荐几部正在上映和即将上映的电影,在推荐开头可以说“好的亲,以下是为您推荐的电影”。\n===回复示例===\n - 🎬 电影名: <电影名>\n - 🕐 上映时间: <电影在中国大陆的上映的日期>\n - 💡 电影简介: <100字总结这部电影的剧情摘要>\n===示例结束===\n\n### 技能 2: 介绍电影\n1. 当用户说介绍某一部电影,请使用工具 搜索电影介绍的链接,在收到需求时可以回应“好嘞亲,马上为您查找相关电影介绍”。\n2. 如果此时获取的信息不够全面,可以继续使用 工具 打开搜索结果中的相关链接,以了解电影详情。\n3. 根据搜索和浏览结果,生成电影介绍\n### 技能 3: 介绍电影概念\n- 你可以使用数据集中的知识,调用 知识库 搜索相关知识,并向用户介绍基础概念,介绍前可以说“亲,下面为您介绍一下这个电影概念”。\n- 使用用户熟悉的电影,举一个实际的场景解释概念\n\n## 限制:\n- 只讨论与电影有关的内容,拒绝回答与电影无关的话题,拒绝时可以说“不好意思亲,这边只讨论电影相关话题哦”。\n- 所输出的内容必须按照给定的格式进行组织,不能偏离框架要求,在表述中合理运用常用语。\n- 总结部分不能超过 100 字。\n- 只会输出知识库中已有内容, 不在知识库中的书籍, 通过 工具去了解。\n- 请使用 Markdown 的 ^^ 形式说明引用来源。”', NULL, '', '2008379264947519489', 'release', 10, NULL, '[{\"key\":1,\"descr\":\"用户性别比例\",\"update\":true}]', NULL, NULL, NULL, NULL, NULL, NULL); + +-- AI流程: 生成图表 +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2008379264947519489', 'admin', '2026-01-06 11:25:05', 'admin', '2026-01-12 19:59:49', 'A01', NULL, 'ghb', '系统_生成图表', '', '', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'271484464342847488\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'271484464342847488\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'271484464342847488\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'271484464342847488\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'271484464342847488\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":508.5,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":91}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":818.5,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":135}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":461.5,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":135}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":2934,\"y\":561.5,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":135}},{\"id\":\"271484464342847488\",\"type\":\"tools\",\"x\":1760,\"y\":449.5,\"properties\":{\"text\":\"查询所有数据库表\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryTablesInfoText\",\"toolDescr\":\"用于查询指定数据源的所有表名和描述\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源code,不填则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryTablesInfoText\",\"method\":\"GET\",\"headers\":{}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":157}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2152,\"y\":621.5,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7,\"timeout\":60}},\"history\":10,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n## 能力\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n## 工作流程\\n1. **需求确认与澄清**:\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n2. **数据获取**:\\n* 判断用户需求涉及的表是否在已知范围内。\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n* 调用工具执行SQL,获取原始数据集。\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n3. **支持的图表类型**:\\n* `bar`: 柱状图\\n* `line`: 折线图、曲线图\\n* `pie`: 饼图\\n4. **数据转换**:\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n* 数据转换时能直接转换就不要调用工具转换。\\n5. **结果封装与输出**:\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n* **双重校验**:\\n* **格式校验**:确保``标签首尾完整闭合。\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n## 输出格式\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n``` html\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n```\\n## 限制\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryTablesInfoText`工具。\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n- **格式严格性**:必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键。\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因\\n\\n\\n## 支持的数据库表\\n\\n\\n{{allTable}}\\n\\n\\n> 注意:以上就是所有的支持的数据库表,禁止再次执行`queryTablesInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"result\",\"name\":\"allTable\",\"nodeId\":\"271484464342847488\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":179}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2535,\"y\":432.5,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":113}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":418.5,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":117}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":604.5,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":135}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":619.5,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":91}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548210219581440\",\"type\":\"base-edge\",\"sourceNodeId\":\"271484464342847488\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"271484464342847488_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":1926,\"y\":402},{\"x\":2026,\"y\":402},{\"x\":1886,\"y\":563},{\"x\":1986,\"y\":563}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2318,\"y\":563},{\"x\":2418,\"y\":563},{\"x\":2269,\"y\":407},{\"x\":2369,\"y\":407}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2701,\"y\":407},{\"x\":2801,\"y\":407},{\"x\":2668,\"y\":525},{\"x\":2768,\"y\":525}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271819058293420032\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"271484464342847488\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"271484464342847488_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1494,\"y\":402},{\"x\":1594,\"y\":402}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); + +-- MCP插件: 数据库插件 +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('2006287314794676226', NULL, '数据库插件', '用于执行数据库操作', 'plugin', 'api', NULL, '', '[{\"name\":\"queryTableMetadata\",\"description\":\"用于查询表的表结构(元数据)\",\"path\":\"/airag/mcp/database/queryTableMetadata\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"tableName\",\"description\":\"表名\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result.tableName\",\"description\":\"表名(数据库实际表名)\",\"type\":\"Object\"},{\"name\":\"result.tableComment\",\"description\":\"表注释(业务含义)\",\"type\":\"Object\"},{\"name\":\"result.columns[].columnName\",\"description\":\"字段名\",\"type\":\"Array\"},{\"name\":\"result.columns[].columnComment\",\"description\":\"字段注释(核心,帮助大模型理解业务)\",\"type\":\"Array\"},{\"name\":\"result.columns[].dataType\",\"description\":\"数据类型(如varchar、int、datetime)\",\"type\":\"Array\"},{\"name\":\"result.columns[].isPrimaryKey\",\"description\":\"是否主键\",\"type\":\"Array\"}]},{\"name\":\"sqlExecute\",\"description\":\"用于执行 SQL 语句,仅能支持执行SELECT语句,不要输入注释等无关信息。\",\"path\":\"/airag/mcp/database/sqlExecute\",\"method\":\"POST\",\"enabled\":true,\"parameters\":[{\"name\":\"sql\",\"description\":\"\",\"type\":\"String\",\"location\":\"Body\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result\",\"description\":\"返回查询的结果,是个对象数组,数组的每一项都是一条数据,每条数据的key都是传入的查询的列。\",\"type\":\"Array\"}]},{\"name\":\"queryTablesInfoText\",\"description\":\"用于查询指定数据源的所有表名和描述\",\"path\":\"/airag/mcp/database/queryTablesInfoText\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源code,不填则系统默认\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]}]', 'enable', 1, '{\"tokenParamName\":\"X-Access-Token\",\"tool_count\":3,\"authType\":\"token\",\"tokenParamValue\":\"\"}', 'admin', '2025-12-31 16:52:26', 'admin', '2026-01-12 19:45:24', 'A01', NULL); + + +-- AI写作 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2010623918706446338', '1892553163993931777', 'AI写作', '/airag/aiwriter/AiWriter', 'super/airag/aiwriter/AiWriter', 1, '', NULL, 1, NULL, '0', 9.00, 0, 'ant-design:edit-filled', 1, 0, 0, 0, NULL, 'admin', '2026-01-12 16:04:32', 'admin', '2026-01-12 16:04:50', 0, 0, NULL, 0); +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2010634128233779202', 'admin', '2026-01-12 16:45:07', 'admin', '2026-01-14 19:56:22', 'A05A01A01', NULL, 'AI写作_示例', NULL, NULL, 'chatSimple', '', '## 角色:精准内容与段落配图生成专家\n你是一位专业的内容生成助手,能够严格按照用户指定的格式、语气、长度和语言要求,直接输出精准匹配的最终内容,并为每个独立段落配上 1 张高相关度的图片。\n## 任务类型识别\n1. 回复类任务:当用户提供原始问题和参考回复时,仅基于给定内容生成精准回复,不得额外添加无关信息(如通知、背景介绍等)。\n2. 文章类任务:当用户提供主题时,撰写结构清晰、内容准确的完整文章,可包含引言、主体段落、总结等部分。\n## 目标\n1. **严格遵循指令**:完全按照用户指定的格式、语气、语言和长度要求生成内容。\n2. **直接输出结果**:仅输出符合要求的正文内容和对应的段落配图,不包含任何额外的标题、解释、道歉或中间过程。\n3. **逐段精准配图**:为每一个独立的段落匹配 1 张与该段内容强相关的图片,图片直接插入到对应段落的末尾,而非统一放在全文结尾。\n4. **适配两种模式**:既能独立创作短文并逐段配图,也能基于给定的原文和参考内容生成精准回复并逐段配图。\n## 核心规则\n1. 严格匹配要求:必须完全遵循用户指定的格式、语气、长度和语言要求。\n\n## 技能\n1. **精准指令解析**:准确识别用户的创作模式(独立创作 / 回复)、格式(消息 / 邮件等)、语气(友善 / 专业等)、语言(中文 / 英文等)和长度(短 / 中 / 长)。\n2. **无冗余输出**:仅生成符合要求的正文内容,不添加任何指令外的信息。\n3. **独立创作能力**:针对独立创作需求,能围绕核心主题生成结构清晰、语言流畅的短文。\n4. **精准回复能力**:针对回复需求,能基于原文和参考内容生成精准匹配的简短回复。\n5. **逐段配图能力**:为每个独立段落提取精准关键词,调用图片工具完成搜索,图片直接插入到对应段落的末尾。\n6. **避免搜索死循环**:每个图片仅使用 1-2 个精准关键词一次搜索完成,不反复调整关键词。\n7. **内容精准性**:回复类内容必须与参考内容完全一致,不得扩写;文章类内容必须准确、专业,不虚构事实。\n## 工作流(内部执行,不对外展示)\n1. **识别需求类型**:判断用户需求是独立创作短文,还是基于给定内容生成回复。\n2. **解析参数要求**:提取并确认格式、语气、语言、长度等所有约束条件。\n3. **生成精准内容**:\n - 独立创作:围绕核心主题,生成符合长度和语气要求的正文,并自然分段。\n - 回复:基于原文和参考内容,生成精准匹配的简短回复,并自然分段。\n4. **逐段匹配配图**:为每个独立段落提取 1-2 个与该段内容强相关的关键词,调用图片工具完成搜索。\n5. **整合输出**:将图片以路径的方式直接插入到对应段落的末尾,仅输出最终的图文内容,不包含任何额外信息或中间过程。\n## 限制\n- 禁止输出标题、解释、过程或额外说明。\n- 只对重点内容配图,非重点内容不配图。\n- 图片必须与重点内容高度相关。\n- 每个重点内容只配 1 张图,避免重复搜索。\n- 图片插入在重点内容附近,不集中放在结尾。\n- 语言必须符合用户指定的要求。', '1897481367743143938', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"}}', '[]', NULL, '[{\"pluginId\":\"1988208474780168193\",\"pluginName\":\"图片搜索\",\"category\":\"plugin\"}]', '', NULL, 1, NULL); + +-- 示例OCR提示词修改 +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'167880707187527680\')).to(\n end.tag(\'167880856269869056\'),\n THEN(\n code_167881149430747136.tag(\'code_167881149430747136\'),\n llm.tag(\'167881839356006400\'),\n end.tag(\'167880661561888768\')\n ).tag(\"code_167881149430747136\")\n ).tag(\'167880707187527680\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":420,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":90,\"width\":332}},{\"id\":\"167880661561888768\",\"type\":\"end\",\"x\":1474,\"y\":341,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\",\"outputType\":\"default\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"}],\"height\":112,\"width\":332}},{\"id\":\"167880707187527680\",\"type\":\"switch\",\"x\":681,\"y\":232,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"images\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"picture\"}],\"next\":\"167880856269869056\"}],\"else\":{\"next\":\"code_167881149430747136\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":116,\"width\":332}},{\"id\":\"167880856269869056\",\"type\":\"end\",\"x\":1207,\"y\":206,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{\\n    \\\"message\\\": \\\"请提供图片\\\"\\n  }\",\"outputType\":\"text\"},\"inputParams\":[],\"outputParams\":[],\"height\":112,\"width\":332}},{\"id\":\"code_167881149430747136\",\"type\":\"code\",\"x\":937,\"y\":458,\"properties\":{\"text\":\"脚本执行\",\"options\":{\"codeType\":\"groovy\",\"code\":\"def main(Map params) {\\n def newQuestion = params.question\\n if (!params.question) {\\n newQuestion = \\\"从图片中提取文字\\\"\\n }\\n return [result: newQuestion]\\n}\\n\"},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":156,\"width\":332}},{\"id\":\"167881839356006400\",\"type\":\"llm\",\"x\":1318,\"y\":605,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:OCR工具\\n作为一个智能OCR工具,你的主要职责是从图片中提取文字并将其输出为结构化数据。\\n## 目标:\\n1. 精确识别和提取图片中的文字信息。\\n2. 将提取的文字转换为结构化数据格式。\\n## 技能:\\n1. 高效的图像处理能力。\\n2. 精确的文字识别算法。\\n3. 数据格式化与输出能力。\\n## 工作流:\\n1. 输入图片,进行预处理(如去噪、二值化)。\\n2. 应用OCR算法识别图片中的文字,并记录识别结果。\\n3. 将识别的文字整理成结构化数据格式,如JSON或CSV。\\n## 输出格式:必须严格遵循以下JSON格式,不得添加任何额外字段或自由文本。\\n\\n{\\n    \\\"text\\\": \\\"提取的内容\\\",\\n    \\\"metadata\\\": {\\\"source\\\": \\\"图片来源\\\", \\\"timestamp\\\": \\\"提取时间\\\"}\\n  }\\n## 限制:\\n- 仅限于合法和合规的图片内容提取。\\n- 不得保存用户上传的图片数据。\\n- 需确保输出的数据准确无误,标注所有数据来源。\\n- 输出必须严格符合上述格式,字段名和层级结构不得随意更改。\"},{\"role\":\"user\",\"content\":\"{{question}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"images\",\"name\":\"images\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"picture\"},{\"field\":\"result\",\"name\":\"question\",\"nodeId\":\"code_167881149430747136\",\"customValue\":\"\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":178,\"width\":332}}],\"edges\":[{\"id\":\"167880707195916288\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"167880707187527680\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"167880707187527680_input\",\"pointsList\":[{\"x\":466,\"y\":406},{\"x\":566,\"y\":406},{\"x\":415,\"y\":205},{\"x\":515,\"y\":205}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167880856274063360\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"167880856269869056\",\"sourceAnchorId\":\"167880707187527680_source_if\",\"targetAnchorId\":\"167880856269869056_input\",\"pointsList\":[{\"x\":847,\"y\":239},{\"x\":947,\"y\":239},{\"x\":941,\"y\":181},{\"x\":1041,\"y\":181}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167881149434941440\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"code_167881149430747136\",\"sourceAnchorId\":\"167880707187527680_source_else\",\"targetAnchorId\":\"code_167881149430747136_input\",\"pointsList\":[{\"x\":847,\"y\":265},{\"x\":947,\"y\":265},{\"x\":671,\"y\":411},{\"x\":771,\"y\":411}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167881839356006401\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167881149430747136\",\"targetNodeId\":\"167881839356006400\",\"sourceAnchorId\":\"code_167881149430747136_output\",\"targetAnchorId\":\"167881839356006400_input\",\"pointsList\":[{\"x\":1103,\"y\":411},{\"x\":1203,\"y\":411},{\"x\":1052,\"y\":547},{\"x\":1152,\"y\":547}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167882293611712512\",\"type\":\"base-edge\",\"sourceNodeId\":\"167881839356006400\",\"targetNodeId\":\"167880661561888768\",\"sourceAnchorId\":\"167881839356006400_output\",\"targetAnchorId\":\"167880661561888768_input\",\"pointsList\":[{\"x\":1484,\"y\":547},{\"x\":1584,\"y\":547},{\"x\":1208,\"y\":316},{\"x\":1308,\"y\":316}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"},{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '1904779811574784002'; + +-- 流程id修改字段长度 +ALTER TABLE `airag_app` +MODIFY COLUMN `flow_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '流程id(多个以逗号分隔)' AFTER `knowledge_ids`; + +-- AI流程: 生成图表 +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-01-16 12:01:03', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = '系统_生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3280,\"y\":446,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2492,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7,\"timeout\":60}},\"history\":10,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n## 能力\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n## 工作流程\\n1. **需求确认与澄清**:\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n* 用户可能要求你通过指定的数据源查询数据(具体的数据源列表从下表得知),若没有指定则不需要传数据源参数。\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n2. **数据获取**:\\n* 判断用户需求涉及的表是否在已知范围内。\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n* 构建查询SQL时,需要明确数据源的数据库类型,根据不同的数据库构建不同的SQL方言。\\n* 调用工具执行SQL,获取原始数据集。\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n3. **支持的图表类型**:\\n* `bar`: 柱状图\\n* `line`: 折线图、曲线图\\n* `pie`: 饼图\\n4. **数据转换**:\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n* 数据转换时能直接转换就不要调用工具转换。\\n5. **结果封装与输出**:\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n* **双重校验**:\\n* **格式校验**:确保``标签首尾完整闭合。\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n## 输出格式\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n``` html\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n```\\n## 限制\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryDataSourceInfoText`工具。\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n- **格式严格性**:必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键。\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因\\n## 默认数据源类型\\n{{dbType}}\\n## 支持的数据源\\n{{allDbSource}}\\n\\n> 注意:以上就是所有的支持的数据源,禁止再次执行和`queryDataSourceInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2885,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2124,\"y\":660,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"274833789969932288\",\"type\":\"tools\",\"x\":1745,\"y\":466,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2658,\"y\":374},{\"x\":2758,\"y\":374},{\"x\":2619,\"y\":605},{\"x\":2719,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":3051,\"y\":605},{\"x\":3151,\"y\":605},{\"x\":3014,\"y\":409},{\"x\":3114,\"y\":409}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2290,\"y\":623},{\"x\":2390,\"y\":623},{\"x\":2226,\"y\":374},{\"x\":2326,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274833790062206976\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"274833789969932288\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"274833789969932288_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1479,\"y\":418},{\"x\":1579,\"y\":418}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274833917929758720\",\"type\":\"base-edge\",\"sourceNodeId\":\"274833789969932288\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"274833789969932288_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1911,\"y\":418},{\"x\":2011,\"y\":418},{\"x\":1858,\"y\":623},{\"x\":1958,\"y\":623}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; + +-- MCP插件: 数据库插件 +UPDATE `airag_mcp` SET `icon` = NULL, `name` = '数据库插件', `descr` = '用于执行数据库操作', `category` = 'plugin', `type` = 'api', `endpoint` = '', `headers` = '{\"X-Sign\":\"true\"}', `tools` = '[{\"name\":\"queryTableMetadata\",\"description\":\"用于查询表的表结构(元数据)\",\"path\":\"/airag/mcp/database/queryTableMetadata\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"tableName\",\"description\":\"表名\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"dbSourceKey\",\"description\":\"数据源key\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result.tableName\",\"description\":\"表名(数据库实际表名)\",\"type\":\"Object\"},{\"name\":\"result.tableComment\",\"description\":\"表注释(业务含义)\",\"type\":\"Object\"},{\"name\":\"result.columns[].columnName\",\"description\":\"字段名\",\"type\":\"Array\"},{\"name\":\"result.columns[].columnComment\",\"description\":\"字段注释(核心,帮助大模型理解业务)\",\"type\":\"Array\"},{\"name\":\"result.columns[].dataType\",\"description\":\"数据类型(如varchar、int、datetime)\",\"type\":\"Array\"},{\"name\":\"result.columns[].isPrimaryKey\",\"description\":\"是否主键\",\"type\":\"Array\"}]},{\"name\":\"sqlExecute\",\"description\":\"用于执行 SQL 语句,仅能支持执行SELECT语句,不要输入注释等无关信息。\",\"path\":\"/airag/mcp/database/sqlExecute\",\"method\":\"POST\",\"enabled\":true,\"parameters\":[{\"name\":\"sql\",\"description\":\"要执行的SQL\",\"type\":\"String\",\"location\":\"Body\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"dbSourceKey\",\"description\":\"数据源key\",\"type\":\"String\",\"location\":\"Body\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result\",\"description\":\"返回查询的结果,是个对象数组,数组的每一项都是一条数据,每条数据的key都是传入的查询的列。\",\"type\":\"Array\"}]},{\"name\":\"queryTablesInfoText\",\"description\":\"用于查询指定数据源的所有表名和描述\",\"path\":\"/airag/mcp/database/queryTablesInfoText\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源code,不填则系统默认\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]},{\"name\":\"queryDataSourceInfoText\",\"description\":\"用于查询所有数据源的信息,不需要传递参数。\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[],\"responses\":[]},{\"name\":\"queryDataSourceType\",\"description\":\"获取默认数据源或指定数据的数据库类型\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]}]', `status` = 'enable', `synced` = 1, `metadata` = '{\"tokenParamName\":\"X-Access-Token\",\"tool_count\":5,\"authType\":\"token\",\"tokenParamValue\":\"\"}', `create_by` = 'admin', `create_time` = '2025-12-31 16:52:26', `update_by` = 'admin', `update_time` = '2026-01-16 12:00:22', `sys_org_code` = 'A01', `tenant_id` = NULL WHERE `id` = '2006287314794676226'; +UPDATE `airag_model` SET `name` = 'deepseek', `model_params` = NULL WHERE `id` = '1897481367743143938'; +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'269048862299471872\'),\n end.tag(\'269049045129183232\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":436.5,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":91}},{\"id\":\"269048862299471872\",\"type\":\"llm\",\"x\":786,\"y\":513.5,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:ECharts和大屏图表配置修改专家\\n你是一位专注于ECharts和大屏图表图表配置修改的专家,能够根据用户需求,精准、高效地修改现有ECharts和大屏图表配置项,并返回完整的、可直接使用的修改后配置对象。\\n## 目标:\\n根据用户提供的具体修改指令(如:修改图表类型、调整数据、更改样式、添加交互等),对用户给出的原始ECharts配置项进行针对性修改,并输出修改后的完整配置对象。\\n## 技能:\\n1. 精通ECharts所有版本的配置项语法、结构及参数含义。\\n2. 能够准确理解用户对图表样式、数据、交互行为的修改意图。\\n3. 具备强大的代码编辑与重构能力,确保修改后的配置项语法正确、结构清晰、无冗余代码。\\n4. 对于非echart图表(componentsData提供的组件,属性中echart:false的即为非echart图表),自行从下面componentsData提供的组件对应的option配置项,修改符合要求的配置并返回。\\n## 工作流:\\n1. **接收与分析**:接收用户提供的原始ECharts配置对象(通常以JSON或JavaScript对象形式)以及具体的修改要求。仔细分析原始配置的结构和用户的修改点。\\n2. **精准修改**:严格依据用户指令,对原始配置对象进行最小化、精准化的修改。确保只改动指定部分,保持其他未提及配置的完整性。对于模糊指令,会基于ECharts最佳实践进行合理推断和实现。\\n3. **校验与格式化**:检查修改后的配置对象语法是否正确,是否符合ECharts规范。将最终配置对象以格式清晰、缩进规范的JSON或JavaScript对象形式呈现。\\n## 输出格式:\\n请始终输出一个完整的、格式化的JavaScript对象(或JSON),即修改后的 `option` 配置,只返回修改的属性配置,不要包含已存在的其他配置,\\n例如将柱体修改成黄色,就返回\\n\\\"compConfig\\\": {\\n    \\\"option\\\": {\\n      { \\\"series\\\": [ { \\\"itemStyle\\\": { \\\"color\\\": \\\"#FFFF00\\\" } } ] }\\n    }\\n}\\n例如修改组件名称为京东销量柱形图,背景色改成黑色就返回\\n\\\"compConfig\\\": {\\n \\\"name\\\":\\\"京东销量柱形图\\\",\\n \\\"background\\\":\\\"#000000\\\",\\n}\\n不要包含任何额外的解释、说明文字或代码块标记(如 ```json ```)。输出应直接以 `{` 开始,以 `}` 结束。\\n示例输出结构:\\n\\\"compConfig\\\": {\\n    \\\"name\\\":\\\"基础柱形图\\\",\\n    \\\"background\\\":\\\"#ffffff\\\",\\n    \\\"borderColor\\\":\\\"#000000\\\",\\n    \\\"option\\\": {\\n      \\\"title\\\": { ... },\\n      \\\"tooltip\\\": { ... },\\n      \\\"xAxis\\\": { ... },\\n      \\\"yAxis\\\": { ... },\\n      \\\"series\\\": [ ... ]\\n    }\\n}\\n## 限制:\\n- 仅对用户提供的原始配置进行修改,不凭空创建全新的图表配置。\\n- 输出必须仅为修改后的配置对象本身,不附带任何分析过程、修改日志或使用建议。\\n- 若用户指令存在歧义或无法实现,应在不破坏配置结构的前提下,做出最合理的默认修改或保留原样,并在配置对象内部以注释(`//`)形式简要说明。\\n- 严格遵守ECharts官方配置规范,不使用已废弃或实验性参数(除非用户明确要求)。\\n- 颜色类型的修改,要以具体色值设置,不要使用英文单词,例如黑色,使用#000000,不要使用black\\n- 修改的option属性,以componentsData中具体组件的option配置为主,结合echart选择符合要求的配置项修改\\n- 组件包含customColor属性的颜色修改,按照customColor的格式修改\\n- 若用户修改名称或者背景色或者边框的属性,以componentsData中第一个柱形图配置为例,去修改返回对应配置即可\\n -名称:对应 compConfig.name\\n -背景色:对应 compConfig.background\\n -边框色:对应 compConfig.borderColor\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"name\\\":\\\"基础柱形图\\\",\\n      \\\"background\\\":\\\"#ffffff\\\",\\n      \\\"borderColor\\\":\\\"#000000\\\",\\n      \\\"option\\\": {\\n        \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}]\\n      }\\n    }\\n  }]\\n组件配置说明\\n compOptionData = [\\n  {\\n    name: \'基础配置\',\\n    optionName: \'BasicOption\',\\n    children: [\\n      {\\\"label\\\": \\\"图层名称修改成\\\", \\\"value\\\": \\\"name\\\"},\\n      {\\\"label\\\": \\\"图层背景色设置成\\\", \\\"value\\\": \\\"background\\\"},\\n      {\\\"label\\\": \\\"图层边框线设置成\\\", \\\"value\\\": \\\"borderColor\\\"},\\n      {\\\"label\\\": \\\"提示语设置为隐藏\\\", \\\"value\\\": \\\"option.tooltip.show\\\"},\\n      {\\\"label\\\": \\\"提示语字体大小设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"提示语字体颜色设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"},\\n    ]\\n  },{\\n    name: \'标题设置\',\\n    optionName: \'TitleOption\',\\n    children: [\\n      {\\\"label\\\": \\\"标题名称修改成\\\", \\\"value\\\": \\\"option.title.text\\\"},\\n      {\\\"label\\\": \\\"标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontColor\\\"},\\n      {\\\"label\\\": \\\"标题字体粗细设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontWeight\\\"},\\n      {\\\"label\\\": \\\"副标题名称修改成\\\", \\\"value\\\": \\\"option.title.subtextStyle\\\"},\\n      {\\\"label\\\": \\\"副标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"副标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontColor\\\"},\\n      {\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"option.title.left\\\"},\\n      {\\\"label\\\": \\\"垂直居中\\\", \\\"value\\\": \\\"option.title.top\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'X轴设置\',\\n    optionName: \'XAxisOption\',\\n    children: [\\n      {\\\"label\\\": \\\"X轴名称修改成\\\", \\\"value\\\": \\\"option.xAxis.name\\\"},\\n      {\\\"label\\\": \\\"X轴名称颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.color\\\"},\\n      {\\\"label\\\": \\\"X轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"X轴标签颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"X轴标签角度\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.rotate\\\"},\\n      {\\\"label\\\": \\\"X轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"X轴轴类型修改成\\\", \\\"value\\\": \\\"option.xAxis.type\\\"},\\n      {\\\"label\\\": \\\"X轴显示网格线\\\", \\\"value\\\": \\\"option.xAxis.splitLine.show\\\"},\\n      {\\\"label\\\": \\\"X轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.splitLine.lineStyle.color\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'Y轴设置\',\\n    optionName: \'YAxisOption\',\\n    children: [\\n      {\\\"label\\\": \\\"Y轴名称修改成\\\", \\\"value\\\": \\\"option.yAxis.name\\\"},\\n      {\\\"label\\\": \\\"Y轴名称颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"Y轴标签颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"Y轴标签角度\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.rotate\\\"},\\n      {\\\"label\\\": \\\"Y轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴轴类型修改成\\\", \\\"value\\\": \\\"option.yAxis.type\\\"},\\n      {\\\"label\\\": \\\"Y轴显示网格线\\\", \\\"value\\\": \\\"option.yAxis.splitLine.show\\\"},\\n      {\\\"label\\\": \\\"Y轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.splitLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴单位\\\", \\\"value\\\": \\\"option.yAxis.yUnit\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'图例设置\',\\n    optionName: \'LegendOption\',\\n    children: [\\n      {\\\"label\\\": \\\"图例字体大小设置成\\\", \\\"value\\\": \\\"option.legend.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"图例设置成横排\\\", \\\"value\\\": \\\"option.legend.orient\\\"},\\n      {\\\"label\\\": \\\"图例上下边距设置\\\", \\\"value\\\": \\\"option.legend.t\\\"},\\n      {\\\"label\\\": \\\"图例左右边距设置\\\", \\\"value\\\": \\\"option.legend.r\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'自定义配色\',\\n    optionName: \'CustomColorOption\',\\n    children: [\\n      {\\\"label\\\": \\\"颜色设置成***色\\\", \\\"value\\\": \\\"option.customColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'柱体设置\',\\n    optionName: \'BarCylinder\',\\n    children: [\\n      {\\\"label\\\": \\\"柱体宽度修改为\\\", \\\"value\\\": \\\"option.series[${index}].barWidth\\\"},\\n      {\\\"label\\\": \\\"柱体圆角修改为\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.barBorderRadius\\\"},\\n      {\\\"label\\\": \\\"柱体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.color\\\"},\\n      {\\\"label\\\": \\\"柱体背景色显隐\\\", \\\"value\\\": \\\"option.series[${index}].showBackground\\\"},\\n      {\\\"label\\\": \\\"柱体背景色颜色\\\", \\\"value\\\": \\\"option.series[${index}].backgroundStyle.color\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'折线设置\',\\n    optionName: \'PolyglineOption\',\\n    children: [\\n      {\\\"label\\\": \\\"折线类型修改\\\", \\\"value\\\": \\\"option.series[${index}].lineType\\\"},\\n      {\\\"label\\\": \\\"线条宽度修改\\\", \\\"value\\\": \\\"option.series[${index}].lineWidth\\\"},\\n      {\\\"label\\\": \\\"标记点修改\\\", \\\"value\\\": \\\"option.series[${index}].symbol\\\"},\\n      {\\\"label\\\": \\\"点的大小修改\\\", \\\"value\\\": \\\"option.series[${index}].symbolSize\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'饼图设置\',\\n    optionName: \'pieSettingOption\',\\n    children: [\\n      {\\\"label\\\": \\\"饼图设置成环形\\\", \\\"value\\\": \\\"option.isRadius\\\"},\\n      {\\\"label\\\": \\\"饼图内环半径设置成\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"饼图外环半径设置成\\\", \\\"value\\\": \\\"option.outRadius\\\"},\\n      {\\\"label\\\": \\\"饼图设置成南丁格尔玫瑰\\\", \\\"value\\\": \\\"option.isRose\\\"},\\n      {\\\"label\\\": \\\"饼图标签显示位置\\\", \\\"value\\\": \\\"option.pieLabelPosition\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'坐标轴边距\',\\n    optionName: \'GridOption\',\\n    children: [\\n      {\\\"label\\\": \\\"左边距修改成\\\", \\\"value\\\": \\\"option.grid.left\\\"},\\n      {\\\"label\\\": \\\"顶边距\\\", \\\"value\\\": \\\"option.grid.top\\\"},\\n      {\\\"label\\\": \\\"右边距\\\", \\\"value\\\": \\\"option.grid.right\\\"},\\n      {\\\"label\\\": \\\"底边距\\\", \\\"value\\\": \\\"option.grid.bottom\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'数值设置\',\\n    optionName: \'NumOption\',\\n    children: [\\n      {\\\"label\\\": \\\"数值显示位置在\\\", \\\"value\\\": \\\"option.series[${index}].label.position\\\"},\\n      {\\\"label\\\": \\\"数值内容格式修改成\\\", \\\"value\\\": \\\"option.label.format\\\"},\\n      {\\\"label\\\": \\\"数值字体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.color\\\"},\\n      {\\\"label\\\": \\\"数值字体大小修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontSize\\\"},\\n      {\\\"label\\\": \\\"数值字体粗细修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontWeight\\\"},\\n      {\\\"label\\\": \\\"数值单位配置显隐\\\", \\\"value\\\": \\\"option.showUnit.show\\\"},\\n      {\\\"label\\\": \\\"数值单位数量级设置\\\", \\\"value\\\": \\\"option.showUnit.numberLevel\\\"},\\n      {\\\"label\\\": \\\"数值单位保留小数\\\", \\\"value\\\": \\\"option.showUnit.decimal\\\"},\\n    ]\\n  }\\n];\\n\\n\"},{\"role\":\"user\",\"content\":\"用户的问题:{{userQuestion}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userQuestion\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":179}},{\"id\":\"269049045129183232\",\"type\":\"end\",\"x\":1272,\"y\":458.5,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{option}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":135}}],\"edges\":[{\"id\":\"269048862303666176\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"269048862299471872\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"269048862299471872_input\",\"pointsList\":[{\"x\":466,\"y\":422},{\"x\":566,\"y\":422},{\"x\":520,\"y\":455},{\"x\":620,\"y\":455}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"269049045129183233\",\"type\":\"base-edge\",\"sourceNodeId\":\"269048862299471872\",\"targetNodeId\":\"269049045129183232\",\"sourceAnchorId\":\"269048862299471872_output\",\"targetAnchorId\":\"269049045129183232_input\",\"pointsList\":[{\"x\":952,\"y\":455},{\"x\":1052,\"y\":455},{\"x\":1006,\"y\":422},{\"x\":1106,\"y\":422}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '2005948202528501762'; + + +-- AI应用: AI生成图表 +UPDATE `airag_app` SET `create_by` = 'admin', `create_time` = '2026-01-06 15:59:01', `update_by` = 'admin', `update_time` = '2026-01-16 17:13:36', `sys_org_code` = 'A01', `tenant_id` = NULL, `name` = 'AI生成图表', `descr` = NULL, `icon` = '', `type` = 'chatFLow', `prologue` = '你好,我是图表生成智能体。', `prompt` = '# 角色\n你是一个犀利的电影解说员,可以使用尖锐幽默的语言,向用户讲解电影剧情、介绍最新上映的电影,还可以用普通人都可以理解的语言讲解电影相关知识。\n\n## 技能\n### 技能 1: 推荐最新上映的电影\n1. 当用户请你推荐最新电影时,需要先了解用户喜欢哪种类型片。如果你已经知道了,请跳过这一步,在询问时可以用“请问您喜欢什么类型的电影呢亲”。\n2. 如果你并不知道用户所说的电影,可以使用 工具搜索电影,了解电影类型。\n3. 根据用户的电影偏好,推荐几部正在上映和即将上映的电影,在推荐开头可以说“好的亲,以下是为您推荐的电影”。\n===回复示例===\n - 🎬 电影名: <电影名>\n - 🕐 上映时间: <电影在中国大陆的上映的日期>\n - 💡 电影简介: <100字总结这部电影的剧情摘要>\n===示例结束===\n\n### 技能 2: 介绍电影\n1. 当用户说介绍某一部电影,请使用工具 搜索电影介绍的链接,在收到需求时可以回应“好嘞亲,马上为您查找相关电影介绍”。\n2. 如果此时获取的信息不够全面,可以继续使用 工具 打开搜索结果中的相关链接,以了解电影详情。\n3. 根据搜索和浏览结果,生成电影介绍\n### 技能 3: 介绍电影概念\n- 你可以使用数据集中的知识,调用 知识库 搜索相关知识,并向用户介绍基础概念,介绍前可以说“亲,下面为您介绍一下这个电影概念”。\n- 使用用户熟悉的电影,举一个实际的场景解释概念\n\n## 限制:\n- 只讨论与电影有关的内容,拒绝回答与电影无关的话题,拒绝时可以说“不好意思亲,这边只讨论电影相关话题哦”。\n- 所输出的内容必须按照给定的格式进行组织,不能偏离框架要求,在表述中合理运用常用语。\n- 总结部分不能超过 100 字。\n- 只会输出知识库中已有内容, 不在知识库中的书籍, 通过 工具去了解。\n- 请使用 Markdown 的 ^^ 形式说明引用来源。”', `model_id` = NULL, `knowledge_ids` = '', `flow_id` = '2008379264947519489', `status` = 'enable', `msg_num` = 30, `metadata` = NULL, `preset_question` = '[{\"key\":1,\"descr\":\"用户性别比例\",\"update\":true}]', `quick_command` = NULL, `plugins` = NULL, `memory_id` = NULL, `variables` = NULL, `iz_open_memory` = NULL, `memory_prompt` = NULL WHERE `id` = '2008448202536456193'; + +-- AI流程: 生成图表 +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-01-16 17:11:05', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = '系统_生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'274833789969932288\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3280,\"y\":446,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2492,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7,\"timeout\":60}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n## 能力\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n## 工作流程\\n1. **需求确认与澄清**:\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n* 用户可能要求你通过指定的数据源查询数据(具体的数据源列表从下表得知),若没有指定则不需要传数据源参数。\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n2. **数据获取**:\\n* 判断用户需求涉及的表是否在已知范围内。\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n* 构建查询SQL时,需要明确数据源的数据库类型,根据不同的数据库构建不同的SQL方言。\\n* 调用工具执行SQL,获取原始数据集。\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n3. **支持的图表类型**:\\n* `bar`: 柱状图\\n* `line`: 折线图、曲线图\\n* `pie`: 饼图\\n* `radar`: 雷达图\\n* `gauge`: 仪表盘\\n* `barline`: 折柱图\\n* `multibar`: 多列柱状图\\n* `multiline`: 多行折线图\\n* `area`: 面积图\\n4. **数据转换**:\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n* 数据转换时能直接转换就不要调用工具转换。\\n5. **结果封装与输出**:\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n* **双重校验**:\\n* **格式校验**:确保``标签首尾完整闭合。\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n## 输出格式\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n``` html\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n```\\n> 注:bar、line、pie为简单图表,可直接通过x、y来展示数据,而radar、gauge、barline、multibar、multiline、area为复杂图表,你需要先通过工具查询示例格式后,严格按照示例格式拼装`data`JSON;该工具支持逗号分割,你尽量一次性查询所有需要的图表示例格式,若你已知晓图表格式,无需再次查询。\\n## 限制\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryDataSourceInfoText`工具。\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n- **格式严格性**:`ghb-chart`标签的前后必须严格保证有两个空行;必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键或示例数据中所需的键。\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因\\n## 默认数据源类型\\n{{dbType}}\\n## 支持的数据源\\n{{allDbSource}}\\n\\n> 注意:以上就是所有的支持的数据源,禁止再次执行和`queryDataSourceInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2885,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2124,\"y\":660,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"274833789969932288\",\"type\":\"tools\",\"x\":1745,\"y\":466,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2658,\"y\":374},{\"x\":2758,\"y\":374},{\"x\":2619,\"y\":605},{\"x\":2719,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":3051,\"y\":605},{\"x\":3151,\"y\":605},{\"x\":3014,\"y\":409},{\"x\":3114,\"y\":409}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2290,\"y\":623},{\"x\":2390,\"y\":623},{\"x\":2226,\"y\":374},{\"x\":2326,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274833790062206976\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"274833789969932288\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"274833789969932288_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1479,\"y\":418},{\"x\":1579,\"y\":418}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274833917929758720\",\"type\":\"base-edge\",\"sourceNodeId\":\"274833789969932288\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"274833789969932288_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1911,\"y\":418},{\"x\":2011,\"y\":418},{\"x\":1858,\"y\":623},{\"x\":1958,\"y\":623}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; + +-- MCP插件: 数据库插件 +UPDATE `airag_mcp` SET `icon` = NULL, `name` = '数据库插件', `descr` = '用于执行数据库操作', `category` = 'plugin', `type` = 'api', `endpoint` = '', `headers` = '{\"X-Sign\":\"true\"}', `tools` = '[{\"name\":\"queryTableMetadata\",\"description\":\"用于查询表的表结构(元数据)\",\"path\":\"/airag/mcp/database/queryTableMetadata\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"tableName\",\"description\":\"表名\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"dbSourceKey\",\"description\":\"数据源key\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result.tableName\",\"description\":\"表名(数据库实际表名)\",\"type\":\"Object\"},{\"name\":\"result.tableComment\",\"description\":\"表注释(业务含义)\",\"type\":\"Object\"},{\"name\":\"result.columns[].columnName\",\"description\":\"字段名\",\"type\":\"Array\"},{\"name\":\"result.columns[].columnComment\",\"description\":\"字段注释(核心,帮助大模型理解业务)\",\"type\":\"Array\"},{\"name\":\"result.columns[].dataType\",\"description\":\"数据类型(如varchar、int、datetime)\",\"type\":\"Array\"},{\"name\":\"result.columns[].isPrimaryKey\",\"description\":\"是否主键\",\"type\":\"Array\"}]},{\"name\":\"sqlExecute\",\"description\":\"用于执行 SQL 语句,仅能支持执行SELECT语句,不要输入注释等无关信息。\",\"path\":\"/airag/mcp/database/sqlExecute\",\"method\":\"POST\",\"enabled\":true,\"parameters\":[{\"name\":\"sql\",\"description\":\"要执行的SQL\",\"type\":\"String\",\"location\":\"Body\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"dbSourceKey\",\"description\":\"数据源key\",\"type\":\"String\",\"location\":\"Body\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result\",\"description\":\"返回查询的结果,是个对象数组,数组的每一项都是一条数据,每条数据的key都是传入的查询的列。\",\"type\":\"Array\"}]},{\"name\":\"queryTablesInfoText\",\"description\":\"用于查询指定数据源的所有表名和描述\",\"path\":\"/airag/mcp/database/queryTablesInfoText\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源code,不填则系统默认\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]},{\"name\":\"queryDataSourceInfoText\",\"description\":\"用于查询所有数据源的信息,不需要传递参数。\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[],\"responses\":[]},{\"name\":\"queryDataSourceType\",\"description\":\"获取默认数据源或指定数据的数据库类型\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]},{\"name\":\"getChartExampleJson\",\"description\":\"用户获取图表示例数据\",\"path\":\"/airag/mcp/database/getChartExampleJson\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"type\",\"description\":\"图表类型,多个用英文逗号分割\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"}],\"responses\":[]}]', `status` = 'enable', `synced` = 1, `metadata` = '{\"tokenParamName\":\"X-Access-Token\",\"tool_count\":6,\"authType\":\"token\",\"tokenParamValue\":\"\"}', `create_by` = 'admin', `create_time` = '2025-12-31 16:52:26', `update_by` = 'admin', `update_time` = '2026-01-16 17:00:51', `sys_org_code` = 'A01', `tenant_id` = NULL WHERE `id` = '2006287314794676226'; + +UPDATE `airag_app` SET `name` = 'Chat2BI', `descr` = 'Chat BI(powered by LLM)'WHERE `id` = '2008448202536456193'; +UPDATE `airag_flow` SET `name` = 'AI大屏SQL助手' WHERE `id` = '2006294471763537922'; +UPDATE `airag_flow` SET `name` = 'AI大屏优化配置' WHERE `id` = '2005948202528501762'; +UPDATE `airag_flow` SET `name` = 'AI大屏生成组件' WHERE `id` = '2004398098378108929'; +UPDATE `airag_flow` SET `name` = 'Chat2BI生成图表' WHERE `id` = '2008379264947519489'; + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2012375501376606210', '1892553163993931777', 'AI工具箱', '/ai/box', 'layouts/default/index', 1, '', NULL, 1, NULL, '0', 11.00, 0, 'ant-design:tool-outlined', 0, 0, 0, 0, NULL, 'admin', '2026-01-17 12:04:42', 'admin', '2026-01-17 12:09:42', 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2012376076054974466', '1892553163993931777', '提示词管理', '/ai/prompt', 'layouts/default/index', 1, '', NULL, 1, NULL, '0', 10.00, 0, 'ant-design:star-outlined', 0, 0, 0, 0, NULL, 'admin', '2026-01-17 12:06:59', 'admin', '2026-01-17 12:09:27', 0, 0, NULL, 0); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(), '-', ''), '1600076470335246337','2012375501376606210', NULL, NOW(), '127.0.0.16'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(), '-', ''), '1600076470335246337','2012376076054974466', NULL, NOW(), '127.0.0.17'); +INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(), '-', ''), '1600076470335246337','1996422809213341698', NULL, NOW(), '127.0.0.17'); + +UPDATE `sys_permission` SET `parent_id` = '1892553163993931777', `name` = '应用门户', `url` = '/app/portal', `component` = 'super/airag/aiapp/chat/portal/AppPortal', `is_route` = 1, `component_name` = '', `redirect` = NULL, `menu_type` = 1, `perms` = NULL, `perms_type` = '0', `sort_no` = 0.00, `always_show` = 0, `icon` = 'ant-design:appstore-filled', `is_leaf` = 1, `keep_alive` = 0, `hidden` = 0, `hide_tab` = 0, `description` = NULL, `create_by` = 'admin', `create_time` = '2025-12-04 11:34:24', `update_by` = 'admin', `update_time` = '2026-01-17 12:10:14', `del_flag` = 0, `rule_flag` = 0, `status` = NULL, `internal_or_external` = 0 WHERE `id` = '1996422809213341698'; +UPDATE `sys_permission` SET `parent_id` = '2012376076054974466', `name` = 'AI评估器', `url` = '/super/airag/experiment', `component` = 'super/airag/aiprompts/AiragExtDataExperiment', `is_route` = 1, `component_name` = '', `redirect` = NULL, `menu_type` = 1, `perms` = NULL, `perms_type` = '0', `sort_no` = 7.10, `always_show` = 0, `icon` = 'ant-design:sliders-outlined', `is_leaf` = 1, `keep_alive` = 0, `hidden` = 0, `hide_tab` = 0, `description` = NULL, `create_by` = 'admin', `create_time` = '2025-12-16 18:48:18', `update_by` = 'admin', `update_time` = '2026-01-17 12:08:14', `del_flag` = 0, `rule_flag` = 0, `status` = NULL, `internal_or_external` = 0 WHERE `id` = '2000880658872508417'; +UPDATE `sys_permission` SET `parent_id` = '2012376076054974466', `name` = 'AI提示词', `url` = '/super/airag/aiprompts', `component` = 'super/airag/aiprompts/AiragPromptsList', `is_route` = 1, `component_name` = '', `redirect` = NULL, `menu_type` = 1, `perms` = NULL, `perms_type` = '0', `sort_no` = 7.00, `always_show` = 0, `icon` = 'ant-design:exclamation-circle-outlined', `is_leaf` = 1, `keep_alive` = 0, `hidden` = 0, `hide_tab` = 0, `description` = NULL, `create_by` = 'admin', `create_time` = '2025-12-12 14:34:16', `update_by` = 'admin', `update_time` = '2026-01-17 12:08:01', `del_flag` = 0, `rule_flag` = 0, `status` = NULL, `internal_or_external` = 0 WHERE `id` = '1999367175911657473'; +UPDATE `sys_permission` SET `parent_id` = '2012375501376606210', `name` = 'AI简历', `url` = '/airag/word', `component` = 'super/airag/wordtpl/EoaWordTemplateList', `is_route` = 1, `component_name` = NULL, `redirect` = NULL, `menu_type` = 1, `perms` = NULL, `perms_type` = '1', `sort_no` = 15.00, `always_show` = 0, `icon` = 'ant-design:file-word-outlined', `is_leaf` = 0, `keep_alive` = 0, `hidden` = 0, `hide_tab` = 0, `description` = NULL, `create_by` = 'admin', `create_time` = '2025-07-09 20:02:21', `update_by` = 'admin', `update_time` = '2026-01-17 12:05:20', `del_flag` = 0, `rule_flag` = 0, `status` = '1', `internal_or_external` = 0 WHERE `id` = '2025070908023480210'; +UPDATE `sys_permission` SET `parent_id` = '2012375501376606210', `name` = 'OCR识别', `url` = '/ai/ocr', `component` = 'super/airag/ocr/AiOcrList', `is_route` = 1, `component_name` = '', `redirect` = NULL, `menu_type` = 1, `perms` = NULL, `perms_type` = '0', `sort_no` = 8.00, `always_show` = 0, `icon` = 'ant-design:scan-outlined', `is_leaf` = 1, `keep_alive` = 0, `hidden` = 0, `hide_tab` = 0, `description` = NULL, `create_by` = 'admin', `create_time` = '2025-04-17 14:22:41', `update_by` = 'admin', `update_time` = '2026-01-17 12:05:13', `del_flag` = 0, `rule_flag` = 0, `status` = NULL, `internal_or_external` = 0 WHERE `id` = '1912753560201089025'; +UPDATE `sys_permission` SET `parent_id` = '2012375501376606210', `name` = 'Ai海报', `url` = '/airag/aiposter/AiPoster', `component` = 'super/airag/aiposter/AiPoster', `is_route` = 1, `component_name` = '', `redirect` = NULL, `menu_type` = 1, `perms` = NULL, `perms_type` = '0', `sort_no` = 8.00, `always_show` = 0, `icon` = 'ant-design:file-image-filled', `is_leaf` = 1, `keep_alive` = 0, `hidden` = 0, `hide_tab` = 0, `description` = NULL, `create_by` = 'admin', `create_time` = '2026-01-06 20:29:33', `update_by` = 'admin', `update_time` = '2026-01-17 12:05:05', `del_flag` = 0, `rule_flag` = 0, `status` = NULL, `internal_or_external` = 0 WHERE `id` = '2008516285254000642'; +UPDATE `sys_permission` SET `parent_id` = '2012375501376606210', `name` = 'AI写作', `url` = '/airag/aiwriter/AiWriter', `component` = 'super/airag/aiwriter/AiWriter', `is_route` = 1, `component_name` = '', `redirect` = NULL, `menu_type` = 1, `perms` = NULL, `perms_type` = '0', `sort_no` = 9.00, `always_show` = 0, `icon` = 'ant-design:edit-filled', `is_leaf` = 1, `keep_alive` = 0, `hidden` = 0, `hide_tab` = 0, `description` = NULL, `create_by` = 'admin', `create_time` = '2026-01-12 16:04:32', `update_by` = 'admin', `update_time` = '2026-01-17 12:04:57', `del_flag` = 0, `rule_flag` = 0, `status` = NULL, `internal_or_external` = 0 WHERE `id` = '2010623918706446338'; + +-- AI流程: 生成图表 +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-01-19 19:13:49', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = 'Chat2BI生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n## 能力\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n## 工作流程\\n1. **需求确认与澄清**:\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n* 用户可能要求你通过指定的数据源查询数据(具体的数据源列表从下表得知),若没有指定则不需要传数据源参数。\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n2. **数据获取**:\\n* 判断用户需求涉及的表是否在已知范围内。\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n* 构建查询SQL时,需要明确数据源的数据库类型,根据不同的数据库构建不同的SQL方言。\\n* 调用工具执行SQL,获取原始数据集。\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n3. **支持的图表类型**:\\n* `bar`: 柱状图\\n* `line`: 折线图、曲线图\\n* `pie`: 饼图\\n* `radar`: 雷达图\\n* `gauge`: 仪表盘\\n* `barline`: 折柱图\\n* `multibar`: 多列柱状图\\n* `multiline`: 多行折线图\\n* `area`: 面积图\\n4. **数据转换**:\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n* 数据转换时能直接转换就不要调用工具转换。\\n5. **结果封装与输出**:\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n* **双重校验**:\\n* **格式校验**:确保``标签首尾完整闭合。\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n## 输出格式\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n``` html\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n```\\n> 注:bar、line、pie为简单图表,可直接通过x、y来展示数据,而radar、gauge、barline、multibar、multiline、area为复杂图表,你需要先通过工具查询示例格式后,严格按照示例格式拼装`data`JSON;该工具支持逗号分割,你尽量一次性查询所有需要的图表示例格式。\\n## 限制\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryDataSourceInfoText`工具。\\n- 简单图表类型格式,或已经查询过的图表类型格式,严禁再次调用工具查询。\\n- 不要向用户提及`ghb-chart`标签以及图表格式相关信息。\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n- **格式严格性**:`ghb-chart`标签的前后必须严格保证有两个空行;必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键或示例数据中所需的键。\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因\\n## 默认数据源类型\\n{{defDbType}}\\n## 支持的数据源\\n{{allDbSource}}\\n\\n> 注意:以上就是所有的支持的数据源,禁止再次执行和`queryDataSourceInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2025070908023480210', '2012375501376606210', 'AI简历', '/airag/word', 'super/airag/wordtpl/EoaWordTemplateList', 1, NULL, NULL, 1, NULL, '1', 15.00, 0, 'ant-design:file-word-outlined', 0, 0, 0, 0, NULL, 'admin', '2025-07-09 20:02:21', 'admin', '2026-01-17 12:05:20', 0, 0, '1', 0); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2025070908023490211', '2025070908023480210', '添加word模版管理', NULL, NULL, 0, NULL, NULL, 2, 'wordtpl:template:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-09 20:02:21', 'admin', '2025-07-09 20:11:09', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2025070908023490212', '2025070908023480210', '编辑word模版管理', NULL, NULL, 0, NULL, NULL, 2, 'wordtpl:template:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-09 20:02:21', 'admin', '2025-07-09 20:11:13', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2025070908023490213', '2025070908023480210', '删除word模版管理', NULL, NULL, 0, NULL, NULL, 2, 'wordtpl:template:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-09 20:02:21', 'admin', '2025-07-09 20:11:17', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2025070908023490214', '2025070908023480210', '批量删除word模版管理', NULL, NULL, 0, NULL, NULL, 2, 'wordtpl:template:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-09 20:02:21', 'admin', '2025-07-09 20:11:21', 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2025070908023490215', '2025070908023480210', '设计word模版', NULL, NULL, 0, NULL, NULL, 2, 'wordtpl:template:design', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-09 20:02:21', 'admin', '2025-07-09 20:19:04', 0, 0, '1', 0); + + +CREATE TABLE `aigc_word_template` ( + `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, + `create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新日期', + `sys_org_code` varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '所属部门', + `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '模版名称', + `code` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '模版编码', + `header` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL COMMENT '页眉', + `footer` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL COMMENT '页脚', + `main` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL COMMENT '主体内容', + `margins` varchar(100) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '页边距', + `width` int(11) NULL DEFAULT NULL COMMENT '宽度', + `height` int(11) NULL DEFAULT NULL COMMENT '高度', + `paper_direction` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '纸张方向 vertical纵向 horizontal横向', + `watermark` varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '水印', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci COMMENT = 'Word模版' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of eoa_word_template +-- ---------------------------- +INSERT INTO `aigc_word_template` VALUES ('1957327567174488065', 'admin', '2025-08-18 14:23:52', 'admin', '2025-12-31 17:03:13', 'A01', '红头文件', 'red_headed_document', '[]', '[]', '[{\"value\":\"\",\"font\":\"微软雅黑\",\"size\":29,\"bold\":false,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"\\n\",\"font\":\"楷体\",\"size\":29,\"bold\":false,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"国\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"炬\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"软\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"件\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"字\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"【\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"2\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"0\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"2\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"0\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"】\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"0\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"0\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"1\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"号\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"\\n\\n\",\"font\":\"楷体\",\"size\":34,\"bold\":true,\"color\":\"#FF0000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\"},{\"value\":\"\\n\",\"font\":\"仿宋\",\"size\":29,\"bold\":true,\"color\":\"#000000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"关\",\"font\":\"仿宋\",\"size\":29,\"bold\":true,\"color\":\"#000000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"于\",\"font\":\"仿宋\",\"size\":29,\"bold\":true,\"color\":\"#000000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"印\",\"font\":\"仿宋\",\"size\":29,\"bold\":true,\"color\":\"#000000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"发\",\"font\":\"仿宋\",\"size\":29,\"bold\":true,\"color\":\"#000000\",\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"center\",\"dashArray\":[]},{\"value\":\"\\n\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"主\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"题\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"词\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\":\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":14,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"\\n\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"抄\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"送\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\":\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"\\n\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":true,\"strikeout\":false,\"rowFlex\":\"left\",\"dashArray\":[]},{\"value\":\"\\n\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\"共\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\"印\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\"份\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\"(\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\"群\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\"发\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\")\",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]},{\"value\":\" \",\"font\":\"仿宋\",\"size\":21,\"bold\":true,\"italic\":false,\"underline\":false,\"strikeout\":false,\"rowFlex\":\"right\",\"dashArray\":[]}]', '[100,120,100,120]', 795, 1124, 'vertical', '{\"data\":\"\",\"color\":\"#AEB5C0\",\"opacity\":0.3,\"size\":200,\"font\":\"Microsoft YaHei\",\"repeat\":false,\"gap\":[10,10]}'); + + +-- author:wangshuai---date:20260123--for: 应用图像识别示例sql提交 --- +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('1996471445272088578', 'admin', '2025-12-04 14:47:40', 'admin', '2025-12-11 19:29:42', 'A06', NULL, '图像识别', NULL, NULL, 'chatFLow', '上传一张图片,我来为你识别图片的内容', '', NULL, '', '1904779811574784002', 'enable', 1, NULL, '[]', NULL, NULL, NULL, NULL, NULL, NULL); + +-- AI流程: 生成图表 +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-01-19 19:13:49', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = 'Chat2BI生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n## 能力\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n## 工作流程\\n1. **需求确认与澄清**:\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n* 用户可能要求你通过指定的数据源查询数据(具体的数据源列表从下表得知),若没有指定则不需要传数据源参数。\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n2. **数据获取**:\\n* 判断用户需求涉及的表是否在已知范围内。\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n* 构建查询SQL时,需要明确数据源的数据库类型,根据不同的数据库构建不同的SQL方言。\\n* 调用工具执行SQL,获取原始数据集。\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n3. **支持的图表类型**:\\n* `bar`: 柱状图\\n* `line`: 折线图、曲线图\\n* `pie`: 饼图\\n* `radar`: 雷达图\\n* `gauge`: 仪表盘\\n* `barline`: 折柱图\\n* `multibar`: 多列柱状图\\n* `multiline`: 多行折线图\\n* `area`: 面积图\\n4. **数据转换**:\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n* 数据转换时能直接转换就不要调用工具转换。\\n5. **结果封装与输出**:\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n* **双重校验**:\\n* **格式校验**:确保``标签首尾完整闭合。\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n## 输出格式\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n``` html\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n```\\n> 注:bar、line、pie为简单图表,可直接通过x、y来展示数据,而radar、gauge、barline、multibar、multiline、area为复杂图表,你需要先通过工具查询示例格式后,严格按照示例格式拼装`data`JSON;该工具支持逗号分割,你尽量一次性查询所有需要的图表示例格式。\\n## 限制\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryDataSourceInfoText`工具。\\n- 简单图表类型格式,或已经查询过的图表类型格式,严禁再次调用工具查询。\\n- 不要向用户提及`ghb-chart`标签以及图表格式相关信息。\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n- **格式严格性**:`ghb-chart`标签的前后必须严格保证有两个空行;必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键或示例数据中所需的键。\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因\\n## 默认数据源类型\\n{{defDbType}}\\n## 支持的数据源\\n{{allDbSource}}\\n\\n> 注意:以上就是所有的支持的数据源,禁止再次执行和`queryDataSourceInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; + +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-01-26 11:17:50', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = 'Chat2BI生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag +(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n\\n## 能力\\n\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n\\n## 工作流程\\n\\n1. **需求确认与澄清**:\\n\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n\\n* 用户可能要求你通过指定的数据源查询数据(具体的数据源列表从下表得知),若没有指定则不需要传数据源参数。\\n\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n\\n2. **数据获取**:\\n\\n* 判断用户需求涉及的表是否在已知范围内。\\n\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n\\n* 构建查询SQL时,需要明确数据源的数据库类型,根据不同的数据库构建不同的SQL方言。\\n\\n* 调用工具执行SQL,获取原始数据集。\\n\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n\\n3. **支持的图表类型**:\\n\\n* `bar`: 柱状图\\n\\n* `line`: 折线图、曲线图\\n\\n* `pie`: 饼图\\n\\n* `radar`: 雷达图\\n\\n* `gauge`: 仪表盘\\n\\n* `barline`: 折柱图\\n\\n* `multibar`: 多列柱状图\\n\\n* `multiline`: 多行折线图\\n\\n* `area`: 面积图\\n\\n4. **数据转换**:\\n\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n\\n* 数据转换时能直接转换就不要调用工具转换。\\n\\n5. **结果封装与输出**:\\n\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n\\n* **双重校验**:\\n\\n* **格式校验**:确保``标签首尾完整闭合。\\n\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n\\n## 输出格式\\n\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n\\n``` html\\n\\n\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n\\n\\n\\n```\\n\\n> 注:bar、line、pie为简单图表,可直接通过x、y来展示数据,而radar、gauge、barline、multibar、multiline、area为复杂图表,你需要先通过工具查询示例格式后,严格按照示例格式拼装`data`JSON;该工具支持逗号分割,你尽量一次性查询所有需要的图表示例格式。\\n\\n## 限制\\n\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryDataSourceInfoText`工具。\\n\\n- 简单图表类型格式,或已经查询过的图表类型格式,严禁再次调用工具查询。\\n\\n- 不要向用户提及`ghb-chart`标签以及图表格式相关信息。\\n\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n\\n- **格式严格性**:`ghb-chart`标签的前后必须严格保证有两个空行;必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键或示例数据中所需的键。\\n\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因\\n\\n## 默认数据源类型\\n\\n{{defDbType}}\\n\\n## 支持的数据源\\n\\n{{allDbSource}}\\n\\n> 注意:\\n\\n当用户未指定切换的数据源时,默认数据源应设为空。\\n\\n以上就是所有的支持的数据源,禁止再次执行和`queryDataSourceInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_1__add_aiapp_img_gen.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_1__add_aiapp_img_gen.sql new file mode 100644 index 0000000..bb469a7 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_1__add_aiapp_img_gen.sql @@ -0,0 +1,3 @@ +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2008090512835629057', 'admin', '2026-01-05 16:17:41', 'admin', '2026-01-26 10:36:57', 'A05A01A01', NULL, '绘画_示例', NULL, NULL, 'chatSimple', NULL, '# 角色:文生图创意引擎\n你是一位精通视觉艺术与AI绘画的创意引擎,能将抽象的文字描述转化为精准、高质量、富有艺术感的图像提示词。\n\n## 目标:\n根据用户提供的文字描述,生成可直接用于主流AI绘画模型(如Midjourney、Stable Diffusion、DALL-E)的详细、结构化、高成功率的提示词,以帮助用户高效获得理想的视觉作品。\n\n## 技能:\n1. **深度语义理解**:准确解析用户描述的意图、核心元素、氛围和情感。\n2. **视觉元素拆解与重构**:将抽象概念分解为具体的视觉构成要素(主体、环境、风格、构图、光影、材质等)。\n3. **提示词工程优化**:精通各类AI绘画模型的语法规则,熟练运用权重分配、负面提示、参数设置等技巧。\n4. **艺术风格知识库**:掌握从古典到现代,从写实到抽象的各种艺术流派、画家风格、电影摄影术语。\n5. **多方案生成与评估**:能针对同一需求提供不同侧重点的提示词变体,并简要说明其预期效果差异。\n\n## 工作流:\n1. **需求澄清与细化**:首先与用户确认其描述中的模糊点(如“好看”具体指什么风格?),并主动询问关键细节(如画幅比例、主要色彩倾向、是否包含特定艺术家风格)。\n2. **结构化提示词构建**:按照“主体描述 + 环境/背景 + 艺术风格/媒介 + 构图/视角 + 光照/色彩 + 画质/参数 + (负面提示)”的逻辑结构构建提示词。\n3. **优化与变体提供**:生成一个主推的、最符合描述的详细提示词。同时,提供1-2个在风格或侧重点上略有不同的变体选项,供用户选择或组合。\n4. **使用建议**:简要说明该提示词在目标平台(如Midjourney)中可能需要调整的参数建议(如 `--ar 16:9`, `--v 6.0`)。\n\n## 输出格式:\n请严格按照以下格式输出,使用清晰的标题和分点:\n\n**用户需求分析摘要:**\n- 核心主题:\n- 期望风格/氛围:\n- 关键视觉元素:\n- 已确认细节:\n\n**主推提示词 (适用于 Midjourney/Stable Diffusion):**\n`[完整的、结构化的英文提示词,包含必要的权重符号如 :: 和参数]`\n\n**提示词变体选项:**\n1. **[变体名称,如“更写实风格”]**:`[变体提示词]`\n * *效果说明:此变体侧重于...*\n2. **[变体名称,如“更抽象表现”]**:`[变体提示词]`\n * *效果说明:此变体侧重于...*\n\n**使用建议:**\n- **平台参数**:建议添加 `--ar [比例] --s [风格化值] --v [版本]` (根据分析给出具体建议)。\n- **调整建议**:如需更...效果,可尝试在提示词中加入“...”关键词;如需避免...,可在负面提示中添加“...”。\n\n## 限制:\n- **反幻觉校验**:所有基于事实的风格或元素引用需确保准确性(如“梵高风格”),若不确定具体特征,用“[需核实具体时期或作品特征]”标注。\n- **伦理与合规**:自动过滤涉及现实人物肖像权争议、暴力血腥、成人内容、特定商标版权等敏感描述。若用户需求涉及潜在风险,应引导至合规表达(如“一个风格化的卡通英雄形象”代替具体超级英雄)。\n- **聚焦提示词本身**:不生成实际图像,不解释AI绘画原理,所有输出必须围绕“生成更好的图像提示词”这一核心任务。\n- **清晰简洁**:在保证信息完整的前提下,提示词和说明应尽可能精炼,避免冗长堆砌关键词。', '1897481367743143938', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"},\"izDraw\":\"1\",\"drawModelId\":\"2008060119398899713\"}', NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +INSERT INTO `airag_model` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `provider`, `model_name`, `credential`, `base_url`, `model_type`, `model_params`, `activate_flag`) VALUES ('2008060119398899713', 'admin', '2026-01-05 14:16:55', 'admin', '2026-01-27 20:11:51', 'A05A01A01', NULL, '智普图片生成', 'ZHIPU', 'glm-image', '{\"apiKey\":\"76ca78587074479d8939a13\"}', 'https://open.bigmodel.cn', 'IMAGE', NULL, 1); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_2__add_aiwriteblog.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_2__add_aiwriteblog.sql new file mode 100644 index 0000000..4489664 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.1_2__add_aiwriteblog.sql @@ -0,0 +1 @@ +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2011769909807579138', 'admin', '2026-01-15 19:58:18', 'admin', '2026-01-20 18:01:01', 'A05A01A01', NULL, 'ghb', 'AI写作_示例', '', '', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'274870264732975104\')).to(\n THEN(\n llm.tag(\'274870308194353152\'),\n reply.tag(\'275189628369862656\'),\n end.tag(\'274870895589851136\')\n ).tag(\"274870308194353152\"),\n THEN(\n llm.tag(\'274870324602470400\'),\n reply.tag(\'275189722339049472\'),\n end.tag(\'274870677188247552\')\n ).tag(\"274870324602470400\")\n ).tag(\'274870264732975104\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":493,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false},{\"field\":\"type\",\"name\":\"类型\",\"type\":\"string\",\"required\":true}],\"outputParams\":[],\"width\":332,\"height\":90}},{\"id\":\"274870264732975104\",\"type\":\"switch\",\"x\":786,\"y\":506,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"type\",\"operator\":\"EQUALS\",\"value\":\"polish\",\"type\":\"string\"}],\"next\":\"274870308194353152\"}],\"else\":{\"next\":\"274870324602470400\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":116}},{\"id\":\"274870308194353152\",\"type\":\"llm\",\"x\":1239,\"y\":410.5,\"properties\":{\"text\":\"文章润色\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"## 角色:专业文章编辑\\n你是一位经验丰富、技巧高超的专业编辑,擅长根据用户提供的原始内容,进行深度优化和润色,提升文章的整体质量、可读性和影响力。\\n## 目标:\\n1. 精准理解用户原文的核心思想、目标受众和写作意图,严格保留原文所有内容(包括图片格式及图片相关描述,绝不删除、修改图片路径或调整图片位置)。\\n2. 运用专业的编辑技巧(如结构调整、语言润色、逻辑强化、亮点突出等)对文章进行全面改进,仅优化文字部分,不触碰任何图片相关元素。\\n3. 在润色过程中,严格参考用户原文的长度、语气、格式和语言风格,确保优化后的文章与原文风格一致、节奏一致、篇幅保持相近,不随意扩展或压缩内容。\\n4. 输出一篇在保持原意、保留全部原文内容(含图片)的基础上,更精炼、流畅、有力且符合目标场景的优化版本。\\n## 技能:\\n1. 深度理解与分析:能快速把握文章主旨、论点、论据和情感基调,识别原文的亮点、待改进之处,同时精准定位图片位置及相关关联内容,确保文字优化与图片适配。\\n2. 结构优化大师:擅长重组段落顺序、调整句间逻辑、优化叙事流程,使文章结构清晰、层层递进,调整过程中严格保留图片的原有位置及上下文关联。\\n3. 语言润色专家:拥有丰富的词汇储备和敏锐的语感,能消除冗余、修正语病、替换平淡词汇,提升文字的表现力和专业性,不改动任何图片格式及图片描述。\\n4. 风格适配能力:能根据文章类型(如学术论文、商业文案、创意故事等)和目标读者,调整并统一全文的语言风格,确保文字风格与图片内容协调统一。\\n5. 风格复刻能力:能精准识别并模仿用户原文的语气(正式、口语、幽默、客观、感性等)、格式(标题层级、段落结构、列表方式等)、语言习惯(用词偏好、句式特点、节奏风格),使润色后的文章在风格上与原文保持高度一致。\\n## 工作流:\\n1. 接收与分析:首先,我会请求用户提供需要改进的文章全文(含图片格式)。收到后,我将仔细阅读,分析其核心观点、目标读者、现有结构、语言风格及主要问题(如逻辑不清、表达啰嗦、重点模糊等),同时标注图片位置及关联文字,确保优化不影响图片呈现。\\n2. 风格与格式识别:在分析阶段,我会特别关注用户原文的长度、语气、格式和语言风格,并将其作为润色的重要参考依据,确保优化后的文章与原文风格一致。\\n3. 制定编辑方案:基于分析,规划具体的改进方向(如开篇优化、论点强化、案例润色、结论升华等),明确所有方案均不涉及图片删除、修改,仅针对文字部分调整,可向用户简要确认理解与改进思路。\\n4. 执行优化编辑:按照既定方案,逐部分优化文字内容,包括重写开头结尾、调整段落布局、精炼句子、丰富细节、增强逻辑连接词、统一术语和语气。全程严格保留原文中的所有图片格式、图片路径及图片位置,确保图片与优化后文字衔接自然。\\n5. 风格一致性检查:在完成文字优化后,我会进行一次风格一致性复核,确保文章的语气、格式、语言风格与用户原文保持一致,篇幅长度不出现显著偏差。\\n6. 输出与核对:最终呈现优化后的完整文章,再次核对确认所有图片元素均未改动,仅文字部分得到提升,确保原文内容(含图片)无遗漏、无篡改。\\n## 输出格式:\\n- 输出优化后的完整文章,全文保留原文所有图片格式、图片路径及图片位置,仅优化文字部分,图片相关内容与位置完全复刻原文。\\n## 限制:\\n1. 严格忠实于用户原文的核心事实、核心观点及所有内容(含图片),不得歪曲、杜撰信息,不得删除、修改任何图片格式、图片路径或图片位置。\\n2. 所有优化需基于可靠的写作与编辑原则,仅针对文字部分开展,避免主观臆断或个人风格过度强加,确保文字优化不影响图片的呈现及上下文关联。\\n3. 若遇到原文中存在事实模糊或逻辑硬伤之处,应在优化版本中通过 [建议核实] 或调整表述予以提示,而非擅自修改事实,同时不触碰图片相关元素。\\n4. 不添加与原文主题无关的内容或评价,不新增、删减图片,不调整图片排列顺序。\\n5. 润色过程中必须参考用户原文的长度、语气、格式和语言风格,不得随意改变原文的整体风格或篇幅结构。\"},{\"role\":\"user\",\"content\":\"{{content}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"content\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":205,\"remarks\":\"文章润色\"}},{\"id\":\"274870324602470400\",\"type\":\"llm\",\"x\":1240,\"y\":682.5,\"properties\":{\"text\":\"文章创作,图文格式\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"## 角色:精准内容与段落配图生成专家\\n\\n你是一位专业的内容生成助手,能够严格按照用户指定的格式、语气、长度和语言要求,直接输出精准匹配的最终内容,并为每个独立段落配上 1 张高相关度的图片。\\n\\n## 任务类型识别\\n\\n1. 回复类任务:当用户提供原始问题和参考回复时,仅基于给定内容生成精准回复,不得额外添加无关信息(如通知、背景介绍等)。\\n\\n2. 文章类任务:当用户提供主题时,撰写结构清晰、内容准确的完整文章,可包含引言、主体段落、总结等部分。\\n\\n## 目标\\n\\n1. **严格遵循指令**:完全按照用户指定的格式、语气、语言和长度要求生成内容。\\n\\n2. **直接输出结果**:仅输出符合要求的正文内容和对应的段落配图,不包含任何额外的标题、解释、道歉或中间过程。\\n\\n3. **逐段精准配图**:为每一个独立的段落匹配 1 张与该段内容强相关的图片,图片直接插入到对应段落的末尾,而非统一放在全文结尾。\\n\\n4. **适配两种模式**:既能独立创作短文并逐段配图,也能基于给定的原文和参考内容生成精准回复并逐段配图。\\n\\n\\n## 核心规则\\n\\n1. 严格匹配要求:必须完全遵循用户指定的格式、语气、长度和语言要求。\\n\\n## 技能\\n\\n1. **精准指令解析**:准确识别用户的创作模式(独立创作 / 回复)、格式(消息 / 邮件等)、语气(友善 / 专业等)、语言(中文 / 英文等)和长度(短 / 中 / 长)。\\n\\n2. **无冗余输出**:仅生成符合要求的正文内容,不添加任何指令外的信息。\\n\\n3. **独立创作能力**:针对独立创作需求,能围绕核心主题生成结构清晰、语言流畅的短文。\\n\\n4. **精准回复能力**:针对回复需求,能基于原文和参考内容生成精准匹配的简短回复。\\n\\n5. **逐段配图能力**:为每个独立段落提取精准关键词,调用search_photos(图片搜索工具)完成搜索,图片直接插入到对应段落的末尾。\\n\\n6. **避免搜索死循环**:每个图片仅使用 1-2 个精准关键词一次搜索完成,不反复调整关键词。\\n\\n7. **内容精准性**:回复类内容必须与参考内容完全一致,不得扩写;文章类内容必须准确、专业,不虚构事实。\\n\\n## 工作流(内部执行,不对外展示)\\n\\n1. **识别需求类型**:判断用户需求是独立创作短文,还是基于给定内容生成回复。\\n\\n2. **解析参数要求**:提取并确认格式、语气、语言、长度等所有约束条件。\\n\\n3. **生成精准内容**:\\n\\n - 独立创作:围绕核心主题,生成符合长度和语气要求的正文,并自然分段。\\n\\n - 回复:基于原文和参考内容,生成精准匹配的简短回复,并自然分段。\\n\\n4. **逐段匹配配图**:为每个独立段落提取 1-2 个与该段内容强相关的关键词,调用图片工具完成搜索。\\n\\n5. **整合输出**:将图片以路径的方式直接插入到对应段落的末尾,仅输出最终的图文内容,不包含任何额外信息或中间过程。\\n\\n## 限制\\n\\n- 必须等待 搜索图片 工具返回结果后,再将图片与文字内容整合输出,禁止在工具调用过程中提前生成最终回复。\\n\\n- 禁止输出标题、解释、过程或额外说明。\\n\\n- 只对重点内容配图,非重点内容不配图。\\n\\n- 图片必须与重点内容高度相关。\\n\\n- 每个重点内容只配 1 张图,避免重复搜索。\\n\\n- 图片插入在重点内容附近,不集中放在结尾。\\n\\n- 语言必须符合用户指定的要求。\"},{\"role\":\"user\",\"content\":\"{{content}}\"}],\"showToolExecution\":false,\"plugins\":[{\"pluginId\":\"1988208474780168193\",\"pluginName\":\"图片搜索\",\"category\":\"mcp\"}]},\"inputParams\":[{\"field\":\"content\",\"name\":\"content\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":205,\"remarks\":\"文章创作,图文格式\"}},{\"id\":\"274870677188247552\",\"type\":\"end\",\"x\":2189,\"y\":631,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{result}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"result\",\"nodeId\":\"274870324602470400\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":134}},{\"id\":\"274870895589851136\",\"type\":\"end\",\"x\":2149,\"y\":366,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{result}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"result\",\"nodeId\":\"274870308194353152\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":134}},{\"id\":\"275189628369862656\",\"type\":\"reply\",\"x\":1725,\"y\":364,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{content}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"content\",\"nodeId\":\"274870308194353152\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":112}},{\"id\":\"275189722339049472\",\"type\":\"reply\",\"x\":1725,\"y\":637,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{content}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"content\",\"nodeId\":\"274870324602470400\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":112}}],\"edges\":[{\"id\":\"274870264774918144\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"274870264732975104\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"274870264732975104_input\",\"pointsList\":[{\"x\":466,\"y\":479},{\"x\":566,\"y\":479},{\"x\":520,\"y\":479},{\"x\":620,\"y\":479}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274870308244684800\",\"type\":\"base-edge\",\"sourceNodeId\":\"274870264732975104\",\"targetNodeId\":\"274870308194353152\",\"sourceAnchorId\":\"274870264732975104_source_if\",\"targetAnchorId\":\"274870308194353152_input\",\"pointsList\":[{\"x\":952,\"y\":513},{\"x\":1052,\"y\":513},{\"x\":973,\"y\":339},{\"x\":1073,\"y\":339}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274870324673773568\",\"type\":\"base-edge\",\"sourceNodeId\":\"274870264732975104\",\"targetNodeId\":\"274870324602470400\",\"sourceAnchorId\":\"274870264732975104_source_else\",\"targetAnchorId\":\"274870324602470400_input\",\"pointsList\":[{\"x\":952,\"y\":539},{\"x\":1052,\"y\":539},{\"x\":974,\"y\":611},{\"x\":1074,\"y\":611}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"275189628491497472\",\"type\":\"base-edge\",\"sourceNodeId\":\"274870308194353152\",\"targetNodeId\":\"275189628369862656\",\"sourceAnchorId\":\"274870308194353152_output\",\"targetAnchorId\":\"275189628369862656_input\",\"pointsList\":[{\"x\":1405,\"y\":339},{\"x\":1505,\"y\":339},{\"x\":1459,\"y\":339},{\"x\":1559,\"y\":339}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"275189651216236544\",\"type\":\"base-edge\",\"sourceNodeId\":\"275189628369862656\",\"targetNodeId\":\"274870895589851136\",\"sourceAnchorId\":\"275189628369862656_output\",\"targetAnchorId\":\"274870895589851136_input\",\"pointsList\":[{\"x\":1891,\"y\":339},{\"x\":1991,\"y\":339},{\"x\":1883,\"y\":330},{\"x\":1983,\"y\":330}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"275189722511015936\",\"type\":\"base-edge\",\"sourceNodeId\":\"274870324602470400\",\"targetNodeId\":\"275189722339049472\",\"sourceAnchorId\":\"274870324602470400_output\",\"targetAnchorId\":\"275189722339049472_input\",\"pointsList\":[{\"x\":1406,\"y\":611},{\"x\":1506,\"y\":611},{\"x\":1459,\"y\":612},{\"x\":1559,\"y\":612}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"275189728907329536\",\"type\":\"base-edge\",\"sourceNodeId\":\"275189722339049472\",\"targetNodeId\":\"274870677188247552\",\"sourceAnchorId\":\"275189722339049472_output\",\"targetAnchorId\":\"274870677188247552_input\",\"pointsList\":[{\"x\":1891,\"y\":612},{\"x\":1991,\"y\":612},{\"x\":1923,\"y\":595},{\"x\":2023,\"y\":595}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"result\",\"nodeId\":\"274870308194353152\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"},{\"field\":\"type\",\"name\":\"类型\",\"required\":true,\"type\":\"string\"}]}', ''); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_0__all_upgrade.sql b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_0__all_upgrade.sql new file mode 100644 index 0000000..5f57768 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_0__all_upgrade.sql @@ -0,0 +1,437 @@ + +-- AI提示词、AI评估器示例 +INSERT INTO `airag_prompts` (`id`, `name`, `prompt_key`, `description`, `content`, `category`, `tags`, `model_id`, `model_param`, `status`, `version`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('2013923394830508034', '旅行规划师', 'travel_planner', '一位顶级的旅游规划师,合理规划用户出行安排', '# 角色:旅行规划师\n帮助用户轻松规划他们的旅行,提供个性化的旅行建议和行程安排。\n\n## 目标:\n1. 为用户设计符合其需求和偏好的旅行计划。\n2. 提供详细的行程安排,包括交通、住宿、景点等信息。\n\n## 技能:\n1. 精通旅游目的地的知识,能够提供最新的旅行资讯。\n2. 具备优秀的沟通能力,能够有效理解用户需求。\n3. 熟悉预算管理,能够提供性价比高的旅行选项。\n\n## 工作流:\n1. 收集用户的旅行需求和偏好,包括目的地、预算、出发时间等。\n2. 分析用户需求,制定个性化的旅行计划,包括行程安排和预算分配。\n3. 向用户提供完整的旅行计划,并根据反馈进行调整。 \n\n## 输出格式:\n以清晰的行程表形式输出,包括日期、活动安排、交通方式等信息。\n\n## 限制:\n- 不提供涉及违法或不合规活动的建议。\n- 尊重用户隐私,不询问不必要的个人信息。\n- 确保所有信息来源可靠,标注必要的参考资料。', NULL, NULL, '1897481367743143938', '{\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"},\"promptVariables\":\"\"}', '0', NULL, 0, 'admin', '2026-01-21 18:35:29', 'admin', '2026-01-22 10:06:04', 'A05A01', NULL); +INSERT INTO `airag_prompts` (`id`, `name`, `prompt_key`, `description`, `content`, `category`, `tags`, `model_id`, `model_param`, `status`, `version`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('2013938349776609282', '需求采集器', 'product_requirement', '一位资深的 IT 咨询顾问,可以与客户进行初步沟通,并结构化地记录下他们的核心需求,并根据用户提供的项目基本信息生成一份标准化的需求采集纪要。', '你是一位资深的 IT 咨询顾问,你的任务是与客户进行初步沟通,并结构化地记录下他们的核心需求。请根据用户提供的项目基本信息,生成一份标准化的需求采集纪要。\n\n**工作准则**:\n1. **结构化输出**:严格按照 JSON 格式输出,包含项目背景、核心痛点、期望目标和关键干系人四个部分。\n2. **提炼关键信息**:从用户的零散描述中,精准提炼出关键信息,并以专业、简洁的语言进行归纳。\n3. **补充待办事项**:根据需求信息,自动生成 3-5 个需要进一步澄清或跟进的问题,放入 `follow_up_questions` 字段。\n4. **保持客观**:仅记录和分析用户提供的信息,不添加主观臆断。\n\n**客户信息**:\n- 客户公司:`{{client_company}}`\n- 项目名称:`{{project_name}}`\n- 初步描述:`{{initial_description}}`', NULL, NULL, '1897481367743143938', '{\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"},\"temperature\":0.7,\"timeout\":60}', '0', NULL, 0, 'admin', '2026-01-21 19:34:54', 'admin', '2026-01-21 19:48:45', 'A05A01', NULL); +INSERT INTO `airag_prompts` (`id`, `name`, `prompt_key`, `description`, `content`, `category`, `tags`, `model_id`, `model_param`, `status`, `version`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) VALUES ('2014166382018056194', '文章生成器', 'article_generator', '一位资深的文章生成顾问,可以根据用户提供的基本信息生成一份标准化的文章。', '## 角色:精准内容与段落配图生成专家\n你是一位专业的内容生成助手,能够严格按照用户指定的格式、语气、长度和语言要求,直接输出精准匹配的最终内容,并为每个独立段落配上 1 张高相关度的图片。\n## 任务类型识别\n1. 回复类任务:当用户提供原始问题和参考回复时,仅基于给定内容生成精准回复,不得额外添加无关信息(如通知、背景介绍等)。\n2. 文章类任务:当用户提供主题时,撰写结构清晰、内容准确的完整文章,可包含引言、主体段落、总结等部分。\n## 目标\n1. **严格遵循指令**:完全按照用户指定的格式、语气、语言和长度要求生成内容。\n2. **直接输出结果**:仅输出符合要求的正文内容和对应的段落配图,不包含任何额外的标题、解释、道歉或中间过程。\n3. **逐段精准配图**:为每一个独立的段落匹配 1 张与该段内容强相关的图片,图片直接插入到对应段落的末尾,而非统一放在全文结尾。\n4. **适配两种模式**:既能独立创作短文并逐段配图,也能基于给定的原文和参考内容生成精准回复并逐段配图。\n## 核心规则\n1. 严格匹配要求:必须完全遵循用户指定的格式、语气、长度和语言要求。\n\n## 技能\n1. **精准指令解析**:准确识别用户的创作模式(独立创作 / 回复)、格式(消息 / 邮件等)、语气(友善 / 专业等)、语言(中文 / 英文等)和长度(短 / 中 / 长)。\n2. **无冗余输出**:仅生成符合要求的正文内容,不添加任何指令外的信息。\n3. **独立创作能力**:针对独立创作需求,能围绕核心主题生成结构清晰、语言流畅的短文。\n4. **精准回复能力**:针对回复需求,能基于原文和参考内容生成精准匹配的简短回复。\n5. **逐段配图能力**:为每个独立段落提取精准关键词,调用图片工具完成搜索,图片直接插入到对应段落的末尾。\n6. **避免搜索死循环**:每个图片仅使用 1-2 个精准关键词一次搜索完成,不反复调整关键词。\n7. **内容精准性**:回复类内容必须与参考内容完全一致,不得扩写;文章类内容必须准确、专业,不虚构事实。\n## 工作流(内部执行,不对外展示)\n1. **识别需求类型**:判断用户需求是独立创作短文,还是基于给定内容生成回复。\n2. **解析参数要求**:提取并确认格式、语气、语言、长度等所有约束条件。\n3. **生成精准内容**:\n - 独立创作:围绕核心主题,生成符合长度和语气要求的正文,并自然分段。\n - 回复:基于原文和参考内容,生成精准匹配的简短回复,并自然分段。\n4. **逐段匹配配图**:为每个独立段落提取 1-2 个与该段内容强相关的关键词,调用图片工具完成搜索。\n5. **整合输出**:将图片以路径的方式直接插入到对应段落的末尾,仅输出最终的图文内容,不包含任何额外信息或中间过程。\n## 限制\n- 禁止输出标题、解释、过程或额外说明。\n- 只对重点内容配图,非重点内容不配图。\n- 图片必须与重点内容高度相关。\n- 每个重点内容只配 1 张图,避免重复搜索。\n- 图片插入在重点内容附近,不集中放在结尾。\n- 语言必须符合用户指定的要求。', NULL, NULL, '1897481367743143938', '{\"promptVariables\":\"\",\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"}}', '0', NULL, 0, 'admin', '2026-01-22 10:41:02', 'admin', '2026-01-22 10:43:34', 'A05A01', NULL); +INSERT INTO `airag_ext_data` (`id`, `biz_type`, `name`, `descr`, `tags`, `data_value`, `status`, `dataset_value`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `version`) VALUES ('2013925759721709570', 'evaluator', '相关性', '输出是否引用了文本中的真实引用', NULL, '您是一位专业的数据标注员,负责评估模型输出是否引用了所提供文本中的真实引语。您的任务是根据以下评分标准给出评分:\n<评分标准>\n 正确引用真实引语的提交内容应:\n - 准确指出文本中实际存在的引语。\n - 以与文本中完全一致的措辞呈现引语,或者进行恰当的意译,且能清晰地对应到文本的特定部分。\n - 不编造或错误归属引语。\n\n 在打分时,您应该扣除分数的情况包括:\n - 提及文本中不存在的引语。\n - 错误引用或歪曲现有引语的内容。\n - 声称有引语,但在文本中找不到对应的部分。\n\n\n<指导说明>\n - 仔细阅读输入的问题、模型的输出以及参考文本。\n - 将输出中引用的引语与参考文本的内容进行对比。\n - 确认引语引用准确且能追溯到文本中。\n\n\n<提醒>\n 目标是评估提交内容是否准确引用了文本中的真实引语。\n\n\n{{input}}\n\n{{output}}\n\n使用下面的参考输出来帮助你评估响应的正确性:\n{{reference}}', 'completed', '{\"columns\":[{\"id\":\"mknwmv3o0f1dg2wtyymu\",\"name\":\"input\",\"description\":\"作为输入投递给评测对象\",\"dataType\":\"String\",\"required\":false},{\"id\":\"mknwmv3om7kd0x0axz\",\"name\":\"reference_output\",\"description\":\"预期理想输出,可作为评估时的参考标准\",\"dataType\":\"String\",\"required\":false}],\"dataSource\":[{\"id\":\"mkoumpl32mh827mg5b4\",\"input\":\"低代码与零代码的区别,并列举相关优秀的产品\",\"reference_output\":\"ghbboot是低代码产品中最流行的产品之一\"}]}', '{\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"},\"modelId\":\"1897481367743143938\"}', 'admin', '2026-01-21 18:44:53', 'admin', '2026-01-22 11:10:02', 'A05A01', NULL, NULL); +INSERT INTO `airag_ext_data` (`id`, `biz_type`, `name`, `descr`, `tags`, `data_value`, `status`, `dataset_value`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `version`) VALUES ('2013934773104844801', 'evaluator', '简洁性', '输出内容是否简洁', NULL, '您是一位专业的数据标注员,负责评估模型输出的简洁性。您的任务是根据以下评分标准给出评分:\n<评分标准>\n 完美简洁的答案应当:\n - 仅包含被请求的确切信息。\n - 使用最少的词汇来传达完整的答案。\n - 省略客套话、模棱两可的表述和不必要的背景信息。\n - 不包含关于答案或模型能力的元评论。\n - 避免冗余信息或重复表述。\n - 除非明确要求,否则不包含解释内容。\n\n 在打分时,您应该扣除分数的情况有:\n - 诸如“我认为”“我觉得”或“答案是”之类的引导性短语。\n - 像“可能”“大概”或“据我所知”这样的模糊表述。\n - 不必要的背景或上下文信息。\n - 未被要求的解释内容。\n - 跟进问题或提供更多信息的提议。\n - 冗余信息或重复表述。\n - 像“希望这有帮助”或“如果您还需要其他信息请告诉我”这样的礼貌用语。\n\n\n<指导说明>\n - 仔细阅读输入的问题和模型的输出。\n - 全面检查输出中是否存在任何不必要的元素,尤其是上述<评分标准>中提到的那些。\n - 分数应反映输出在多大程度上遵循了评分标准,即仅包含所请求的必要信息。\n\n\n<提醒>\n 目标是奖励那些提供完整答案且无任何多余信息的回复。\n\n\n<输入>\n{{input}}\n\n\n<输出>\n{{output}}\n', 'completed', '{\"columns\":[{\"id\":\"mkos4jtjgvma0wfx0jj\",\"name\":\"input\",\"description\":\"作为输入投递给评测对象\",\"dataType\":\"String\",\"required\":false},{\"id\":\"mkos4jtjrfci6zhots\",\"name\":\"reference_output\",\"description\":\"预期理想输出,可作为评估时的参考标准\",\"dataType\":\"String\",\"required\":false}],\"dataSource\":[{\"id\":\"mkou9jjw7esctgvxkjb\",\"input\":\"客户提出新的需求包括考勤统计优化、聊天添加语音功能、聊天记录,展示已读未读消息等\",\"reference_output\":\"整理客户需求,要求简洁\"}]}', '{\"modelId\":\"1897481367743143938\",\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"}}', 'admin', '2026-01-21 19:20:42', 'admin', '2026-01-22 10:36:07', 'A05A01', NULL, NULL); +INSERT INTO `airag_ext_data` (`id`, `biz_type`, `name`, `descr`, `tags`, `data_value`, `status`, `dataset_value`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `version`) VALUES ('2013935404041412609', 'evaluator', '正确性', '提交的内容是否正确、准确、真实', NULL, '您是一位专业的数据标注员,负责评估模型输出的正确性。您的任务是根据以下评分标准给出评分:\n<评分标准>\n 正确的答案应当:\n - 提供准确且完整的信息\n - 不包含事实性错误\n - 回答问题的所有部分\n - 逻辑上保持一致\n - 使用精确和准确的术语\n\n 在打分时,您应该进行扣分的情况包括:\n - 事实性错误或不准确的信息\n - 不完整或部分的答案\n - 具有误导性或模糊不清的陈述\n - 错误的术语使用\n - 逻辑不一致\n - 缺失关键信息\n\n\n<指导说明>\n - 仔细阅读输入的问题和模型的输出。\n - 将输出与参考输出进行对比,以检查事实的准确性和完整性。\n - 重点关注输出中所呈现信息的正确性,而非其风格或冗长程度。\n\n\n<提醒>\n 目标是评估回复的事实正确性和完整性。\n\n\n<输入>\n{{input}}\n\n\n<输出>\n{{output}}\n\n\n<参考输出>\n{{reference_output}}\n', 'completed', '{\"columns\":[{\"id\":\"mknyouj31m060qn09us\",\"name\":\"input\",\"description\":\"作为输入投递给评测对象\",\"dataType\":\"String\",\"required\":true},{\"id\":\"mknyouj3sjssa90a5uo\",\"name\":\"reference_output\",\"description\":\"预期理想输出,可作为评估时的参考标准\",\"dataType\":\"String\",\"required\":true}],\"dataSource\":[{\"id\":\"mkosu9w5irflh4356kl\",\"input\":\"帮我规划上海三日游,这周末出发。\",\"reference_output\":\"正确输出地点为上海的三日游规划,包括景点攻略\"}]}', '{\"modelId\":\"1897481367743143938\",\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"}}', 'admin', '2026-01-21 19:23:12', 'admin', '2026-01-22 10:07:51', 'A05A01', NULL, NULL); +INSERT INTO `airag_ext_data` (`id`, `biz_type`, `name`, `descr`, `tags`, `data_value`, `status`, `dataset_value`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `version`) VALUES ('2014158034904305665', 'track', NULL, NULL, NULL, '{\"id\":\"mkosu9w5irflh4356kl\",\"input\":\"帮我规划上海三日游,这周末出发。\",\"reference_output\":\"正确输出地点为上海的三日游规划,包括景点攻略\",\"userQuery\":\"帮我规划上海三日游,这周末出发。\",\"promptAnswer\":\"### 上海三日游行程规划(周末出发) \\n**出发时间**:本周末(假设周五晚或周六早上抵达上海) \\n**预算范围**:中等预算(人均约1500-2000元,不含往返大交通) \\n**主题推荐**:经典地标+文化体验+美食探索 \\n\\n---\\n\\n### **行程总览**\\n| 日期 | 上午活动 | 下午活动 | 晚上活动 | 住宿推荐 |\\n|------------|--------------------------|--------------------------|--------------------------|----------------------|\\n| **第一天** | 外滩+南京路步行街 | 豫园+城隍庙 | 黄浦江夜游 | 南京东路附近经济型酒店 |\\n| **第二天** | 上海博物馆 | 新天地+田子坊 | 上海中心大厦观光 | 同第一天住宿 |\\n| **第三天** | 迪士尼小镇(或武康路) | 陆家嘴金融区+东方明珠 | 返程 | — |\\n\\n---\\n\\n### **详细行程安排**\\n#### **第一天:经典地标与老城风情**\\n- **上午(9:00-12:00)** \\n - **外滩**:欣赏万国建筑群,眺望陆家嘴天际线(免费)。 \\n - **南京路步行街**:步行至南京路,体验商业街氛围,推荐老字号“沈大成”品尝糕点。 \\n- **中午(12:00-13:30)** \\n - **午餐**:南京路附近的“老正兴菜馆”(本帮菜,人均80-120元)。 \\n- **下午(14:00-17:00)** \\n - **豫园**:游览明代园林(门票40元),感受江南园林艺术。 \\n - **城隍庙**:逛传统集市,品尝南翔小笼包(人均30元)。 \\n- **晚上(18:30-20:30)** \\n - **黄浦江夜游**:从十六铺码头乘船游览(门票120元/人),欣赏两岸夜景。 \\n- **交通**:全天以地铁为主(推荐购买“上海公共交通卡”或使用支付宝乘车码)。 \\n\\n#### **第二天:文化与现代融合**\\n- **上午(9:30-12:30)** \\n - **上海博物馆**(免费,需提前预约):参观青铜器、书画等藏品(人民广场站)。 \\n- **中午(12:30-14:00)** \\n - **午餐**:博物馆附近“大壶春”生煎(人均30元)。 \\n- **下午(14:30-17:30)** \\n - **新天地**:石库门建筑改造的时尚街区,适合拍照。 \\n - **田子坊**:文艺小巷,逛创意小店、咖啡馆。 \\n- **晚上(18:30-21:00)** \\n - **上海中心大厦观光厅**(门票180元):登中国第一高楼俯瞰夜景。 \\n- **交通**:地铁1号线、10号线覆盖主要景点。 \\n\\n#### **第三天:自然与都市风光**\\n- **选项A(适合亲子/乐园爱好者)** \\n - **上午**:迪士尼小镇(免费进入,可购物拍照,地铁11号线直达)。 \\n - **下午**:陆家嘴环形天桥+东方明珠外观拍照。 \\n- **选项B(适合文艺爱好者)** \\n - **上午**:武康路历史街区,打卡武康大楼,逛特色咖啡馆。 \\n - **下午**:陆家嘴参观上海海洋水族馆(门票160元)或环球金融中心观光厅。 \\n- **晚上**:根据返程时间安排,推荐陆家嘴“正大广场”用餐后返程。 \\n\\n---\\n\\n### **预算分配参考(人均)**\\n| 项目 | 费用估算 |\\n|------------|-------------------|\\n| 住宿 | 600元(两晚经济型酒店) |\\n| 餐饮 | 400元 |\\n| 门票 | 400元 |\\n| 市内交通 | 100元 |\\n| 其他 | 100元 |\\n\\n---\\n\\n### **注意事项**\\n1. **预约提醒**:上海博物馆、热门餐厅建议提前1-2天预约。 \\n2. **天气准备**:本周末上海多云,气温18-25℃,建议携带薄外套和雨具。 \\n3. **交通提示**:地铁运营至23:00左右,夜间可打车(推荐使用滴滴)。 \\n\\n如果需要更详细的餐厅推荐、小众景点或调整行程节奏,请随时告诉我! 🌆\",\"answerScore\":\"得分:0.8\\n原因:模型输出提供了上海三日游的详细规划,包含了行程总览、详细安排、预算和注意事项,信息准确且逻辑一致。然而,参考输出仅要求“正确输出地点为上海的三日游规划,包括景点攻略”,模型输出虽然满足要求,但可能过于详细,且部分内容(如预算、天气)超出了“景点攻略”的严格范围,但未出现事实错误或不准确信息。因此,应该给出的分数是0.8。\"}', 'completed', NULL, '2013935404041412609', NULL, '2026-01-22 10:07:51', NULL, '2026-01-22 10:07:51', NULL, NULL, 1); +INSERT INTO `airag_ext_data` (`id`, `biz_type`, `name`, `descr`, `tags`, `data_value`, `status`, `dataset_value`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `version`) VALUES ('2014165148687159297', 'track', NULL, NULL, NULL, '{\"id\":\"mkou9jjw7esctgvxkjb\",\"input\":\"客户提出新的需求包括考勤统计优化、聊天添加语音功能、聊天记录,展示已读未读消息等\",\"reference_output\":\"整理客户需求,要求简洁\",\"userQuery\":\"客户提出新的需求包括考勤统计优化、聊天添加语音功能、聊天记录,展示已读未读消息等\",\"promptAnswer\":\"```json\\n{\\n \\\"项目背景\\\": \\\"客户公司计划对现有系统进行功能升级,主要涉及考勤管理和即时通讯两个核心模块的优化与增强。\\\",\\n \\\"核心痛点\\\": [\\n \\\"现有考勤统计功能可能效率低下或数据展示不够直观,无法满足精细化管理需求。\\\",\\n \\\"即时通讯功能单一,缺乏语音消息、聊天记录管理以及消息状态(已读/未读)追踪等现代通讯应用的基础能力。\\\"\\n ],\\n \\\"期望目标\\\": [\\n \\\"优化考勤统计功能,提升数据处理效率与报表展示的清晰度、实用性。\\\",\\n \\\"为即时通讯模块增加发送/接收语音消息的功能。\\\",\\n \\\"实现聊天记录的本地或云端存储与便捷检索。\\\",\\n \\\"在聊天界面中清晰展示消息的已读或未读状态。\\\"\\n ],\\n \\\"关键干系人\\\": [\\n \\\"人力资源部门(考勤统计功能的主要使用者)\\\",\\n \\\"全体需要使用即时通讯功能的员工\\\",\\n \\\"系统管理员(负责功能配置与维护)\\\",\\n \\\"项目决策与管理人员\\\"\\n ],\\n \\\"follow_up_questions\\\": [\\n \\\"考勤统计优化具体指哪些方面?例如,是希望生成更丰富的报表(如迟到早退分析、加班统计)、提升统计速度,还是与请假、出差等流程做更深度整合?\\\",\\n \\\"对于语音功能,需要明确技术细节:是否支持实时对讲?语音消息是否有时长限制?是否需要支持播放进度控制、语音转文字等附加功能?\\\",\\n \\\"聊天记录是希望永久保存,还是可设置自动清理周期?存储方案是倾向于本地设备存储,还是公司统一的云端存储?是否需要支持跨设备同步?\\\",\\n \\\"消息的“已读未读”状态,是否需要更细化的功能,如“已读回执”、“对方正在输入...”提示,或针对群聊的“部分成员已读”状态显示?\\\",\\n \\\"这些新功能是否有明确的优先级或期望的上线时间表?是否有相关的预算范围或技术平台限制(如必须兼容特定移动端或浏览器)?\\\"\\n ]\\n}\\n```\",\"answerScore\":\"得分:0.0\\n原因:输出完全不符合简洁性要求。它包含了大量未被请求的背景信息(如“项目背景”、“核心痛点”、“期望目标”、“关键干系人”)、解释性内容以及跟进问题(“follow_up_questions”)。回答没有直接提供被请求的确切信息,而是进行了扩展分析和提问,使用了大量不必要的词汇和结构。因此,应该给出的分数是0.0。\"}', 'completed', NULL, '2013934773104844801', NULL, '2026-01-22 10:36:07', NULL, '2026-01-22 10:36:07', NULL, NULL, 1); +INSERT INTO `airag_ext_data` (`id`, `biz_type`, `name`, `descr`, `tags`, `data_value`, `status`, `dataset_value`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `version`) VALUES ('2014173681512108034', 'track', NULL, NULL, NULL, '{\"id\":\"mkoumpl32mh827mg5b4\",\"input\":\"低代码与零代码的区别,并列举相关优秀的产品\",\"reference_output\":\"ghbboot是低代码产品中最流行的产品之一\",\"userQuery\":\"低代码与零代码的区别,并列举相关优秀的产品\",\"promptAnswer\":\"低代码和零代码平台都旨在降低软件开发的门槛,但它们面向的用户群体和技术实现方式有所不同。\\n\\n**低代码平台** 主要面向有一定技术背景的专业开发者或业务技术人员。它通过提供可视化的拖拽组件和模型驱动的逻辑来构建应用,但**仍然允许开发者直接编写代码**来处理复杂的业务逻辑、集成或自定义功能。这种方式在提升开发速度的同时,保留了高度的灵活性和扩展性。例如,开发者可以为一个电商应用快速搭建出用户界面和订单流程,再通过编写代码来实现一个独特的推荐算法或与某个特定老旧系统的深度集成。\\n\\n![低代码平台开发界面](https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80)\\n\\n**零代码平台** 则完全面向**业务人员或完全没有编程经验的用户**。它的目标是让用户通过纯粹的图形化配置,如表单设计、流程编排和规则设定,就能构建出满足特定需求的应用,**整个过程无需编写任何代码**。这极大地加速了简单业务流程的数字化,例如,人力资源部门的同事可以自己搭建一个请假审批流程或一个活动报名系统。\\n\\n![零代码平台操作示意](https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&auto=format&fit=crop&w-800&q=80)\\n\\n**核心区别总结**:\\n1. **目标用户**:低代码主要服务于“平民开发者”或专业开发者;零代码则服务于纯粹的业务人员。\\n2. **技术要求**:低代码需要基本的编程思维或技能;零代码几乎无技术门槛。\\n3. **灵活性**:低代码可通过编码实现高度定制;零代码的能力受限于平台预置的功能模块。\\n4. **应用场景**:低代码适用于复杂的企业级应用;零代码更擅长解决部门级的、流程固定的轻量级应用。\\n\\n**相关优秀产品列举**:\\n* **低代码平台代表**:\\n * **Mendix**:企业级低代码平台的领导者,功能全面,支持高度复杂的应用开发。\\n * **OutSystems**:另一家头部厂商,以高性能、高集成能力和快速开发著称。\\n * **微软 Power Apps**:背靠微软生态,与Office 365、Dynamics 365等产品无缝集成,易用性强。\\n* **零代码平台代表**:\\n * **Airtable**:以智能表格为基础,融合了数据库的强大功能和表格的易用性,适合管理多种业务数据。\\n * **钉钉宜搭 / 飞书多维表格**:国内协同办公平台内置的零代码工具,与办公场景深度结合,上手极快。\\n * **简道云**:国内知名的零代码应用搭建平台,在数据收集、流程管理和报表展示方面表现突出。\\n\\n![企业级软件平台对比](https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80)\",\"answerScore\":\"得分:0.0\\n原因:模型输出中完全没有引用参考文本中提供的引语“ghbboot是低代码产品中最流行的产品之一”。模型列举了其他低代码产品,但未提及参考文本中的特定内容,因此未能准确引用文本中实际存在的引语。因此,应该给出的分数是0.0。\"}', 'completed', NULL, '2013925759721709570', NULL, '2026-01-22 11:10:02', NULL, '2026-01-22 11:10:01', NULL, NULL, 1); + + +-- 图片解析回复json升级sql +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2014623894994300930', 'admin', '2026-01-23 16:59:01', 'admin', '2026-01-23 17:19:31', 'A04', '1', 'ghb', '示例_聊天_OCR', '', 'https://ghbdev.oss-cn-beijing.aliyuncs.com/temp/1dataOCR_1743065089791.png', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'167880707187527680\')).to(\n end.tag(\'167880856269869056\'),\n THEN(\n code_167881149430747136.tag(\'code_167881149430747136\'),\n llm.tag(\'167881839356006400\'),\n end.tag(\'167880661561888768\')\n ).tag(\"code_167881149430747136\")\n ).tag(\'167880707187527680\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":421,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"167880661561888768\",\"type\":\"end\",\"x\":1474,\"y\":364,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{data}}\",\"outputType\":\"text\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"}],\"height\":136,\"width\":332}},{\"id\":\"167880707187527680\",\"type\":\"switch\",\"x\":681,\"y\":233,\"properties\":{\"text\":\"条件分支\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"images\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"picture\"}],\"next\":\"167880856269869056\"}],\"else\":{\"next\":\"code_167881149430747136\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"height\":118,\"width\":332}},{\"id\":\"167880856269869056\",\"type\":\"end\",\"x\":1207,\"y\":207,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":true,\"outputContent\":\"{\\n    \\\"message\\\": \\\"请提供图片\\\"\\n  }\",\"outputType\":\"text\"},\"inputParams\":[],\"outputParams\":[],\"height\":114,\"width\":332}},{\"id\":\"code_167881149430747136\",\"type\":\"code\",\"x\":937,\"y\":459,\"properties\":{\"text\":\"脚本执行\",\"options\":{\"codeType\":\"groovy\",\"code\":\"def main(Map params) {\\n def newQuestion = params.question\\n if (!params.question) {\\n newQuestion = \\\"从图片中提取文字\\\"\\n }\\n return [result: newQuestion]\\n}\\n\"},\"inputParams\":[{\"field\":\"content\",\"name\":\"question\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}},{\"id\":\"167881839356006400\",\"type\":\"llm\",\"x\":1318,\"y\":606,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:OCR工具\\n作为一个智能OCR工具,你的主要职责是从图片中提取文字并将其输出为结构化数据。\\n## 目标:\\n1. 精确识别和提取图片中的文字信息。\\n2. 将提取的文字转换为结构化数据格式。\\n## 技能:\\n1. 高效的图像处理能力。\\n2. 精确的文字识别算法。\\n3. 数据格式化与输出能力。\\n## 工作流:\\n1. 输入图片,进行预处理(如去噪、二值化)。\\n2. 应用OCR算法识别图片中的文字,并记录识别结果。\\n3. 将识别的文字整理成结构化数据格式,如JSON或CSV。\\n## 输出格式:\\n\\n当前图中的内容为: ```提取的内容```\\n## 限制:\\n- 仅限于合法和合规的图片内容提取。\\n- 不得保存用户上传的图片数据。\\n- 需确保输出的数据准确无误,标注所有数据来源。\\n- 输出必须严格符合上述格式,字段名和层级结构不得随意更改。\"},{\"role\":\"user\",\"content\":\"{{question}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"images\",\"name\":\"images\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"picture\"},{\"field\":\"result\",\"name\":\"question\",\"nodeId\":\"code_167881149430747136\",\"customValue\":\"\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}}],\"edges\":[{\"id\":\"167880707195916288\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"167880707187527680\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"167880707187527680_input\",\"pointsList\":[{\"x\":466,\"y\":406},{\"x\":566,\"y\":406},{\"x\":415,\"y\":205},{\"x\":515,\"y\":205}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167880856274063360\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"167880856269869056\",\"sourceAnchorId\":\"167880707187527680_source_if\",\"targetAnchorId\":\"167880856269869056_input\",\"pointsList\":[{\"x\":847,\"y\":239},{\"x\":947,\"y\":239},{\"x\":941,\"y\":181},{\"x\":1041,\"y\":181}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167881149434941440\",\"type\":\"base-edge\",\"sourceNodeId\":\"167880707187527680\",\"targetNodeId\":\"code_167881149430747136\",\"sourceAnchorId\":\"167880707187527680_source_else\",\"targetAnchorId\":\"code_167881149430747136_input\",\"pointsList\":[{\"x\":847,\"y\":265},{\"x\":947,\"y\":265},{\"x\":671,\"y\":411},{\"x\":771,\"y\":411}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167881839356006401\",\"type\":\"base-edge\",\"sourceNodeId\":\"code_167881149430747136\",\"targetNodeId\":\"167881839356006400\",\"sourceAnchorId\":\"code_167881149430747136_output\",\"targetAnchorId\":\"167881839356006400_input\",\"pointsList\":[{\"x\":1103,\"y\":411},{\"x\":1203,\"y\":411},{\"x\":1052,\"y\":547},{\"x\":1152,\"y\":547}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"167882293611712512\",\"type\":\"base-edge\",\"sourceNodeId\":\"167881839356006400\",\"targetNodeId\":\"167880661561888768\",\"sourceAnchorId\":\"167881839356006400_output\",\"targetAnchorId\":\"167880661561888768_input\",\"pointsList\":[{\"x\":1484,\"y\":547},{\"x\":1584,\"y\":547},{\"x\":1208,\"y\":327},{\"x\":1308,\"y\":327}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"field\":\"text\",\"name\":\"data\",\"nodeId\":\"167881839356006400\"},{\"field\":\"outputText\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); +UPDATE `airag_app` SET `flow_id` = '2014623894994300930' WHERE `id` = '1996471445272088578'; + +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-01-26 11:17:50', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = 'Chat2BI生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag +(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n\\n## 能力\\n\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n\\n## 工作流程\\n\\n1. **需求确认与澄清**:\\n\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n\\n* 用户可能要求你通过指定的数据源查询数据(具体的数据源列表从下表得知),若没有指定则不需要传数据源参数。\\n\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n\\n2. **数据获取**:\\n\\n* 判断用户需求涉及的表是否在已知范围内。\\n\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n\\n* 构建查询SQL时,需要明确数据源的数据库类型,根据不同的数据库构建不同的SQL方言。\\n\\n* 调用工具执行SQL,获取原始数据集。\\n\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n\\n3. **支持的图表类型**:\\n\\n* `bar`: 柱状图\\n\\n* `line`: 折线图、曲线图\\n\\n* `pie`: 饼图\\n\\n* `radar`: 雷达图\\n\\n* `gauge`: 仪表盘\\n\\n* `barline`: 折柱图\\n\\n* `multibar`: 多列柱状图\\n\\n* `multiline`: 多行折线图\\n\\n* `area`: 面积图\\n\\n4. **数据转换**:\\n\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n\\n* 数据转换时能直接转换就不要调用工具转换。\\n\\n5. **结果封装与输出**:\\n\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n\\n* **双重校验**:\\n\\n* **格式校验**:确保``标签首尾完整闭合。\\n\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n\\n## 输出格式\\n\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n\\n``` html\\n\\n\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n\\n\\n\\n```\\n\\n> 注:bar、line、pie为简单图表,可直接通过x、y来展示数据,而radar、gauge、barline、multibar、multiline、area为复杂图表,你需要先通过工具查询示例格式后,严格按照示例格式拼装`data`JSON;该工具支持逗号分割,你尽量一次性查询所有需要的图表示例格式。\\n\\n## 限制\\n\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryDataSourceInfoText`工具。\\n\\n- 简单图表类型格式,或已经查询过的图表类型格式,严禁再次调用工具查询。\\n\\n- 不要向用户提及`ghb-chart`标签以及图表格式相关信息。\\n\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n\\n- **格式严格性**:`ghb-chart`标签的前后必须严格保证有两个空行;必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键或示例数据中所需的键。\\n\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因\\n\\n## 默认数据源类型\\n\\n{{defDbType}}\\n\\n## 支持的数据源\\n\\n{{allDbSource}}\\n\\n> 注意:\\n\\n当用户未指定切换的数据源时,默认数据源应设为空。\\n\\n以上就是所有的支持的数据源,禁止再次执行和`queryDataSourceInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; + + + +-- word生成 还是提示json格式不对 --- +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'215734195065536512\'),\n enhanceJava.tag(\'215740280715427840\'),\n end.tag(\'215735188368998400\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":404,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"个人简介\",\"type\":\"string\",\"required\":true},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"profile\",\"name\":\"基础信息\",\"type\":\"string\",\"required\":true},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"height\":92,\"width\":332}},{\"id\":\"215734195065536512\",\"type\":\"llm\",\"x\":746,\"y\":404,\"properties\":{\"text\":\"生成word文档\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"### 核心指令(必须100%遵守)\\n你必须只输出可被Java的JSON.parse()正确解析的JSON数组,禁止输出任何解释、注释、换行符(除JSON内部合法换行)或JSON以外的文字。若生成的JSON存在任何不合法/不完整问题,必须先自动修正,再输出最终结果。\\n### 一、JSON语法强制规则(违反则直接修正)\\n1. 输出仅为纯JSON数组,外层无任何引号/包裹符,数组括号必须成对闭合,无缺失/多余;\\n2. 所有键名(key)必须用英文双引号包裹,禁止单引号/无引号/中文引号;\\n3. 所有分隔符(逗号、冒号、大括号、中括号)必须是英文符号,禁止中文符号(如,:【】{});\\n4. 字符串值中的特殊字符(换行符、双引号)必须正确转义:\\n   - 换行符用 `\\\\n` 表示(禁止直接换行);\\n   - 字符串内的双引号需转义为 `\\\\\\\"`;\\n5. 禁止出现语法错误:无多余逗号(如 [{},])、无缺失逗号、无未闭合的括号、无乱码/不可见字符;\\n6. 输出的JSON必须是**完整的**,禁止截断/缺失内容(如list的valueList未结束、数组未闭合)。\\n### 二、业务规则强制要求(违反则自动补充/修正)\\n1. 字段基础规则:\\n   - 每个对象必须包含\\\"key\\\"字段(值可为空字符串 \\\"\\\");\\n   - \\\"type\\\"字段仅允许取值:\\\"title\\\"、\\\"list\\\"、\\\"\\\";\\n2. title类型强制规则:\\n   - type=\\\"title\\\"时,必须同时包含\\\"level\\\"(取值:first~sixth)、\\\"valueList\\\"(数组)、\\\"value\\\"字段;\\n   - \\\"value\\\"字段**必须以 `\\\\n` 结尾**,且不能为空(默认值:\\\"\\\\n\\\");\\n   - \\\"valueList\\\"数组内的每个元素必须包含\\\"value\\\"字段,支持font/size/bold/rowFlex等样式字段;\\n3. list类型强制规则:\\n   - type=\\\"list\\\"时,必须同时包含\\\"listType\\\"(ul/ol)、\\\"listStyle\\\"(disc/decimal/circle/square/checkbox)、\\\"valueList\\\"(非空数组)字段;\\n   - \\\"valueList\\\"数组内的每个元素必须包含\\\"value\\\"字段,不能为空;\\n4. 其他类型规则:\\n   - type=\\\"separator\\\"时,必须包含\\\"dashArray\\\"字段(数组,如 [1]);\\n   - 主动换行必须使用 `{ \\\"type\\\": \\\"\\\", \\\"value\\\": \\\"\\\\n\\\" }`,不同对象之间不自动换行;\\n   - 所有字符串值禁止为空(无内容时填空字符串 \\\"\\\",禁止null)。\\n### 三、生成后自检流程(必须执行)\\n1. 第一步:检查语法合法性\\n   - 验证是否能被Java JSON.parse()解析(模拟校验:无语法错误、符号正确、括号闭合);\\n   - 若存在语法错误,立即修正(如单引号转双引号、补充缺失括号、删除多余逗号);\\n2. 第二步:检查业务完整性\\n   - 遍历所有对象,检查title类型是否缺失level/value字段,缺失则补充(level默认second,value默认\\\"\\\\n\\\");\\n   - 检查list类型是否缺失listType/listStyle/valueList字段,缺失则补充(listType默认ul,listStyle默认disc,valueList默认空数组 []);\\n   - 检查title的value是否以\\\"\\\\n\\\"结尾,未结尾则补充;\\n3. 第三步:检查输出完整性\\n   - 验证JSON数组是否完整闭合,无截断/缺失内容;\\n   - 验证输出内容仅为JSON数组,无任何额外字符。\\n### 四、生成示例(参考此格式/规则)\\n[\\n    {\\n        \\\"type\\\": \\\"title\\\",\\n        \\\"level\\\": \\\"first\\\",\\n        \\\"valueList\\\": [\\n            {\\n                \\\"value\\\": \\\"个人简历\\\",\\n                \\\"font\\\": \\\"微软雅黑\\\",\\n                \\\"size\\\": 26,\\n                \\\"bold\\\": true,\\n                \\\"rowFlex\\\": \\\"center\\\"\\n            }\\n        ],\\n        \\\"value\\\": \\\"\\\\n\\\",\\n        \\\"key\\\": \\\"\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"\\\",\\n        \\\"value\\\": \\\"\\\\n\\\",\\n        \\\"key\\\": \\\"\\\"\\n    },\\n    {\\n        \\\"type\\\": \\\"list\\\",\\n        \\\"listType\\\": \\\"ul\\\",\\n        \\\"listStyle\\\": \\\"disc\\\",\\n        \\\"valueList\\\": [\\n            { \\\"value\\\": \\\"合法的列表项\\\", \\\"key\\\": \\\"\\\" }\\n        ],\\n        \\\"key\\\": \\\"\\\"\\n    }\\n]\\n### 五、最终输出要求\\n1. 仅输出修正后的完整JSON数组,无任何其他文字;\\n2. 输出前必须完成上述所有自检步骤,确保100%符合Java解析规范;\\n3. 生成的JSON需结构完整、字段齐全,禁止出现截断/缺失(如list未结束、数组未闭合)。\"},{\"role\":\"user\",\"content\":\"请根据以上字段和示例,生成一个完整的个人简历文档 JSON。\\n- 至少包含基础信息、个人优势、工作经历、项目经理、教育经历等模块。\\n- 若基础数据不足,可以适当生成参考数据。\\n- 用户信息如下:\\n基础资料:{{base}}\\n简介:{{profile}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"profile\",\"name\":\"base\",\"nodeId\":\"start-node\"},{\"field\":\"content\",\"name\":\"profile\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"height\":180,\"width\":332}},{\"id\":\"215735188368998400\",\"type\":\"end\",\"x\":1779,\"y\":408,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"\"},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"height\":114,\"width\":332}},{\"id\":\"215740280715427840\",\"type\":\"enhanceJava\",\"x\":1316,\"y\":405,\"properties\":{\"text\":\"Java 增强\",\"options\":{\"model\":{\"modeId\":\"1890232564262739969\",\"params\":{\"model\":\"OpenAI\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:JSON检验和修复专家\\n你是一位专门负责检验和修复JSON字符串的专家,确保其能被Java的`JSON.parse()`方法成功解析,并返回修复后的、可解析的JSON字符串原文。\\n## 目标:\\n1. 接收用户提供的JSON字符串,诊断其语法错误。\\n2. 智能修复常见的JSON格式问题(如引号缺失、尾随逗号、注释等),使其符合标准JSON规范。\\n3. 输出修复后的、可直接用于`JSON.parse()`的JSON字符串原文。\\n## 技能:\\n1. **深度语法分析**:精准识别JSON字符串中的语法错误位置和类型(如未闭合的引号、括号或花括号,错误的键值分隔符,非法字符等)。\\n2. **上下文感知修复**:根据JSON结构上下文,智能推断并应用最合理的修复方案(例如,为未加引号的键名添加双引号,移除对象或数组末尾的非法逗号)。\\n3. **标准合规性**:严格遵循IETF RFC 8259 JSON数据交换标准,确保输出为有效JSON。\\n4. **最小改动原则**:在保证修复有效的前提下,尽可能保持原始字符串的结构和意图,只修改必要的部分。\\n## 工作流:\\n1. **接收与初步检验**:接收用户输入的字符串,尝试使用`JSON.parse()`进行解析。若解析成功,则直接返回原字符串并告知其有效。\\n2. **错误诊断与定位**:若解析失败,捕获`SyntaxError`异常,分析错误信息以定位问题的大致位置和类型。\\n3. **详细扫描与修复**:逐字符扫描整个字符串,结合错误定位,系统性地检查并修复以下常见问题:\\n* 为未使用双引号的属性名(key)添加双引号。\\n* 确保所有字符串值由双引号包裹。\\n* 移除对象字面量`{}`或数组字面量`[]`中最后一个元素后的尾随逗号。\\n* 将单引号替换为双引号。\\n* 移除JavaScript风格的注释(`//` 单行注释, `/* */` 多行注释)。\\n* 转义字符串中未转义的控制字符(如换行符`\\\\n`、制表符`\\\\t`)。\\n* 检查并修正括号`[]`和花括号`{}`的配对与嵌套。\\n4. **验证与输出**:对修复后的字符串再次尝试`JSON.parse()`。若成功,则输出修复后的JSON字符串原文\\n## 输出格式:\\n- **当JSON有效时**:输出原始字符串。\\n- **当JSON被成功修复时**:然后换行输出修复后的JSON字符串原文。\\n## 限制:\\n- 仅处理语法错误,不验证JSON数据的业务逻辑或语义正确性。\\n- 对于歧义过大或结构严重损坏(如大量缺失内容)的JSON,可能无法修复,此时应清晰说明原因。\\n- 所有输出必须是纯文本格式,仅包含上述指定的提示信息和JSON字符串本身,不添加任何额外的Markdown代码块标记(如 ```json ```)。\\n- 严格遵守最小改动原则,避免对原始数据做出不必要的、可能改变其原意的修改。\"},{\"role\":\"user\",\"content\":\"{{word}}\"}],\"enhance\":{\"path\":\"ghbDemoAiWordGen\",\"type\":\"spring\"}},\"inputParams\":[{\"field\":\"text\",\"name\":\"resp\",\"nodeId\":\"215734195065536512\",\"customValue\":\"\"}],\"outputParams\":[{\"field\":\"result\",\"name\":\"返回结果\",\"type\":\"string\"}],\"height\":158,\"width\":332}}],\"edges\":[{\"id\":\"215734195073925120\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"215734195065536512\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"215734195065536512_input\",\"pointsList\":[{\"x\":466,\"y\":389},{\"x\":566,\"y\":389},{\"x\":480,\"y\":345},{\"x\":580,\"y\":345}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"215740398487289856\",\"type\":\"base-edge\",\"sourceNodeId\":\"215740280715427840\",\"targetNodeId\":\"215735188368998400\",\"sourceAnchorId\":\"215740280715427840_output\",\"targetAnchorId\":\"215735188368998400_input\",\"pointsList\":[{\"x\":1482,\"y\":357},{\"x\":1582,\"y\":357},{\"x\":1513,\"y\":382},{\"x\":1613,\"y\":382}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"279144043506167808\",\"type\":\"base-edge\",\"sourceNodeId\":\"215734195065536512\",\"targetNodeId\":\"215740280715427840\",\"sourceAnchorId\":\"215734195065536512_output\",\"targetAnchorId\":\"215740280715427840_input\",\"pointsList\":[{\"x\":912,\"y\":345},{\"x\":1012,\"y\":345},{\"x\":1050,\"y\":357},{\"x\":1150,\"y\":357}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"field\":\"result\",\"name\":\"resp\",\"nodeId\":\"215740280715427840\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"个人简介\",\"required\":true,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"profile\",\"name\":\"基础信息\",\"required\":true,\"type\":\"string\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '1952634605517447170'; + + +-- 应用增加快捷指令 +UPDATE `airag_app` SET `preset_question` = '[{\"key\":1,\"sort\":1,\"descr\":\"HIP 0603T Series\",\"update\":false},{\"key\":2,\"sort\":2,\"descr\":\"CHIP 1206HC Series\",\"update\":false},{\"key\":3,\"sort\":3,\"descr\":\"BRICK 1032ST Series\",\"update\":true}]' WHERE `id` = '1993651187913981953'; +UPDATE `airag_app` SET `preset_question` = '[{\"key\":1,\"sort\":1,\"descr\":\"请生成一张具有日本风格的动漫成年女孩。\",\"update\":false},{\"key\":2,\"sort\":2,\"descr\":\"请生成一幅中国神话故事中,手持武器的哪吒形象。\",\"update\":true}]' WHERE `id` = '2008090512835629057'; + +-- AI 生成图表、修改配置项-升级SQL +UPDATE `airag_flow` SET `name` = 'AI大屏生成组件', `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'267492142677889024\'),\n end.tag(\'267498945805422592\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":629,\"y\":-41,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"267492142677889024\",\"type\":\"llm\",\"x\":1138,\"y\":0,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"## 硬性要求:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。\\n不要输出任何解释、注释或 JSON 以外的文字。\\n# 角色:数据可视化专家\\n你是一位精通ECharts的数据可视化和大屏配置的专家,能够根据用户需求,智能选择最合适的图表类型,并生成高质量、可直接使用的ECharts配置项。\\n## 目标:\\n1. 根据用户提供的需求描述,分析其核心意图(如趋势分析、比较分析、占比分析等)。\\n2. 从下面给定的图表组件类型componentsData中,选择最匹配需求的一种。\\n3. 结合用户提供的数据结构,生成一份完整、规范、可运行的 ECharts 配置项(JSON格式)。\\n4. 非echart图表,参考componentsData组件配置,生成一份完整、规范、的配置项即可(JSON格式)。\\n5. 结合用户需求生成一个不超过15字的标题,并设置到返回JSON的title字段上。\\n6. 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n8. 热力地图,要生成echart的\\\"visualMap\\\"属性\\n## 技能:\\n1. **需求解析能力**:能够准确理解用户对数据可视化的业务需求,并将其转化为技术实现目标。\\n2. **图表选型能力**:精通折线图、柱状图、饼图、地图、散点图等从多种图表类型的特点与应用场景,能做出最佳选择。\\n3. **ECharts配置能力**:熟练掌握ECharts的option配置语法,能高效构建包含标题、坐标轴、图例、系列、提示框等完整组件的图表。\\n4. **数据适配能力**:能够将提供的 `chartData` 数据,自行分型类型并结合需求,将数据结构正确地映射到所选图表的 `series.data` 中。\\n5. **图表分析能力**:能够将提供的 `componentsData` 数据,自行分型类型并结合需求,选择生成适配的组件并返回规范合适的JSON配置。\\n## 工作流:\\n1. **需求分析**:仔细阅读 `{userInput}`,判断用户希望展示数据的何种关系(趋势、比较、占比、分布、相关)。\\n2. **图表选型**:根据第一步的分析结论,从componentsData图表类型中锁定唯一最合适的类型。\\n3. 对于ECharts图表构建基础option对象框架,包含 `title`, `tooltip`, `legend`, `grid`, `xAxis`, `yAxis`, `series` 等必要组件。\\n4. 根据选定的图表类型,配置 `series` 中的 `type` 和关键属性(如折线图的 `smooth`,饼图的 `radius`)。\\n5. 将用户提供的 `{chartData}` 数据结构,按照ECharts要求的格式进行处理和赋值(例如,对于柱状图,可能需要将数据拆分为类目轴数据和系列数据)。\\n6. 应用通用的美化原则(如配色清晰、标签易读、布局合理),生成最终配置。\\n7. 输出格式化:将生成的完整option对象,以格式规范、缩进清晰的JSON字符串形式输出。\\n8. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n## 输出格式:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。包含name,data,option,三个字段值,不要输出任何解释、注释或 JSON 以外的文字。\\n1.name:图表类型`name`(组件数据的key值(示例:如果渲染的柱形图,就设置为JBar),注意name值必须componentsData数据提供的组件compType值,不能是其他值);\\n2.api:上下文变量中提取出来的api,存在就赋值到输出接口的api中,不存在就设置为{API};\\n3.sql:上下文变量中提取出来的sql,存在就赋值到输出接口的sql中,不存在就设置为{SQL};\\n4.title:结合用户需求生成一个不超过15字的标题title,赋值到输出接口的title中;\\n5.option: 如果符合需求的是echart图表,就生成echart可直接使用的`option`对象,该option对象可直接用于ECharts.init().setOption()的配置项。如果符合要求的是非echart的图表,可参考componentsData中对应图表的option配置项生成,没有配置项就返回option:{}。不要包含其他的任何额外的解释、说明或markdown代码块标记。可以根据配置项中 echart:true来判断是否是echart图表\\n示例输出结构(以柱状图为例):\\n6.data: 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7. 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n示例输出结构(以柱状图为例):\\n[{\\nname:\\\"JBar\\\",\\noption:{\\n \\\"title\\\": { \\\"text\\\": \\\"示例标题\\\", \\\"left\\\": \\\"center\\\" },\\n \\\"tooltip\\\": {},\\n \\\"legend\\\": { \\\"data\\\": [\\\"示例图例\\\"] },\\n \\\"xAxis\\\": { \\\"type\\\": \\\"category\\\", \\\"data\\\": [\\\"衬衫\\\", \\\"羊毛衫\\\", \\\"雪纺衫\\\"] },\\n \\\"yAxis\\\": { \\\"type\\\": \\\"value\\\" },\\n \\\"series\\\": [ { \\\"name\\\": \\\"销量\\\", \\\"type\\\": \\\"bar\\\", \\\"data\\\": [5, 20, 36] } ]\\n },\\n api:{API},\\n sql:{SQL},\\n title:\\\"\\\",\\n data:[]\\n}]\\n## 限制:\\n- 必须严格从组件数据提供的componentsData中选择一种,不得自行创造或推荐其他图表类型。\\n- 生成的所有配置必须基于用户提供的 `{userInput}` 和可用的 `chartData`,不得虚构数据字段或结构。\\n- 输出必须为纯JSON格式,无需也无法在JSON中注释“这里是标题”等内容。配置的正确性由键值对本身保证。\\n- 遵循数据可视化最佳实践,避免误导性图表(如扭曲的比例尺、不恰当的图表类型)。\\n- 反幻觉校验:若 `{userInput}` 中提到的数据维度在 `chartData` 中无法找到对应字段,则在相关配置处使用空值或占位符,并在最终输出的JSON对象之外,以独立文本形式简要说明缺失情况。但首要输出仍是JSON配置本身。\\n- 伦理审查模块:若需求或数据涉及敏感信息(如个人身份信息),在配置中应对数据进行聚合或匿名化处理,避免直接暴露。\\n- 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n- 严格按照示例输出结构返回,不要包含```json```等信息\\n- 最多生成10个仪表盘组件\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"name\\\": \\\"基础柱形图\\\",\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"堆叠柱形图\\\",\\n    \\\"compType\\\": \\\"JStackBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态柱形图\\\",\\n    \\\"compType\\\": \\\"JDynamicBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"胶囊图\\\",\\n    \\\"compType\\\": \\\"JCapsuleChart\\\",\\n    \\\"echart\\\": false\\n    \\\"chartData\\\": [\\n        {\\n          name: \'苹果\',\\n          value: 1000879,\\n          type: \'手机品牌\',\\n    }],\\n    \\\"option\\\": {\\n        showValue: false,\\n        unit: \'\',\\n        customColor: [],\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          show: true,\\n          textStyle: {\\n            color: \'#464646\',\\n            fontWeight: \'normal\',\\n          },\\n        },\\n      }\\n  },\\n  {\\n    \\\"name\\\": \\\"基础条形图\\\",\\n    \\\"compType\\\": \\\"JHorizontalBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"背景柱形图\\\",\\n    \\\"compType\\\": \\\"JBackgroundBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比柱形图\\\",\\n    \\\"compType\\\": \\\"JMultipleBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"正负条形图\\\",\\n    \\\"compType\\\": \\\"JNegativeBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"折柱图\\\",\\n    \\\"compType\\\": \\\"JMixLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"百分比条形图\\\",\\n    \\\"compType\\\": \\\"JPercentBar\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"基础饼图\\\",\\n    \\\"compType\\\": \\\"JPie\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"南丁格尔玫瑰图\\\",\\n    \\\"compType\\\": \\\"JRose\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"旋转饼图\\\",\\n    \\\"compType\\\": \\\"JRotatePie\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        grid: {\\n          show: false,\\n          bottom: 115,\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          subtext: \'\',\\n          textStyle: {\\n            fontWeight: \'normal\',\\n          },\\n          show: true,\\n        },\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        tooltip: {\\n          trigger: \'item\',\\n        },\\n        legend: {\\n          orient: \'vertical\',\\n        },\\n        series: [\\n          {\\n            name: \'\',\\n            type: \'pie\',\\n            data: [],\\n            emphasis: {\\n              itemStyle: {\\n                shadowBlur: 10,\\n                shadowOffsetX: 0,\\n                shadowColor: \'rgba(0, 0, 0, 0.5)\',\\n              },\\n            },\\n          },\\n        ],\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"基础折线图\\\",\\n    \\\"compType\\\": \\\"JLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"平滑曲线图\\\",\\n    \\\"compType\\\": \\\"JSmoothLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"阶梯折线图\\\",\\n    \\\"compType\\\": \\\"JStepLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"面积图\\\",\\n    \\\"compType\\\": \\\"JArea\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比折线图\\\",\\n    \\\"compType\\\": \\\"JMultipleLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"双轴图\\\",\\n    \\\"compType\\\": \\\"DoubleLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础进度图\\\",\\n    \\\"compType\\\": \\\"JCustomProgress\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        barWidth: 19,\\n        padding: 12,\\n        progressColor: \'#76c7c0\',\\n        backgroundColor: \'#ffffff\',\\n        titleColor: \'#fff\',\\n        titleFontSize: 16,\\n        titlePosition: \'top\',\\n        valueColor: \'#fff\',\\n        valueFontSize: 16,\\n        valuePosition: \'middle\',\\n        valueXOffset: 0,\\n        valueYOffset: 0,\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"进度图\\\",\\n    \\\"compType\\\": \\\"JProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"列表进度图\\\",\\n    \\\"compType\\\": \\\"JListProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"圆形进度图\\\",\\n    \\\"compType\\\": \\\"JRoundProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"水波图\\\",\\n    \\\"compType\\\": \\\"JLiquid\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"象形柱图\\\",\\n    \\\"compType\\\": \\\"JPictorialBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"象形图\\\",\\n    \\\"compType\\\": \\\"JPictorial\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"男女占比\\\",\\n    \\\"compType\\\": \\\"JGender\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"普通散点图\\\",\\n    \\\"compType\\\": \\\"JScatter\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"气泡图\\\",\\n    \\\"compType\\\": \\\"JBubble\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色仪表盘\\\",\\n    \\\"compType\\\": \\\"JColorGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"渐变仪表盘\\\",\\n    \\\"compType\\\": \\\"JAntvGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"半圆仪表盘\\\",\\n    \\\"compType\\\": \\\"JSemiGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"普通漏斗图\\\",\\n    \\\"compType\\\": \\\"JFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"金字塔漏斗图\\\",\\n    \\\"compType\\\": \\\"JPyramidFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3D金字塔\\\",\\n    \\\"compType\\\": \\\"JPyramid3D\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"普通雷达图\\\",\\n    \\\"compType\\\": \\\"JRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"圆形雷达图\\\",\\n    \\\"compType\\\": \\\"JCircleRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"饼状环形图\\\",\\n    \\\"compType\\\": \\\"JRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色环形图\\\",\\n    \\\"compType\\\": \\\"JBreakRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础环形图\\\",\\n    \\\"compType\\\": \\\"JRingProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态环形图\\\",\\n    \\\"compType\\\": \\\"JActiveRing\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"玉珏图\\\",\\n    \\\"compType\\\": \\\"JRadialBar\\\",\\n    \\\"echart\\\": false\\n  },\\n    {\\n    \\\"name\\\": \\\"矩形图\\\",\\n    \\\"compType\\\": \\\"JRectangle\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"象限图\\\",\\n    \\\"compType\\\": \\\"JQuadrant\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"3D柱形图\\\",\\n    \\\"compType\\\": \\\"JBarGroup3d\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"3D分组柱形图\\\",\\n    \\\"compType\\\": \\\"JBar3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(横向)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(竖向+序号)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(高亮)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n   {\\n    \\\"name\\\": \\\"统计概览(卡片模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false\\n  },\\n   {\\n    \\\"name\\\": \\\"统计概览(背景模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片轮播\\\",\\n    \\\"compType\\\": \\\"JCardCarousel\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"文本\\\",\\n    \\\"compType\\\": \\\"JText\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"翻牌器\\\",\\n    \\\"compType\\\": \\\"JCountTo\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"颜色块\\\",\\n    \\\"compType\\\": \\\"JColorBlock\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数值\\\",\\n    \\\"compType\\\": \\\"JNumber\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轨道环形文字\\\",\\n    \\\"compType\\\": \\\"JOrbitRing\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"字符云\\\",\\n    \\\"compType\\\": \\\"JWordCloud\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"图层字符云\\\",\\n    \\\"compType\\\": \\\"JImgWordCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"闪动字符云\\\",\\n    \\\"compType\\\": \\\"JFlashCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轮播表\\\",\\n    \\\"compType\\\": \\\"JScrollBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"表格\\\",\\n    \\\"compType\\\": \\\"JScrollTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"发展历程\\\",\\n    \\\"compType\\\": \\\"JDevHistory\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据表格\\\",\\n    \\\"compType\\\": \\\"JCommonTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据列表\\\",\\n    \\\"compType\\\": \\\"JList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"排行榜\\\",\\n    \\\"compType\\\": \\\"JScrollRankingBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"个性排名(前四)\\\",\\n    \\\"compType\\\": \\\"JFlashList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"气泡排名(前五)\\\",\\n    \\\"compType\\\": \\\"JBubbleRank\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(单行)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(多行+序号)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(带表头)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"区域地图\\\",\\n    \\\"compType\\\": \\\"JAreaMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"散点地图\\\",\\n    \\\"compType\\\": \\\"JBubbleMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"柱形地图\\\",\\n    \\\"compType\\\": \\\"JBarMap\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"热力地图\\\",\\n    \\\"compType\\\": \\\"JHeatMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d柱形图\\\",\\n    \\\"compType\\\": \\\"JBar3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d分组柱形图\\\",\\n    \\\"compType\\\": \\\"JBarGroup3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"日历\\\",\\n    \\\"compType\\\": \\\"JPermanentCalendar\\\",\\n    \\\"echart\\\": false\\n  }\\n]\"},{\"role\":\"user\",\"content\":\"用户的问题: {{userInput}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userInput\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"267498945805422592\",\"type\":\"end\",\"x\":1630,\"y\":-36,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{res}}\",\"outputType\":\"default\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":114}}],\"edges\":[{\"id\":\"271609331975028736\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"267492142677889024\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"267492142677889024_input\",\"pointsList\":[{\"x\":795,\"y\":-56},{\"x\":895,\"y\":-56},{\"x\":872,\"y\":-59},{\"x\":972,\"y\":-59}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274786344761540608\",\"type\":\"base-edge\",\"sourceNodeId\":\"267492142677889024\",\"targetNodeId\":\"267498945805422592\",\"sourceAnchorId\":\"267492142677889024_output\",\"targetAnchorId\":\"267498945805422592_input\",\"pointsList\":[{\"x\":1304,\"y\":-59},{\"x\":1404,\"y\":-59},{\"x\":1364,\"y\":-62},{\"x\":1464,\"y\":-62}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '2004398098378108929'; +UPDATE `airag_flow` SET `name` = 'AI大屏优化配置', `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'269048862299471872\'),\n end.tag(\'269049045129183232\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":437,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"269048862299471872\",\"type\":\"llm\",\"x\":788,\"y\":473,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:ECharts和大屏图表配置修改专家\\n你是一位专注于ECharts和大屏图表图表配置修改的专家,能够根据用户需求,精准、高效地修改现有ECharts和大屏图表配置项,并返回完整的、可直接使用的修改后配置对象。\\n## 目标:\\n根据用户提供的具体修改指令(如:修改图表类型、调整数据、更改样式、添加交互等),对用户给出的原始ECharts配置项进行针对性修改,并输出修改后的完整配置对象。\\n## 技能:\\n1. 精通ECharts所有版本的配置项语法、结构及参数含义。\\n2. 能够准确理解用户对图表样式、数据、交互行为的修改意图。\\n3. 具备强大的代码编辑与重构能力,确保修改后的配置项语法正确、结构清晰、无冗余代码。\\n4. 对于非echart图表(componentsData提供的组件,属性中echart:false的即为非echart图表),自行从下面componentsData提供的组件对应的option配置项,修改符合要求的配置并返回。\\n## 工作流:\\n1. **接收与分析**:接收用户提供的原始ECharts配置对象(通常以JSON或JavaScript对象形式)以及具体的修改要求。仔细分析原始配置的结构和用户的修改点。\\n2. **精准修改**:严格依据用户指令,对原始配置对象进行最小化、精准化的修改。确保只改动指定部分,保持其他未提及配置的完整性。对于模糊指令,会基于ECharts最佳实践进行合理推断和实现。\\n3. **校验与格式化**:检查修改后的配置对象语法是否正确,是否符合ECharts规范。将最终配置对象以格式清晰、缩进规范的JSON或JavaScript对象形式呈现。\\n## 输出格式:\\n请始终输出一个完整的、格式化的JavaScript对象(或JSON),即修改后的 `option` 配置,只返回修改的属性配置,不要包含已存在的其他配置,\\n## 示例:\\n将柱体修改成黄色,就返回\\n\\\"compConfig\\\": {\\n    \\\"option\\\": {\\n      { \\\"series\\\": [ { \\\"itemStyle\\\": { \\\"color\\\": \\\"#FFFF00\\\" } } ] }\\n    }\\n}\\n修改组件名称为京东销量柱形图,背景色改成黑色就返回\\n\\\"compConfig\\\": {\\n \\\"name\\\":\\\"京东销量柱形图\\\",\\n \\\"background\\\":\\\"#000000\\\",\\n}\\n不要包含任何额外的解释、说明文字或代码块标记(如 ```json ```)。输出应直接以 `{` 开始,以 `}` 结束。\\n示例输出结构:\\n\\\"compConfig\\\": {\\n    \\\"name\\\":\\\"基础柱形图\\\",\\n    \\\"background\\\":\\\"#ffffff\\\",\\n    \\\"borderColor\\\":\\\"#000000\\\",\\n    \\\"option\\\": {\\n      \\\"title\\\": { ... },\\n      \\\"tooltip\\\": { ... },\\n      \\\"xAxis\\\": { ... },\\n      \\\"yAxis\\\": { ... },\\n      \\\"series\\\": [ ... ]\\n    }\\n}\\n## 限制:\\n- 仅对用户提供的原始配置进行修改,不凭空创建全新的图表配置。\\n- 输出必须仅为修改后的配置对象本身,不附带任何分析过程、修改日志或使用建议。\\n- 若用户指令存在歧义或无法实现,应在不破坏配置结构的前提下,做出最合理的默认修改或保留原样,并在配置对象内部以注释(`//`)形式简要说明。\\n- 严格遵守ECharts官方配置规范,不使用已废弃或实验性参数(除非用户明确要求)。\\n- 颜色类型的修改,要以具体色值设置,不要使用英文单词,例如黑色,使用#000000,不要使用black\\n- 修改的option属性,以componentsData中具体组件的option配置为主,结合echart选择符合要求的配置项修改\\n- [\'JRadioButton\', \'JRadialBar\', \'JActiveRing\', \'JRing\', \'JPyramidFunnel\', \'JFunnel\', \'JBubble\', \'DoubleLineBar\', \'JMultipleLine\', \'JArea\', \'JLine\', \'JRotatePie\', \'JRose\', \'JPie\', \'JMixLineBar\', \'JPercentBar\', \'JMultipleBar\', \'JCapsuleChart\', \'JStackBar\', \'JQuadrant\'] 这些组件的相关颜色属性修改,按照 \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}] 的格式修改; - 组件不包含customColor属性的颜色属性修改,按照对应组件配置的属性value数值去修改\\n- 柱体颜色属性修改使用 option.series[${index}].itemStyle.color\\n- 配置项粗细的修改参数包含 [{ label: \'默认\', value: \'normal\' } { label: \'粗体\', value: \'bold\' } { label: \'细体\', value: \'lighter\' }]\\n- 若用户修改名称或者背景色或者边框的属性,以componentsData中第一个柱形图配置为例,去修改返回对应配置即可\\n -名称:对应 compConfig.name\\n -背景色:对应 compConfig.background\\n -边框色:对应 compConfig.borderColor\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"name\\\":\\\"基础柱形图\\\",\\n      \\\"background\\\":\\\"#ffffff\\\",\\n      \\\"borderColor\\\":\\\"#000000\\\"\\n    }\\n  }]\\n组件配置说明\\n compOptionData = [\\n  {\\n    name: \'基础配置\',\\n    optionName: \'BasicOption\',\\n    children: [\\n      {\\\"label\\\": \\\"图层名称修改成\\\", \\\"value\\\": \\\"name\\\"},\\n      {\\\"label\\\": \\\"图层背景色设置成\\\", \\\"value\\\": \\\"background\\\"},\\n      {\\\"label\\\": \\\"图层边框线设置成\\\", \\\"value\\\": \\\"borderColor\\\"},\\n      {\\\"label\\\": \\\"提示语设置为隐藏\\\", \\\"value\\\": \\\"option.tooltip.show\\\"},\\n      {\\\"label\\\": \\\"提示语字体大小设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"提示语字体颜色设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"},\\n    ]\\n  },{\\n    name: \'标题设置\',\\n    optionName: \'TitleOption\',\\n    children: [\\n      {\\\"label\\\": \\\"标题名称修改成\\\", \\\"value\\\": \\\"option.title.text\\\"},\\n      {\\\"label\\\": \\\"标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontColor\\\"},\\n      {\\\"label\\\": \\\"标题字体粗细设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontWeight\\\"},\\n      {\\\"label\\\": \\\"副标题名称修改成\\\", \\\"value\\\": \\\"option.title.subtextStyle\\\"},\\n      {\\\"label\\\": \\\"副标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"副标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontColor\\\"},\\n      {\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"option.title.left\\\"},\\n      {\\\"label\\\": \\\"垂直居中\\\", \\\"value\\\": \\\"option.title.top\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'X轴设置\',\\n    optionName: \'XAxisOption\',\\n    children: [\\n      {\\\"label\\\": \\\"X轴名称修改成\\\", \\\"value\\\": \\\"option.xAxis.name\\\"},\\n      {\\\"label\\\": \\\"X轴名称颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.color\\\"},\\n      {\\\"label\\\": \\\"X轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"X轴标签颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"X轴标签角度\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.rotate\\\"},\\n      {\\\"label\\\": \\\"X轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"X轴轴类型修改成\\\", \\\"value\\\": \\\"option.xAxis.type\\\"},\\n      {\\\"label\\\": \\\"X轴显示网格线\\\", \\\"value\\\": \\\"option.xAxis.splitLine.show\\\"},\\n      {\\\"label\\\": \\\"X轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.splitLine.lineStyle.color\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'Y轴设置\',\\n    optionName: \'YAxisOption\',\\n    children: [\\n      {\\\"label\\\": \\\"Y轴名称修改成\\\", \\\"value\\\": \\\"option.yAxis.name\\\"},\\n      {\\\"label\\\": \\\"Y轴名称颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"Y轴标签颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"Y轴标签角度\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.rotate\\\"},\\n      {\\\"label\\\": \\\"Y轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴轴类型修改成\\\", \\\"value\\\": \\\"option.yAxis.type\\\"},\\n      {\\\"label\\\": \\\"Y轴显示网格线\\\", \\\"value\\\": \\\"option.yAxis.splitLine.show\\\"},\\n      {\\\"label\\\": \\\"Y轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.splitLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴单位\\\", \\\"value\\\": \\\"option.yAxis.yUnit\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'图例设置\',\\n    optionName: \'LegendOption\',\\n    children: [\\n      {\\\"label\\\": \\\"图例字体大小设置成\\\", \\\"value\\\": \\\"option.legend.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"图例设置成横排\\\", \\\"value\\\": \\\"option.legend.orient\\\"},\\n      {\\\"label\\\": \\\"图例上下边距设置\\\", \\\"value\\\": \\\"option.legend.t\\\"},\\n      {\\\"label\\\": \\\"图例左右边距设置\\\", \\\"value\\\": \\\"option.legend.r\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'自定义配色\',\\n    optionName: \'CustomColorOption\',\\n    children: [\\n      {\\\"label\\\": \\\"颜色设置成***色\\\", \\\"value\\\": \\\"option.customColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'柱体设置\',\\n    optionName: \'BarCylinder\',\\n    children: [\\n      {\\\"label\\\": \\\"柱体宽度修改为\\\", \\\"value\\\": \\\"option.series[${index}].barWidth\\\"},\\n      {\\\"label\\\": \\\"柱体圆角修改为\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.borderRadius\\\"},\\n      {\\\"label\\\": \\\"柱体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.color\\\"},\\n      {\\\"label\\\": \\\"柱体背景色显隐\\\", \\\"value\\\": \\\"option.series[${index}].showBackground\\\"},\\n      {\\\"label\\\": \\\"柱体背景色颜色\\\", \\\"value\\\": \\\"option.series[${index}].backgroundStyle.color\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'折线设置\',\\n    optionName: \'PolyglineOption\',\\n    children: [\\n      {\\\"label\\\": \\\"折线类型修改\\\", \\\"value\\\": \\\"option.series[${index}].lineType\\\"},\\n      {\\\"label\\\": \\\"线条宽度修改\\\", \\\"value\\\": \\\"option.series[${index}].lineWidth\\\"},\\n      {\\\"label\\\": \\\"标记点修改\\\", \\\"value\\\": \\\"option.series[${index}].symbol\\\"},\\n      {\\\"label\\\": \\\"点的大小修改\\\", \\\"value\\\": \\\"option.series[${index}].symbolSize\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'饼图设置\',\\n    optionName: \'pieSettingOption\',\\n    children: [\\n      {\\\"label\\\": \\\"饼图设置成环形\\\", \\\"value\\\": \\\"option.isRadius\\\"},\\n      {\\\"label\\\": \\\"饼图内环半径设置成\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"饼图外环半径设置成\\\", \\\"value\\\": \\\"option.outRadius\\\"},\\n      {\\\"label\\\": \\\"饼图设置成南丁格尔玫瑰\\\", \\\"value\\\": \\\"option.isRose\\\"},\\n      {\\\"label\\\": \\\"饼图标签显示位置\\\", \\\"value\\\": \\\"option.pieLabelPosition\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'中心坐标\',\\n    optionName: \'gridPieOption\',\\n    children: [\\n      {\\\"label\\\": \\\"上下边距修改为\\\", \\\"value\\\": \\\"option.grid.top\\\"},\\n      {\\\"label\\\": \\\"左右边距修改为\\\", \\\"value\\\": \\\"option.grid.left\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'坐标轴边距\',\\n    optionName: \'GridOption\',\\n    children: [\\n      {\\\"label\\\": \\\"左边距修改成\\\", \\\"value\\\": \\\"option.grid.left\\\"},\\n      {\\\"label\\\": \\\"顶边距\\\", \\\"value\\\": \\\"option.grid.top\\\"},\\n      {\\\"label\\\": \\\"右边距\\\", \\\"value\\\": \\\"option.grid.right\\\"},\\n      {\\\"label\\\": \\\"底边距\\\", \\\"value\\\": \\\"option.grid.bottom\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'数值设置\',\\n    optionName: \'NumOption\',\\n    children: [\\n      {\\\"label\\\": \\\"数值显示位置在\\\", \\\"value\\\": \\\"option.series[${index}].label.position\\\"},\\n      {\\\"label\\\": \\\"数值内容格式修改成\\\", \\\"value\\\": \\\"option.label.format\\\"},\\n      {\\\"label\\\": \\\"数值字体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.color\\\"},\\n      {\\\"label\\\": \\\"数值字体大小修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontSize\\\"},\\n      {\\\"label\\\": \\\"数值字体粗细修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontWeight\\\"},\\n      {\\\"label\\\": \\\"数值单位配置显隐\\\", \\\"value\\\": \\\"option.showUnit.show\\\"},\\n      {\\\"label\\\": \\\"数值单位数量级设置\\\", \\\"value\\\": \\\"option.showUnit.numberLevel\\\"},\\n      {\\\"label\\\": \\\"数值单位保留小数\\\", \\\"value\\\": \\\"option.showUnit.decimal\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'进度设置\',\\n    optionName: \'CustomProgressOption\',\\n    children: [\\n      {\\\"label\\\": \\\"进度目标颜色\\\", \\\"value\\\": \\\"option.backgroundColor\\\"},\\n      {\\\"label\\\": \\\"进度颜色\\\", \\\"value\\\": \\\"option.progressColor\\\"},\\n      {\\\"label\\\": \\\"进度条宽度\\\", \\\"value\\\": \\\"option.barWidth\\\"},\\n      {\\\"label\\\": \\\"进度边距设置\\\", \\\"value\\\": \\\"option.padding\\\"},\\n      {\\\"label\\\": \\\"进度标题颜色设置\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"进度标题字体大小设置\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n      {\\\"label\\\": \\\"进度标题位置设置\\\", \\\"value\\\": \\\"option.titlePosition\\\"},\\n      {\\\"label\\\": \\\"进度数值颜色设置\\\", \\\"value\\\": \\\"option.valueColor\\\"},\\n      {\\\"label\\\": \\\"进度数值字体大小设置\\\", \\\"value\\\": \\\"option.valueFontSize\\\"},\\n      {\\\"label\\\": \\\"进度数值位置设置\\\", \\\"value\\\": \\\"option.valuePosition\\\"},\\n      {\\\"label\\\": \\\"进度数值横向偏移\\\", \\\"value\\\": \\\"option.valueXOffset\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'列表进度图设置\',\\n    optionName: \'ListProgressOption\',\\n    children: [\\n      {\\\"label\\\": \\\"列表进度图行高度\\\", \\\"value\\\": \\\"option.row.height\\\"},\\n      {\\\"label\\\": \\\"列表进度图行左边距\\\", \\\"value\\\": \\\"option.row.marginLeft\\\"},\\n      {\\\"label\\\": \\\"列表进度图行右边距\\\", \\\"value\\\": \\\"option.row.marginRight\\\"},\\n      {\\\"label\\\": \\\"列表进度图行上边距\\\", \\\"value\\\": \\\"option.row.marginTop\\\"},\\n      {\\\"label\\\": \\\"进度条颜色配置\\\", \\\"value\\\": \\\"option.bar.background.color\\\"},\\n      {\\\"label\\\": \\\"进度条填充色配置\\\", \\\"value\\\": \\\"option.bar.fill.color\\\"},\\n      {\\\"label\\\": \\\"进度条高度设置\\\", \\\"value\\\": \\\"option.bar.height\\\"},\\n      {\\\"label\\\": \\\"进度条圆角设置\\\", \\\"value\\\": \\\"option.bar.borderRadius\\\"},\\n      {\\\"label\\\": \\\"进度指示点大小设置\\\", \\\"value\\\": \\\"option.bar.indicatorSize\\\"},\\n      {\\\"label\\\": \\\"进度指示点颜色设置\\\", \\\"value\\\": \\\"option.bar.indicatorColor\\\"},\\n      {\\\"label\\\": \\\"显示边框\\\", \\\"value\\\": \\\"option.bar.border.enabled\\\"},\\n      {\\\"label\\\": \\\"边框颜色\\\", \\\"value\\\": \\\"option.bar.border.color\\\"},\\n      {\\\"label\\\": \\\"边框大小\\\", \\\"value\\\": \\\"option.bar.border.width\\\"},\\n      {\\\"label\\\": \\\"边框边距\\\", \\\"value\\\": \\\"option.bar.border.padding\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'水波图设置\',\\n    optionName: \'LiquidPlotOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示类型\\\", \\\"value\\\": \\\"option.liquidType\\\"},\\n      {\\\"label\\\": \\\"波纹颜色\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"波纹个数\\\", \\\"value\\\": \\\"option.count\\\"},\\n      {\\\"label\\\": \\\"波纹长度\\\", \\\"value\\\": \\\"option.length\\\"},\\n      {\\\"label\\\": \\\"外框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"},\\n      {\\\"label\\\": \\\"外框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"},\\n      {\\\"label\\\": \\\"间距\\\", \\\"value\\\": \\\"option.distance\\\"},\\n      {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.strokeOpacity\\\"},\\n      {\\\"label\\\": \\\"文本颜色配置\\\", \\\"value\\\": \\\"option.textColor\\\"},\\n      {\\\"label\\\": \\\"文本字体大小配置\\\", \\\"value\\\": \\\"option.textFontSize\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'象形图设置\',\\n    optionName: \'PictorialOption\',\\n    children: [\\n      {\\\"label\\\": \\\"象形图柱体颜色设置\\\", \\\"value\\\": \\\"option.barColor\\\"},\\n      {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.barOpacity\\\"},\\n      {\\\"label\\\": \\\"间距设置\\\", \\\"value\\\": \\\"option.count\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'仪表盘设置\',\\n    optionName: \'GaugeOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.series[0].axisLabel.show\\\"},\\n      {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.series[0].axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.series[0].axisLabel.fontSize\\\"},\\n      {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.series[0].axisTick.show\\\"},\\n      {\\\"label\\\": \\\"刻度线长度\\\", \\\"value\\\": \\\"option.series[0].axisTick.length\\\"},\\n      {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.series[0].axisTick.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"显示分割线\\\", \\\"value\\\": \\\"option.series[0].splitLine.show\\\"},\\n      {\\\"label\\\": \\\"分割线长度\\\", \\\"value\\\": \\\"option.series[0].splitLine.length\\\"},\\n      {\\\"label\\\": \\\"分割线颜色\\\", \\\"value\\\": \\\"option.series[0].splitLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"指标字号\\\", \\\"value\\\": \\\"option.series[0].detail.fontSize\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'渐变仪表盘设置\',\\n    optionName: \'AntvGaugeOption\',\\n    children: [\\n      {\\\"label\\\": \\\"仪表盘粗细设置\\\", \\\"value\\\": \\\"option.gaugeWidth\\\"},\\n      {\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.axisLabelShow\\\"},\\n      {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.axisLabelColor\\\"},\\n      {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.axisLabelFontSize\\\"},\\n      {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.axisTickShow\\\"},\\n      {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.lineColor\\\"},\\n      {\\\"label\\\": \\\"文本颜色\\\", \\\"value\\\": \\\"option.valueColor\\\"},\\n      {\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"},\\n      {\\\"label\\\": \\\"指针颜色\\\", \\\"value\\\": \\\"option.indicatorColor\\\"},\\n      {\\\"label\\\": \\\"指针粗细\\\", \\\"value\\\": \\\"option.indicatorLength\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'尺寸设置\',\\n    optionName: \'Pyramid3DOption\',\\n    children: [\\n      {\\\"label\\\": \\\"缩放\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"尺寸\\\", \\\"value\\\": \\\"option.size\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'环形设置\',\\n    optionName: \'RingOption\',\\n    children: [\\n      {\\\"label\\\": \\\"内半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"外半径\\\", \\\"value\\\": \\\"option.outRadius\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'环形图设置\',\\n    optionName: \'ActiveRingPlotOption\',\\n    children: [\\n      {\\\"label\\\": \\\"环形图颜色设置\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"环形图背景色设置\\\", \\\"value\\\": \\\"option.bgColor\\\"},\\n      {\\\"label\\\": \\\"环形图外环半径\\\", \\\"value\\\": \\\"option.outRadius\\\"},\\n      {\\\"label\\\": \\\"环形图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"环形图标题字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"环形图标题字体颜色\\\", \\\"value\\\": \\\"option.fontColor\\\"},\\n      {\\\"label\\\": \\\"环形图标题字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"},\\n      {\\\"label\\\": \\\"环形图数值字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"},\\n      {\\\"label\\\": \\\"环形图数值字体颜色\\\", \\\"value\\\": \\\"option.valueFontColor\\\"},\\n      {\\\"label\\\": \\\"环形图数值字体粗细\\\", \\\"value\\\": \\\"option.valueFontWeight\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'动态环形图设置\',\\n    optionName: \'ActiveRingOption\',\\n    children: [\\n      {\\\"label\\\": \\\"动态环形图显示原始值\\\", \\\"value\\\": \\\"option.showOriginValue\\\"},\\n      {\\\"label\\\": \\\"动态环形图文字颜色\\\", \\\"value\\\": \\\"option.textColor\\\"},\\n      {\\\"label\\\": \\\"动态环形图文字大小\\\", \\\"value\\\": \\\"option.textFontSize\\\"},\\n      {\\\"label\\\": \\\"动态环形图线条宽度\\\", \\\"value\\\": \\\"option.lineWidth\\\"},\\n      {\\\"label\\\": \\\"动态环形图环半径\\\", \\\"value\\\": \\\"option.radius\\\"},\\n      {\\\"label\\\": \\\"动态环形图动态环半径\\\", \\\"value\\\": \\\"option.activeRadius\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'玉珏设置\',\\n    optionName: \'RadialBarOption\',\\n    children: [\\n      {\\\"label\\\": \\\"玉珏图显示圆角\\\", \\\"value\\\": \\\"option.radiusShow\\\"},\\n      {\\\"label\\\": \\\"玉珏图背景显示\\\", \\\"value\\\": \\\"option.bgShow\\\"},\\n      {\\\"label\\\": \\\"玉珏图外环半径\\\", \\\"value\\\": \\\"option.radius\\\"},\\n      {\\\"label\\\": \\\"玉珏图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"玉珏图最大旋转角\\\", \\\"value\\\": \\\"option.maxAngle\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'矩形图设置\',\\n    optionName: \'RectangleOption\',\\n    children: [\\n      {\\\"label\\\": \\\"矩形图文本颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"矩形图文本字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n      {\\\"label\\\": \\\"矩形图显示图例\\\", \\\"value\\\": \\\"option.showLegend\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'文本设置\',\\n    optionName: \'TextOption\',\\n    children: [\\n      {\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.body.fontSize\\\"},\\n      {\\\"label\\\": \\\"文本字体间距\\\", \\\"value\\\": \\\"option.body.letterSpacing\\\"},\\n      {\\\"label\\\": \\\"文本字体颜色\\\", \\\"value\\\": \\\"option.body.color\\\"},\\n      {\\\"label\\\": \\\"文本启用千分符\\\", \\\"value\\\": \\\"option.body.thousandSeparator\\\"},\\n      {\\\"label\\\": \\\"文本水平间距\\\", \\\"value\\\": \\\"option.body.marginLeft\\\"},\\n      {\\\"label\\\": \\\"文本垂直间距\\\", \\\"value\\\": \\\"option.body.marginTop\\\"},\\n      {\\\"label\\\": \\\"文本开启跑马灯\\\", \\\"value\\\": \\\"option.horseLamp\\\"},\\n      {\\\"label\\\": \\\"文本开启超链接\\\", \\\"value\\\": \\\"option.isLink\\\"},\\n      {\\\"label\\\": \\\"文本超链接地址\\\", \\\"value\\\": \\\"option.openUrl\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'内部设置\',\\n    optionName: \'CountToTextOption\',\\n    children: [\\n      {\\\"label\\\": \\\"字体粗细设置\\\", \\\"value\\\": \\\"option.fontWeight\\\"},\\n      {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.fontColor\\\"},\\n      {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.prefixFontSize\\\"},\\n      {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixFontColor\\\"},\\n      {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"},\\n      {\\\"label\\\": \\\"前缀字体对齐方式\\\", \\\"value\\\": \\\"option.prefixTextAlign\\\"},\\n      {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixGridX\\\"},\\n      {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"},\\n      {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixFontColor\\\"},\\n      {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"},\\n      {\\\"label\\\": \\\"后缀字体对齐方式\\\", \\\"value\\\": \\\"option.suffixTextAlign\\\"},\\n      {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixGridX\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'颜色块设置\',\\n    optionName: \'ColorBlockOption\',\\n    children: [\\n      {\\\"label\\\": \\\"颜色块行数设置\\\", \\\"value\\\": \\\"option.lineNum\\\"},\\n      {\\\"label\\\": \\\"颜色块边距设置\\\", \\\"value\\\": \\\"option.padding\\\"},\\n      {\\\"label\\\": \\\"颜色块X间距设置\\\", \\\"value\\\": \\\"option.borderSplitx\\\"},\\n      {\\\"label\\\": \\\"颜色块Y间距设置\\\", \\\"value\\\": \\\"option.borderSplity\\\"},\\n      {\\\"label\\\": \\\"小数位数设置\\\", \\\"value\\\": \\\"option.decimals\\\"},\\n      {\\\"label\\\": \\\"字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"字体颜色\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"},\\n      {\\\"label\\\": \\\"字体对齐方式\\\", \\\"value\\\": \\\"option.textAlign\\\"},\\n      {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.borderSplity\\\"},\\n      {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixColor\\\"},\\n      {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"},\\n      {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixSplitx\\\"},\\n      {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixSplity\\\"},\\n      {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"},\\n      {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixColor\\\"},\\n      {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"},\\n      {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixSplitx\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'字体设置\',\\n    optionName: \'FlashCloudOption\',\\n    children: [\\n      {\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.textSize\\\"},\\n      {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.textColor\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'字符云设置\',\\n    optionName: \'WordCloudOption\',\\n    children: [\\n      {\\\"label\\\": \\\"字体颜色配置\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"字体间距设置\\\", \\\"value\\\": \\\"option.padding\\\"},\\n      {\\\"label\\\": \\\"字体旋转设置\\\", \\\"value\\\": \\\"option.rotation\\\"},\\n      {\\\"label\\\": \\\"字体最大值设置\\\", \\\"value\\\": \\\"option.minSize\\\"},\\n      {\\\"label\\\": \\\"字体最小值设置\\\", \\\"value\\\": \\\"option.maxSize\\\"},\\n      {\\\"label\\\": \\\"字体形状设置\\\", \\\"value\\\": \\\"option.series[0].shape\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'轮播表格设置\',\\n    optionName: \'ScrollBoardOpt\',\\n    children: [\\n      {\\\"label\\\": \\\"悬浮暂停设置\\\", \\\"value\\\": \\\"option.hoverPause\\\"},\\n      {\\\"label\\\": \\\"等待时间设置\\\", \\\"value\\\": \\\"option.waitTime\\\"},\\n      {\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.index\\\"},\\n      {\\\"label\\\": \\\"表格列宽\\\", \\\"value\\\": \\\"option.indexWidth\\\"},\\n      {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.headShow\\\"},\\n      {\\\"label\\\": \\\"表头颜色\\\", \\\"value\\\": \\\"option.headerBGC\\\"},\\n      {\\\"label\\\": \\\"表头行高\\\", \\\"value\\\": \\\"option.headerHeight\\\"},\\n      {\\\"label\\\": \\\"每页行数\\\", \\\"value\\\": \\\"option.rowNum\\\"},\\n      {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddRowBGC\\\"},\\n      {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenRowBGC\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'表格设置\',\\n    optionName: \'ScrollTableStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.ranking\\\"},\\n      {\\\"label\\\": \\\"开启滚动\\\", \\\"value\\\": \\\"option.scroll\\\"},\\n      {\\\"label\\\": \\\"滚动时间\\\", \\\"value\\\": \\\"option.scrollTime\\\"},\\n      {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.showHead\\\"},\\n      {\\\"label\\\": \\\"表头背景颜色\\\", \\\"value\\\": \\\"option.headerBgColor\\\"},\\n      {\\\"label\\\": \\\"表头字体颜色\\\", \\\"value\\\": \\\"option.headerFontColor\\\"},\\n      {\\\"label\\\": \\\"表头字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"行高设置\\\", \\\"value\\\": \\\"option.lineHeight\\\"},\\n      {\\\"label\\\": \\\"边框显示\\\", \\\"value\\\": \\\"option.showBorder\\\"},\\n      {\\\"label\\\": \\\"边框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"},\\n      {\\\"label\\\": \\\"边框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"},\\n      {\\\"label\\\": \\\"边框线类型\\\", \\\"value\\\": \\\"option.borderStyle\\\"},\\n      {\\\"label\\\": \\\"表格字体颜色\\\", \\\"value\\\": \\\"option.bodyFontColor\\\"},\\n      {\\\"label\\\": \\\"表格字体大小\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"},\\n      {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddColor\\\"},\\n      {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'历程设置\',\\n    optionName: \'DevHistoryOption\',\\n    children: [\\n      {\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"轮播间隔\\\", \\\"value\\\": \\\"option.waitTime\\\"},\\n      {\\\"label\\\": \\\"历程背景色\\\", \\\"value\\\": \\\"option.typeBackColor\\\"},\\n      {\\\"label\\\": \\\"历程字体颜色\\\", \\\"value\\\": \\\"option.typeFontColor\\\"},\\n      {\\\"label\\\": \\\"内容字体颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"内容字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'数据表格设置\',\\n    optionName: \'TableStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"表头颜色\\\", \\\"value\\\": \\\"option.headerColor\\\"},\\n      {\\\"label\\\": \\\"表头字体大小\\\", \\\"value\\\": \\\"option.headerFontSize\\\"},\\n      {\\\"label\\\": \\\"表头颜色\\\", \\\"value\\\": \\\"option.headerFontSize\\\"},\\n      {\\\"label\\\": \\\"内容字体颜色\\\", \\\"value\\\": \\\"option.bodyColor\\\"},\\n      {\\\"label\\\": \\\"内容字体大小\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"},\\n      {\\\"label\\\": \\\"内容背景颜色\\\", \\\"value\\\": \\\"option.bodyBgColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'列表设置\',\\n    optionName: \'ListStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"显示标题前缀\\\", \\\"value\\\": \\\"option.showTitlePrefix\\\"},\\n      {\\\"label\\\": \\\"显示时间前缀\\\", \\\"value\\\": \\\"option.showTimePrefix\\\"},\\n      {\\\"label\\\": \\\"列表布局设置\\\", \\\"value\\\": \\\"option.layout\\\"},\\n      {\\\"label\\\": \\\"标题字体颜色\\\", \\\"value\\\": \\\"option.titleFontColor\\\"},\\n      {\\\"label\\\": \\\"标题字体粗细\\\", \\\"value\\\": \\\"option.titleFontWeight\\\"},\\n      {\\\"label\\\": \\\"标题字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n      {\\\"label\\\": \\\"内容图标颜色\\\", \\\"value\\\": \\\"option.iconColor\\\"},\\n      {\\\"label\\\": \\\"内容颜色\\\", \\\"value\\\": \\\"option.contentColor\\\"},\\n      {\\\"label\\\": \\\"开启动画设置\\\", \\\"value\\\": \\\"option.isEnableAnimation\\\"},\\n      {\\\"label\\\": \\\"轮播时间(毫秒)设置\\\", \\\"value\\\": \\\"option.scrollTime\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'滚动设置\',\\n    optionName: \'ScrollOption\',\\n    children: [\\n      {\\\"label\\\": \\\"是否排序\\\", \\\"value\\\": \\\"option.sort\\\"},\\n      {\\\"label\\\": \\\"轮播方式设置单行\\\", \\\"value\\\": \\\"option.carousel\\\",\\\"options\\\": [{\\\"label\\\": \\\"单行\\\", \\\"value\\\": \\\"single\\\"}, {\\\"label\\\": \\\"整页\\\", \\\"value\\\": \\\"page\\\"},]},\\n      {\\\"label\\\": \\\"显示行数\\\", \\\"value\\\": \\\"option.rowNum\\\"},\\n      {\\\"label\\\": \\\"滚动时间(毫秒)设置\\\", \\\"value\\\": \\\"option.waitTime\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'气泡排名设置\',\\n    optionName: \'BubbleRankingStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"比例设置\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"显示提示词\\\", \\\"value\\\": \\\"option.showTip\\\"},\\n      {\\\"label\\\": \\\"提示词颜色设置为\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"提示词宽度设置为\\\", \\\"value\\\": \\\"option.tipWidth\\\"},\\n      {\\\"label\\\": \\\"提示词内容颜色设置\\\", \\\"value\\\": \\\"option.tipFontColor\\\"},\\n      {\\\"label\\\": \\\"提示词内容字体大小设置\\\", \\\"value\\\": \\\"option.tipFontSize\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'气泡排名设置\',\\n    optionName: \'BubbleRankingStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"比例设置\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"显示提示词\\\", \\\"value\\\": \\\"option.showTip\\\"},\\n      {\\\"label\\\": \\\"提示词颜色设置为\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"提示词宽度设置为\\\", \\\"value\\\": \\\"option.tipWidth\\\"},\\n      {\\\"label\\\": \\\"提示词内容颜色设置\\\", \\\"value\\\": \\\"option.tipFontColor\\\"},\\n      {\\\"label\\\": \\\"提示词内容字体大小设置\\\", \\\"value\\\": \\\"option.tipFontSize\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'地图设置\',\\n    optionName: \'MapOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示区域名称\\\", \\\"value\\\": \\\"option.geo.label.normal.show\\\"},\\n      {\\\"label\\\": \\\"区域名称颜色设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.color\\\"},\\n      {\\\"label\\\": \\\"区域名称字体大小设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.fontSize\\\"},\\n      {\\\"label\\\": \\\"是否开启钻取\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"},\\n      {\\\"label\\\": \\\"导航文字颜色设置\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"},\\n      {\\\"label\\\": \\\"是否开启鼠标缩放\\\", \\\"value\\\": \\\"option.geo.roam\\\"},\\n      {\\\"label\\\": \\\"缩放比例设置\\\", \\\"value\\\": \\\"option.geo.zoom\\\"},\\n      {\\\"label\\\": \\\"地图长宽比设置\\\", \\\"value\\\": \\\"option.geo.aspectScale\\\"},\\n      {\\\"label\\\": \\\"地图顶边距设置\\\", \\\"value\\\": \\\"option.geo.top\\\"},\\n      {\\\"label\\\": \\\"地图左边距设置\\\", \\\"value\\\": \\\"option.geo.left\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'地图配色设置\',\\n    optionName: \'LineMapColorOption\',\\n    children: [\\n      {\\\"label\\\": \\\"启用渐变色\\\", \\\"value\\\": \\\"commonOption.gradientColor\\\"},\\n      {\\\"label\\\": \\\"中心颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"},\\n      {\\\"label\\\": \\\"边缘颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color2\\\"},\\n      {\\\"label\\\": \\\"区域颜色设置\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"},\\n      {\\\"label\\\": \\\"区域高亮颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.emphasis.areaColor\\\"},\\n      {\\\"label\\\": \\\"区域边界颜色\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.borderColor\\\"},\\n      {\\\"label\\\": \\\"阴影大小设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowBlur\\\"},\\n      {\\\"label\\\": \\\"阴影水平偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetX\\\"},\\n      {\\\"label\\\": \\\"阴影垂直偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetY\\\"},\\n      {\\\"label\\\": \\\"阴影颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'视觉映射设置\',\\n    optionName: \'VisualMapOptoin\',\\n    children: [\\n      {\\\"label\\\": \\\"开启视觉映射\\\", \\\"value\\\": \\\"option.visualMap.show\\\"},\\n      {\\\"label\\\": \\\"视觉映射类型\\\", \\\"value\\\": \\\"option.visualMap.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"continuous\\\", \\\"value\\\": \\\"continuous\\\"}, {\\\"label\\\": \\\"piecewise\\\", \\\"value\\\": \\\"piecewise\\\"}]},\\n      {\\\"label\\\": \\\"视觉映射文本颜色\\\", \\\"value\\\": \\\"option.visualMap.textStyle.color\\\"},\\n      {\\\"label\\\": \\\"视觉映射文本粗细\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontWeight\\\"},\\n      {\\\"label\\\": \\\"视觉映射文本字体大小设置\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"区域边界最小值\\\", \\\"value\\\": \\\"option.visualMap.min\\\"},\\n      {\\\"label\\\": \\\"区域边界最大值\\\", \\\"value\\\": \\\"option.visualMap.max\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'地图散点设置\',\\n    optionName: \'ScatterOption\',\\n    children: [\\n      {\\\"label\\\": \\\"地图散点大小设置\\\", \\\"value\\\": \\\"option.area.markerSize\\\"},\\n      {\\\"label\\\": \\\"地图散点形状设置\\\", \\\"value\\\": \\\"option.area.markerShape\\\"},\\n      {\\\"label\\\": \\\"地图散点类型设置\\\", \\\"value\\\": \\\"option.area.markerType\\\"},\\n      {\\\"label\\\": \\\"地图散点颜色设置\\\", \\\"value\\\": \\\"option.area.markerColor\\\"},\\n      {\\\"label\\\": \\\"地图散点文本显示\\\", \\\"value\\\": \\\"option.area.scatterLabelShow\\\"},\\n      {\\\"label\\\": \\\"地图散点文本颜色设置\\\", \\\"value\\\": \\\"option.area.scatterLabelColor\\\"},\\n      {\\\"label\\\": \\\"地图散点文本显示位置设置\\\", \\\"value\\\": \\\"option.area.scatterLabelPosition\\\"},\\n      {\\\"label\\\": \\\"地图散点文本字体大小设置\\\", \\\"value\\\": \\\"option.area.scatterFontSize\\\"},\\n      {\\\"label\\\": \\\"地图散点数量设置\\\", \\\"value\\\": \\\"option.area.markerCount\\\"},\\n      {\\\"label\\\": \\\"地图散点透明度设置\\\", \\\"value\\\": \\\"option.area.markerOpacity\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'热力地图设置\',\\n    optionName: \'HeatOption\',\\n    children: [\\n      {\\\"label\\\": \\\"热力点大小设置\\\", \\\"value\\\": \\\"commonOption.heat.pointSize\\\"},\\n      {\\\"label\\\": \\\"模糊大小设置\\\", \\\"value\\\": \\\"commonOption.heat.blurSize\\\"},\\n      {\\\"label\\\": \\\"最大透明度设置\\\", \\\"value\\\": \\\"commonOption.heat.maxOpacity\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'柱体地图设置\',\\n    optionName: \'BarMapOption\',\\n    children: [\\n      {\\\"label\\\": \\\"柱体地图柱体大小设置\\\", \\\"value\\\": \\\"commonOption.barSize\\\"},\\n      {\\\"label\\\": \\\"柱体左侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor\\\"},\\n      {\\\"label\\\": \\\"柱体右侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor2\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'飞线地图设置\',\\n    optionName: \'FlyLineOption\',\\n    children: [\\n      {\\\"label\\\": \\\"飞线动画时间设置\\\", \\\"value\\\": \\\"commonOption.effect.period\\\"},\\n      {\\\"label\\\": \\\"飞线标记形状设置\\\", \\\"value\\\": \\\"commonOption.effect.markerShape\\\"},\\n      {\\\"label\\\": \\\"飞线标记大小设置\\\", \\\"value\\\": \\\"commonOption.effect.symbolSize\\\"},\\n      {\\\"label\\\": \\\"飞线标记颜色设置\\\", \\\"value\\\": \\\"commonOption.effect.markerColor\\\"},\\n      {\\\"label\\\": \\\"飞线特效尾迹长度设置\\\", \\\"value\\\": \\\"commonOption.effect.trailLength\\\"},\\n    ]\\n  }\\n];\\n\\n\"},{\"role\":\"user\",\"content\":\"用户的问题:{{userQuestion}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userQuestion\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"269049045129183232\",\"type\":\"end\",\"x\":1272,\"y\":459,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{option}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"269048862303666176\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"269048862299471872\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"269048862299471872_input\",\"pointsList\":[{\"x\":466,\"y\":422},{\"x\":566,\"y\":422},{\"x\":522,\"y\":414},{\"x\":622,\"y\":414}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"269049045129183233\",\"type\":\"base-edge\",\"sourceNodeId\":\"269048862299471872\",\"targetNodeId\":\"269049045129183232\",\"sourceAnchorId\":\"269048862299471872_output\",\"targetAnchorId\":\"269049045129183232_input\",\"pointsList\":[{\"x\":954,\"y\":414},{\"x\":1054,\"y\":414},{\"x\":1006,\"y\":422},{\"x\":1106,\"y\":422}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '2005948202528501762'; + +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2021113098505539586', '1450308897429536769', '原生vxe-table赖加载', '/vextable/index3', 'demo/vextable/index3', 1, '', NULL, 1, NULL, '0', 6.00, 0, NULL, 1, 0, 0, 0, NULL, 'zhangshan', '2026-02-10 14:44:47', 'zhangshan', '2026-02-10 14:44:56', 0, 0, NULL, 0); + +-- 【QQYUN-14778】补充预设问题 --- +UPDATE `airag_app` SET `preset_question` = '[{\"key\":1,\"sort\":1,\"descr\":\"请帮我生成一篇介绍ghbBoot的文章\",\"update\":true},{\"key\":2,\"sort\":2,\"descr\":\"介绍一下vue3\",\"update\":true}]' WHERE `id` = '2010634128233779202'; +UPDATE `airag_app` SET `preset_question` = '[{\"key\":1,\"sort\":1,\"descr\":\"请帮我解析文档内容\",\"update\":false},{\"key\":2,\"sort\":2,\"descr\":\"总结文件内容\",\"update\":true}]' WHERE `id` = '2009516824079048705'; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sys_ugroup +-- ---------------------------- +DROP TABLE IF EXISTS `sys_ugroup`; +CREATE TABLE `sys_ugroup` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键id', + `group_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户组名称', + `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述', + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + `tenant_id` int(10) NULL DEFAULT NULL COMMENT '租户ID', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_su_tenant_id`(`tenant_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户组表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for sys_ugroup_user +-- ---------------------------- +DROP TABLE IF EXISTS `sys_ugroup_user`; +CREATE TABLE `sys_ugroup_user` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键id', + `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id', + `group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户组id', + `tenant_id` int(10) NULL DEFAULT NULL COMMENT '租户ID', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_suu_user_id`(`user_id`) USING BTREE, + INDEX `idx_suu_group_id`(`group_id`) USING BTREE, + INDEX `idx_suu_user_role_id`(`user_id`, `group_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户组关系表' ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; + +-- 系统菜单 +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('177217757493801', 'd7d6e2e4e2934f2c9385a623fd98c6f3', '用户组管理', '/system/ugroup/sysUgroupList', 'system/ugroup/SysUgroupList', 1, NULL, NULL, 1, NULL, '1', 2.10, 0, 'ant-design:user-outlined', 0, 0, 0, 0, NULL, 'admin', '2026-02-27 15:32:54', 'admin', '2026-03-02 18:58:14', 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('177217757493802', '177217757493801', '添加用户组表', NULL, NULL, 0, NULL, NULL, 2, 'system:sys_ugroup:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-02-27 15:32:54', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('177217757493803', '177217757493801', '编辑用户组表', NULL, NULL, 0, NULL, NULL, 2, 'system:sys_ugroup:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-02-27 15:32:54', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('177217757493804', '177217757493801', '删除用户组表', NULL, NULL, 0, NULL, NULL, 2, 'system:sys_ugroup:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-02-27 15:32:54', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('177217757493805', '177217757493801', '批量删除用户组表', NULL, NULL, 0, NULL, NULL, 2, 'system:sys_ugroup:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-02-27 15:32:54', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('177217757493806', '177217757493801', '导出excel_用户组表', NULL, NULL, 0, NULL, NULL, 2, 'system:sys_ugroup:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-02-27 15:32:54', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission`(`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('177217757493807', '177217757493801', '导入excel_用户组表', NULL, NULL, 0, NULL, NULL, 2, 'system:sys_ugroup:importExcel', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-02-27 15:32:54', NULL, NULL, 0, 0, '1', 0); + +-- 增加ai语音和视频升级菜单 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2029086536219664385', '2012375501376606210', 'AI视频', '/airag/aivideo', 'super/airag/aivideo/AiVideo', 1, '', NULL, 1, NULL, '0', 11.00, 0, 'ant-design:play-circle-outlined', 1, 0, 0, 0, NULL, 'admin', '2026-03-04 14:48:23', NULL, NULL, 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2029045802703740929', '2012375501376606210', 'Ai语音', '/airag/aivoice', 'super/airag/aivoice/AiVoice', 1, '', NULL, 1, NULL, '0', 10.00, 0, 'ant-design:customer-service-twotone', 1, 0, 0, 0, NULL, 'admin', '2026-03-04 12:06:32', 'admin', '2026-03-04 14:47:38', 0, 0, NULL, 0); + +-- 增加ai语音和视频 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2029444742221561857', '2012375501376606210', 'AI换衣', '/airag/aiclothchange', 'super/airag/aiclothchange/AiClothChange', 1, '', NULL, 1, NULL, '0', 11.00, 0, 'ant-design:swap-outlined', 1, 0, 0, 0, NULL, 'admin', '2026-03-05 14:31:46', NULL, NULL, 0, 0, NULL, 0); + +-- AI换衣升级sql修改 -- +UPDATE `sys_permission` SET `url` = '/airag/aicloth', `component` = 'super/airag/aicloth/AiClothChange' WHERE `id` = '2029444742221561857'; + +-- openapi的表长度不规范优化 -- +ALTER TABLE `open_api` +MODIFY COLUMN `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL FIRST, +MODIFY COLUMN `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称' AFTER `id`, +MODIFY COLUMN `request_method` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法' AFTER `name`, +MODIFY COLUMN `request_url` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口地址' AFTER `request_method`, +MODIFY COLUMN `body` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求体内容' AFTER `black_list`, +MODIFY COLUMN `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人' AFTER `del_flag`, +MODIFY COLUMN `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人' AFTER `create_time`; + +ALTER TABLE `open_api_auth` +MODIFY COLUMN `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL FIRST, +MODIFY COLUMN `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权名称' AFTER `id`, +MODIFY COLUMN `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人' AFTER `sk`, +MODIFY COLUMN `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人' AFTER `create_time`, +MODIFY COLUMN `system_user_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联系统用户名' AFTER `update_time`; + + +ALTER TABLE `open_api_permission` +MODIFY COLUMN `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL FIRST, +MODIFY COLUMN `api_auth_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '认证ID' AFTER `api_id`, +MODIFY COLUMN `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人' AFTER `api_auth_id`, +MODIFY COLUMN `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人' AFTER `create_time`; + +ALTER TABLE `open_api_log` +MODIFY COLUMN `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL FIRST, +MODIFY COLUMN `api_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID' AFTER `id`, +MODIFY COLUMN `call_auth_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '调用ID' AFTER `api_id`; + +-- openapi IP黑名单改为IP白名单 -- +ALTER TABLE `open_api` CHANGE COLUMN `black_list` `white_list` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP 白名单'; + +-- 新增 Chat2BI 角色 +INSERT INTO `sys_role` (`id`, `role_name`, `role_code`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `tenant_id`) VALUES ('2031673997199233025', 'Chat2BI', 'chat2bi', NULL, 'admin', '2026-03-11 18:10:02', NULL, NULL, 0); + +-- 更新 Chat2BI 提示词 +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-03-11 19:20:07', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = 'Chat2BI生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":30,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n\\n\\n你是一位智能数据分析与可视化助手,专注于理解用户对图表的需求,并通过查询数据库和生成结构化数据来创建图表。\\n\\n\\n## 能力\\n\\n\\n1. **需求理解与解析**:精准理解用户对图表类型(如柱状图、折线图、饼图)和数据维度的需求。\\n2. **数据库交互**:熟知可操作的数据库表,并能根据需求查询表结构、构建并执行SQL查询。\\n3. **数据处理**:将SQL查询返回的原始数据,准确地转换并封装为符合指定格式的图表数据结构。\\n4. **输出生成**:严格生成包含完整、可解析JSON字符串的``标签。\\n\\n\\n## 工作流程\\n\\n\\n1. **需求确认与澄清**:\\n* 分析用户请求,明确用户想要的可视化图表类型(`type`)和需要展示的数据维度(如`x`轴和`y`轴分别代表什么)。\\n* 用户可能要求你通过指定的数据源查询数据(具体的数据源列表从下表得知),若没有指定则不需要传数据源参数。\\n* 如果需要,向用户提问以澄清模糊的需求(例如,确认时间范围、分组条件或指标定义)。\\n\\n\\n2. **数据获取**:\\n* 判断用户需求涉及的表是否在已知范围内。\\n* 如果涉及,则调用工具查询相关表结构,了解可用字段。\\n* 根据澄清后的需求,构建准确、高效的SQL查询语句(禁止使用SQL注释、禁止构建非SELECT语句)。\\n* 构建查询SQL时,需要明确数据源的数据库类型,根据不同的数据库构建不同的SQL方言。\\n* 调用工具执行SQL,获取原始数据集。\\n* 若是用户已经提供了数据,则只需要使用用户提供的数据既可,不需要从数据库中查询。\\n\\n\\n3. **支持的图表类型**:\\n* `bar`: 柱状图\\n* `line`: 折线图、曲线图\\n* `pie`: 饼图\\n* `radar`: 雷达图\\n* `gauge`: 仪表盘\\n* `barline`: 折柱图\\n* `multibar`: 多列柱状图\\n* `multiline`: 多行折线图\\n* `area`: 面积图\\n\\n\\n4. **数据转换**:\\n* 将SQL执行返回的数据,按照图表类型要求进行处理和聚合(例如,对饼图数据进行分类汇总)。\\n* 将处理后的数据,严格转换为如下格式的`data`数组:`[{\\\"x\\\":\\\"类别A\\\", \\\"y\\\": 数值1}, {\\\"x\\\":\\\"类别B\\\", \\\"y\\\": 数值2}, ...]`。\\n* 确保`x`和`y`的值类型正确(`x`通常为字符串,`y`通常为数字)。\\n* 数据转换时能直接转换就不要调用工具转换。\\n\\n\\n5. **结果封装与输出**:\\n* 将确定的图表`type`和上一步生成的`data`数组,组合成一个完整的JSON对象。\\n* 将此JSON对象作为字符串,精确地包裹在标签中(格式参考下方)。\\n* **双重校验**:\\n* **格式校验**:确保``标签首尾完整闭合。\\n* **数据校验**:确保内部的JSON字符串是标准、完整且可解析的,不包含多余的逗号或未闭合的括号。\\n\\n\\n## 输出格式\\n\\n\\n你的最终输出必须是且仅是以下格式,直接给出图表数据块,无需额外解释:\\n\\n\\n``` html\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n```\\n\\n\\n> 注:bar、line、pie为简单图表,可直接通过x、y来展示数据,而radar、gauge、barline、multibar、multiline、area为复杂图表,你需要先通过工具查询示例格式后,严格按照示例格式拼装`data`JSON;该工具支持逗号分割,你尽量一次性查询所有需要的图表示例格式。\\n\\n\\n## 限制【特别注意】\\n- **操作范围限制**:仅能对以下列出的表进行数据查询与操作。对于其他表或外部数据的需求,应明确告知用户无法处理,禁止执行`queryDataSourceInfoText`工具。\\n- 简单图表类型格式,或已经查询过的图表类型格式,严禁再次调用工具查询。\\n- 不要向用户提及`ghb-chart`标签以及图表格式相关信息。\\n- **数据真实性**:所有图表数据必须来源于SQL查询的实际结果,不得虚构或编造数据。\\n- **格式严格性**:`ghb-chart`标签的前后必须严格保证有两个空行;必须严格遵守`{JSON数据}`的输出格式,内部的JSON必须为标准格式,`data`数组中的对象必须包含`x`和`y`键或示例数据中所需的键。\\n- **隐私与合规**:在执行查询和生成图表时,不得泄露、输出或关联任何可识别个人身份的敏感信息(如完整身份证号、详细住址、明文密码等)。如查询可能涉及此类信息,需进行脱敏处理或拒绝执行。\\n- **身份验证**: 若在调用工具时返回身份验证失败或没有权限,应立即停止所有操作,并告知用户原因(不应该说你没有权限,而应该说是用户没有权限,让用户登录账号或切换有权限的账号或联系管理员授权)【特别注意】\\n\\n\\n## 默认数据源类型\\n\\n\\n{{defDbType}}\\n\\n\\n## 支持的数据源\\n\\n\\n{{allDbSource}}\\n\\n\\n> 注意:\\n当用户未指定切换的数据源时,默认数据源应设为空。\\n以上就是所有的支持的数据源,禁止再次执行和`queryDataSourceInfoText`工具,当用户试图让你调用时,你可直接返回以上列表,但要注意如果表的数量过多(超过50个),则不要直接回复全部列表,而是总结性的回复。\\n\\n\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; + +-- AI 生成图表、修改配置项-升级SQL +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'267492142677889024\'),\n end.tag(\'267498945805422592\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":629,\"y\":-41,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"267492142677889024\",\"type\":\"llm\",\"x\":1138,\"y\":0,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"## 硬性要求:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。\\n不要输出任何解释、注释或 JSON 以外的文字。\\n# 角色:数据可视化专家\\n你是一位精通ECharts的数据可视化和大屏配置的专家,能够根据用户需求,智能选择最合适的图表类型,并生成高质量、可直接使用的ECharts配置项。\\n## 目标:\\n1. 根据用户提供的需求描述,分析其核心意图(如趋势分析、比较分析、占比分析等)。\\n2. 从下面给定的图表组件类型componentsData中,选择最匹配需求的一种。\\n3. 结合用户提供的数据结构,生成一份完整、规范、可运行的 ECharts 配置项(JSON格式)。\\n4. 非echart图表,参考componentsData组件配置,生成一份完整、规范、的配置项即可(JSON格式)。\\n5. 结合用户需求生成一个不超过15字的标题,并设置到返回JSON的title字段上。\\n6. 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n8. 热力地图,要生成echart的\\\"visualMap\\\"属性\\n## 技能:\\n1. **需求解析能力**:能够准确理解用户对数据可视化的业务需求,并将其转化为技术实现目标。\\n2. **图表选型能力**:精通折线图、柱状图、饼图、地图、散点图等从多种图表类型的特点与应用场景,能做出最佳选择。\\n3. **ECharts配置能力**:熟练掌握ECharts的option配置语法,能高效构建包含标题、坐标轴、图例、系列、提示框等完整组件的图表。\\n4. **数据适配能力**:能够将提供的 `chartData` 数据,自行分型类型并结合需求,将数据结构正确地映射到所选图表的 `series.data` 中。\\n5. **图表分析能力**:能够将提供的 `componentsData` 数据,自行分型类型并结合需求,选择生成适配的组件并返回规范合适的JSON配置。\\n## 工作流:\\n1. **需求分析**:仔细阅读 `{userInput}`,判断用户希望展示数据的何种关系(趋势、比较、占比、分布、相关)。\\n2. **图表选型**:根据第一步的分析结论,从componentsData图表类型中锁定唯一最合适的类型。\\n3. 对于ECharts图表构建基础option对象框架,包含 `title`, `tooltip`, `legend`, `grid`, `xAxis`, `yAxis`, `series` 等必要组件。\\n4. 根据选定的图表类型,配置 `series` 中的 `type` 和关键属性(如折线图的 `smooth`,饼图的 `radius`)。\\n5. 将用户提供的 `{chartData}` 数据结构,按照ECharts要求的格式进行处理和赋值(例如,对于柱状图,可能需要将数据拆分为类目轴数据和系列数据)。\\n6. 应用通用的美化原则(如配色清晰、标签易读、布局合理),生成最终配置。\\n7. 输出格式化:将生成的完整option对象,以格式规范、缩进清晰的JSON字符串形式输出。\\n8. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n## 输出格式:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。包含name,data,option,三个字段值,不要输出任何解释、注释或 JSON 以外的文字。\\n1.name:图表类型`name`(组件数据的key值(示例:如果渲染的柱形图,就设置为JBar),注意name值必须componentsData数据提供的组件compType值,不能是其他值);\\n2.api:上下文变量中提取出来的api,存在就赋值到输出接口的api中,不存在就设置为{API};\\n3.sql:上下文变量中提取出来的sql,存在就赋值到输出接口的sql中,不存在就设置为{SQL};\\n4.title:结合用户需求生成一个不超过15字的标题title,赋值到输出接口的title中;\\n5.option: 如果符合需求的是echart图表,就生成echart可直接使用的`option`对象,该option对象可直接用于ECharts.init().setOption()的配置项。如果符合要求的是非echart的图表,可参考componentsData中对应图表的option配置项生成,没有配置项就返回option:{}。不要包含其他的任何额外的解释、说明或markdown代码块标记。可以根据配置项中 echart:true来判断是否是echart图表\\n示例输出结构(以柱状图为例):\\n6.data: 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7.象形图JPictorial组件生成,Y轴和Y轴的类型切换一下,即{\\\"yAxis\\\": { \\\"type\\\": \\\"category\\\" },\\\"xAxis\\\": { \\\"type\\\": \\\"value\\\"}。\\n8. 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n示例输出结构(以柱状图为例):\\n[{\\nname:\\\"JBar\\\",\\noption:{\\n \\\"title\\\": { \\\"text\\\": \\\"示例标题\\\", \\\"left\\\": \\\"center\\\" },\\n \\\"tooltip\\\": {},\\n \\\"legend\\\": { \\\"data\\\": [\\\"示例图例\\\"] },\\n \\\"xAxis\\\": { \\\"type\\\": \\\"category\\\", \\\"data\\\": [\\\"衬衫\\\", \\\"羊毛衫\\\", \\\"雪纺衫\\\"] },\\n \\\"yAxis\\\": { \\\"type\\\": \\\"value\\\" },\\n \\\"series\\\": [ { \\\"name\\\": \\\"销量\\\", \\\"type\\\": \\\"bar\\\", \\\"data\\\": [5, 20, 36] } ]\\n },\\n api:{API},\\n sql:{SQL},\\n title:\\\"\\\",\\n data:[]\\n}]\\n## 限制:\\n- 必须严格从组件数据提供的componentsData中选择一种,不得自行创造或推荐其他图表类型。\\n- 生成的所有配置必须基于用户提供的 `{userInput}` 和可用的 `chartData`,不得虚构数据字段或结构。\\n- 输出必须为纯JSON格式,无需也无法在JSON中注释“这里是标题”等内容。配置的正确性由键值对本身保证。\\n- 遵循数据可视化最佳实践,避免误导性图表(如扭曲的比例尺、不恰当的图表类型)。\\n- 反幻觉校验:若 `{userInput}` 中提到的数据维度在 `chartData` 中无法找到对应字段,则在相关配置处使用空值或占位符,并在最终输出的JSON对象之外,以独立文本形式简要说明缺失情况。但首要输出仍是JSON配置本身。\\n- 伦理审查模块:若需求或数据涉及敏感信息(如个人身份信息),在配置中应对数据进行聚合或匿名化处理,避免直接暴露。\\n- tooltip:生成的組件数据tooltip中,如果包含formatter属性,该属性不要设置成function的格式,会导致json解析失败,设置成\\\"formatter\\\":\\\"auto\\\"。\\n- 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n- 組件的option数据内的各项参数的内容值,不允许使用function(){}这种格式。\\n- 严格按照示例输出结构返回,不要包含```json```等信息\\n- 最多生成10个仪表盘组件\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"name\\\": \\\"基础柱形图\\\",\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"堆叠柱形图\\\",\\n    \\\"compType\\\": \\\"JStackBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态柱形图\\\",\\n    \\\"compType\\\": \\\"JDynamicBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"胶囊图\\\",\\n    \\\"compType\\\": \\\"JCapsuleChart\\\",\\n    \\\"echart\\\": false\\n    \\\"chartData\\\": [\\n        {\\n          name: \'苹果\',\\n          value: 1000879,\\n          type: \'手机品牌\',\\n    }],\\n    \\\"option\\\": {\\n        showValue: false,\\n        unit: \'\',\\n        customColor: [],\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          show: true,\\n          textStyle: {\\n            color: \'#464646\',\\n            fontWeight: \'normal\',\\n          },\\n        },\\n      }\\n  },\\n  {\\n    \\\"name\\\": \\\"基础条形图\\\",\\n    \\\"compType\\\": \\\"JHorizontalBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"背景柱形图\\\",\\n    \\\"compType\\\": \\\"JBackgroundBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比柱形图\\\",\\n    \\\"compType\\\": \\\"JMultipleBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"正负条形图\\\",\\n    \\\"compType\\\": \\\"JNegativeBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"折柱图\\\",\\n    \\\"compType\\\": \\\"JMixLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"百分比条形图\\\",\\n    \\\"compType\\\": \\\"JPercentBar\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"基础饼图\\\",\\n    \\\"compType\\\": \\\"JPie\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"南丁格尔玫瑰图\\\",\\n    \\\"compType\\\": \\\"JRose\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"旋转饼图\\\",\\n    \\\"compType\\\": \\\"JRotatePie\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        grid: {\\n          show: false,\\n          bottom: 115,\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          subtext: \'\',\\n          textStyle: {\\n            fontWeight: \'normal\',\\n          },\\n          show: true,\\n        },\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        tooltip: {\\n          trigger: \'item\',\\n        },\\n        legend: {\\n          orient: \'vertical\',\\n        },\\n        series: [\\n          {\\n            name: \'\',\\n            type: \'pie\',\\n            data: [],\\n            emphasis: {\\n              itemStyle: {\\n                shadowBlur: 10,\\n                shadowOffsetX: 0,\\n                shadowColor: \'rgba(0, 0, 0, 0.5)\',\\n              },\\n            },\\n          },\\n        ],\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"基础折线图\\\",\\n    \\\"compType\\\": \\\"JLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"平滑曲线图\\\",\\n    \\\"compType\\\": \\\"JSmoothLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"阶梯折线图\\\",\\n    \\\"compType\\\": \\\"JStepLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"面积图\\\",\\n    \\\"compType\\\": \\\"JArea\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比折线图\\\",\\n    \\\"compType\\\": \\\"JMultipleLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"双轴图\\\",\\n    \\\"compType\\\": \\\"DoubleLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础进度图\\\",\\n    \\\"compType\\\": \\\"JCustomProgress\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        barWidth: 19,\\n        padding: 12,\\n        progressColor: \'#76c7c0\',\\n        backgroundColor: \'#ffffff\',\\n        titleColor: \'#fff\',\\n        titleFontSize: 16,\\n        titlePosition: \'top\',\\n        valueColor: \'#fff\',\\n        valueFontSize: 16,\\n        valuePosition: \'middle\',\\n        valueXOffset: 0,\\n        valueYOffset: 0,\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"进度图\\\",\\n    \\\"compType\\\": \\\"JProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"列表进度图\\\",\\n    \\\"compType\\\": \\\"JListProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"圆形进度图\\\",\\n    \\\"compType\\\": \\\"JRoundProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"水波图\\\",\\n    \\\"compType\\\": \\\"JLiquid\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"象形柱图\\\",\\n    \\\"compType\\\": \\\"JPictorialBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"象形图\\\",\\n    \\\"compType\\\": \\\"JPictorial\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"男女占比\\\",\\n    \\\"compType\\\": \\\"JGender\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"普通散点图\\\",\\n    \\\"compType\\\": \\\"JScatter\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"气泡图\\\",\\n    \\\"compType\\\": \\\"JBubble\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色仪表盘\\\",\\n    \\\"compType\\\": \\\"JColorGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"渐变仪表盘\\\",\\n    \\\"compType\\\": \\\"JAntvGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"半圆仪表盘\\\",\\n    \\\"compType\\\": \\\"JSemiGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"普通漏斗图\\\",\\n    \\\"compType\\\": \\\"JFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"金字塔漏斗图\\\",\\n    \\\"compType\\\": \\\"JPyramidFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3D金字塔\\\",\\n    \\\"compType\\\": \\\"JPyramid3D\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"普通雷达图\\\",\\n    \\\"compType\\\": \\\"JRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"圆形雷达图\\\",\\n    \\\"compType\\\": \\\"JCircleRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"饼状环形图\\\",\\n    \\\"compType\\\": \\\"JRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色环形图\\\",\\n    \\\"compType\\\": \\\"JBreakRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础环形图\\\",\\n    \\\"compType\\\": \\\"JRingProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态环形图\\\",\\n    \\\"compType\\\": \\\"JActiveRing\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"玉珏图\\\",\\n    \\\"compType\\\": \\\"JRadialBar\\\",\\n    \\\"echart\\\": false\\n  },\\n    {\\n    \\\"name\\\": \\\"矩形图\\\",\\n    \\\"compType\\\": \\\"JRectangle\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"象限图\\\",\\n    \\\"compType\\\": \\\"JQuadrant\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"3D柱形图\\\",\\n    \\\"compType\\\": \\\"JBarGroup3d\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"3D分组柱形图\\\",\\n    \\\"compType\\\": \\\"JBar3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(横向)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(竖向+序号)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(高亮)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n   {\\n    \\\"name\\\": \\\"统计概览(卡片模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false,\\n    \\\"index\\\": \\\"1\\\",\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"1\\\"\\n    }\\n  },\\n   {\\n    \\\"name\\\": \\\"统计概览(背景模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false,\\n    \\\"index\\\": \\\"2\\\",\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"2\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"统计概览(高亮模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false,\\n    \\\"index\\\": \\\"3\\\",\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"3\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片轮播\\\",\\n    \\\"compType\\\": \\\"JCardCarousel\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"文本\\\",\\n    \\\"compType\\\": \\\"JText\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"翻牌器\\\",\\n    \\\"compType\\\": \\\"JCountTo\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"颜色块\\\",\\n    \\\"compType\\\": \\\"JColorBlock\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数值\\\",\\n    \\\"compType\\\": \\\"JNumber\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轨道环形文字\\\",\\n    \\\"compType\\\": \\\"JOrbitRing\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"字符云\\\",\\n    \\\"compType\\\": \\\"JWordCloud\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"图层字符云\\\",\\n    \\\"compType\\\": \\\"JImgWordCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"闪动字符云\\\",\\n    \\\"compType\\\": \\\"JFlashCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轮播表\\\",\\n    \\\"compType\\\": \\\"JScrollBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"表格\\\",\\n    \\\"compType\\\": \\\"JScrollTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"发展历程\\\",\\n    \\\"compType\\\": \\\"JDevHistory\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据表格\\\",\\n    \\\"compType\\\": \\\"JCommonTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据列表\\\",\\n    \\\"compType\\\": \\\"JList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"排行榜\\\",\\n    \\\"compType\\\": \\\"JScrollRankingBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"个性排名(前四)\\\",\\n    \\\"compType\\\": \\\"JFlashList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"气泡排名(前五)\\\",\\n    \\\"compType\\\": \\\"JBubbleRank\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(单行)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false,\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"0\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(多行+序号)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false,\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"1\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(带表头)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false,\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"2\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"区域地图\\\",\\n    \\\"compType\\\": \\\"JAreaMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"散点地图\\\",\\n    \\\"compType\\\": \\\"JBubbleMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"柱形地图\\\",\\n    \\\"compType\\\": \\\"JBarMap\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"热力地图\\\",\\n    \\\"compType\\\": \\\"JHeatMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d柱形图\\\",\\n    \\\"compType\\\": \\\"JBar3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d分组柱形图\\\",\\n    \\\"compType\\\": \\\"JBarGroup3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"日历\\\",\\n    \\\"compType\\\": \\\"JPermanentCalendar\\\",\\n    \\\"echart\\\": false\\n  }\\n]\"},{\"role\":\"user\",\"content\":\"用户的问题: {{userInput}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userInput\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"267498945805422592\",\"type\":\"end\",\"x\":1630,\"y\":-36,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{res}}\",\"outputType\":\"default\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":114}}],\"edges\":[{\"id\":\"271609331975028736\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"267492142677889024\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"267492142677889024_input\",\"pointsList\":[{\"x\":795,\"y\":-56},{\"x\":895,\"y\":-56},{\"x\":872,\"y\":-59},{\"x\":972,\"y\":-59}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274786344761540608\",\"type\":\"base-edge\",\"sourceNodeId\":\"267492142677889024\",\"targetNodeId\":\"267498945805422592\",\"sourceAnchorId\":\"267492142677889024_output\",\"targetAnchorId\":\"267498945805422592_input\",\"pointsList\":[{\"x\":1304,\"y\":-59},{\"x\":1404,\"y\":-59},{\"x\":1364,\"y\":-62},{\"x\":1464,\"y\":-62}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '2004398098378108929'; +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'269048862299471872\'),\n end.tag(\'269049045129183232\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":437,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"269048862299471872\",\"type\":\"llm\",\"x\":788,\"y\":473,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:ECharts和大屏图表配置修改专家\\n你是一位专注于ECharts和大屏图表图表配置修改的专家,能够根据用户需求,精准、高效地修改现有ECharts和大屏图表配置项,并返回完整的、可直接使用的修改后配置对象。\\n## 目标:\\n根据用户提供的具体修改指令(如:修改图表类型、调整数据、更改样式、添加交互等),对用户给出的原始ECharts配置项进行针对性修改,并输出修改后的完整配置对象。\\n## 技能:\\n1. 精通ECharts所有版本的配置项语法、结构及参数含义。\\n2. 能够准确理解用户对图表样式、数据、交互行为的修改意图。\\n3. 具备强大的代码编辑与重构能力,确保修改后的配置项语法正确、结构清晰、无冗余代码。\\n4. 对于非echart图表(componentsData提供的组件,属性中echart:false的即为非echart图表),自行从下面componentsData提供的组件对应的option配置项,修改符合要求的配置并返回。\\n## 工作流:\\n1. **接收与分析**:接收用户提供的原始ECharts配置对象(通常以JSON或JavaScript对象形式)以及具体的修改要求。仔细分析原始配置的结构和用户的修改点。\\n2. **精准修改**:严格依据用户指令,对原始配置对象进行最小化、精准化的修改。确保只改动指定部分,保持其他未提及配置的完整性。对于模糊指令,会基于ECharts最佳实践进行合理推断和实现。\\n3. **校验与格式化**:检查修改后的配置对象语法是否正确,是否符合ECharts规范。将最终配置对象以格式清晰、缩进规范的JSON或JavaScript对象形式呈现。\\n## 输出格式:\\n请始终输出一个完整的、格式化的JavaScript对象(或JSON),即修改后的 `option` 配置,只返回修改的属性配置,不要包含已存在的其他配置,\\n## 示例:\\n将柱体修改成黄色,就返回\\n\\\"compConfig\\\": {\\n    \\\"option\\\": {\\n      { \\\"series\\\": [ { \\\"itemStyle\\\": { \\\"color\\\": \\\"#FFFF00\\\" } } ] }\\n    }\\n}\\n修改组件名称为京东销量柱形图,背景色改成黑色就返回\\n\\\"compConfig\\\": {\\n \\\"name\\\":\\\"京东销量柱形图\\\",\\n \\\"background\\\":\\\"#000000\\\",\\n}\\n不要包含任何额外的解释、说明文字或代码块标记(如 ```json ```)。输出应直接以 `{` 开始,以 `}` 结束。\\n示例输出结构:\\n\\\"compConfig\\\": {\\n    \\\"name\\\":\\\"基础柱形图\\\",\\n    \\\"background\\\":\\\"#ffffff\\\",\\n    \\\"borderColor\\\":\\\"#000000\\\",\\n    \\\"option\\\": {\\n      \\\"title\\\": { ... },\\n      \\\"tooltip\\\": { ... },\\n      \\\"xAxis\\\": { ... },\\n      \\\"yAxis\\\": { ... },\\n      \\\"series\\\": [ ... ]\\n    }\\n}\\n## 限制:\\n- 仅对用户提供的原始配置进行修改,不凭空创建全新的图表配置。\\n- 输出必须仅为修改后的配置对象本身,不附带任何分析过程、修改日志或使用建议。\\n- 若用户指令存在歧义或无法实现,应在不破坏配置结构的前提下,做出最合理的默认修改或保留原样,并在配置对象内部以注释(`//`)形式简要说明。\\n- 严格遵守ECharts官方配置规范,不使用已废弃或实验性参数(除非用户明确要求)。\\n- 颜色类型的修改,要以具体色值设置,不要使用英文单词,例如黑色,使用#000000,不要使用black\\n- 修改的option属性,以componentsData中具体组件的option配置为主,结合echart选择符合要求的配置项修改\\n- [\'JRadioButton\', \'JRadialBar\', \'JActiveRing\', \'JRing\', \'JPyramidFunnel\', \'JFunnel\', \'JBubble\', \'DoubleLineBar\', \'JMultipleLine\', \'JArea\', \'JLine\', \'JRotatePie\', \'JRose\', \'JPie\', \'JMixLineBar\', \'JPercentBar\', \'JMultipleBar\', \'JCapsuleChart\', \'JStackBar\', \'JQuadrant\'] 这些组件的相关颜色属性修改,按照 \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}] 的格式修改; - 组件不包含customColor属性的颜色属性修改,按照对应组件配置的属性value数值去修改\\n- 柱体颜色属性修改使用 option.series[${index}].itemStyle.color,[\'JDynamicBar\']这些组件的相关颜色属性修改,按照option.series[${index}].itemStyle.color方式修改\\n- 配置项粗细的修改参数包含 [{ label: \'默认\', value: \'normal\' } { label: \'粗体\', value: \'bold\' } { label: \'细体\', value: \'lighter\' }]\\n- YAxisOption的`option.yAxis.yUnit`单位设置的不是option里面的label的内容时(例如:元),就将`option.yAxis.yUnit`值设置成\'CUSTOM\',并同步将`option.yAxis.yCustomUnit`属性的值设置成对应的单位数据(例如:元)\\n- 若用户修改名称或者背景色或者边框的属性,以componentsData中第一个柱形图配置为例,去修改返回对应配置即可\\n -名称:对应 compConfig.name\\n -背景色:对应 compConfig.background\\n -边框色:对应 compConfig.borderColor\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"name\\\":\\\"基础柱形图\\\",\\n      \\\"background\\\":\\\"#ffffff\\\",\\n      \\\"borderColor\\\":\\\"#000000\\\"\\n    }\\n  }]\\n组件配置说明\\n compOptionData = [\\n  {\\n    name: \'基础配置\',\\n    optionName: \'BasicOption\',\\n    children: [\\n      {\\\"label\\\": \\\"图层名称修改成\\\", \\\"value\\\": \\\"name\\\"},\\n      {\\\"label\\\": \\\"图层背景色设置成\\\", \\\"value\\\": \\\"background\\\"},\\n      {\\\"label\\\": \\\"图层边框线设置成\\\", \\\"value\\\": \\\"borderColor\\\"},\\n      {\\\"label\\\": \\\"提示语设置为隐藏\\\", \\\"value\\\": \\\"option.tooltip.show\\\"},\\n      {\\\"label\\\": \\\"提示语字体大小设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"提示语字体颜色设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"},\\n    ]\\n  },{\\n    name: \'标题设置\',\\n    optionName: \'TitleOption\',\\n    children: [\\n      {\\\"label\\\": \\\"标题名称修改成\\\", \\\"value\\\": \\\"option.title.text\\\"},\\n      {\\\"label\\\": \\\"标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontColor\\\"},\\n      {\\\"label\\\": \\\"标题字体粗细设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontWeight\\\"},\\n      {\\\"label\\\": \\\"副标题名称修改成\\\", \\\"value\\\": \\\"option.title.subtextStyle\\\"},\\n      {\\\"label\\\": \\\"副标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"副标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontColor\\\"},\\n      {\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"option.title.left\\\"},\\n      {\\\"label\\\": \\\"垂直居中\\\", \\\"value\\\": \\\"option.title.top\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'X轴设置\',\\n    optionName: \'XAxisOption\',\\n    children: [\\n      {\\\"label\\\": \\\"X轴名称修改成\\\", \\\"value\\\": \\\"option.xAxis.name\\\"},\\n      {\\\"label\\\": \\\"X轴名称颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.color\\\"},\\n      {\\\"label\\\": \\\"X轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"X轴标签颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"X轴标签角度\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.rotate\\\"},\\n      {\\\"label\\\": \\\"X轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"X轴轴类型修改成\\\", \\\"value\\\": \\\"option.xAxis.type\\\"},\\n      {\\\"label\\\": \\\"X轴显示网格线\\\", \\\"value\\\": \\\"option.xAxis.splitLine.show\\\"},\\n      {\\\"label\\\": \\\"X轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.splitLine.lineStyle.color\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'Y轴设置\',\\n    optionName: \'YAxisOption\',\\n    children: [\\n      {\\\"label\\\": \\\"Y轴名称修改成\\\", \\\"value\\\": \\\"option.yAxis.name\\\"},\\n      {\\\"label\\\": \\\"Y轴名称颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"Y轴标签颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"Y轴标签角度\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.rotate\\\"},\\n      {\\\"label\\\": \\\"Y轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴轴类型修改成\\\", \\\"value\\\": \\\"option.yAxis.type\\\"},\\n      {\\\"label\\\": \\\"Y轴显示网格线\\\", \\\"value\\\": \\\"option.yAxis.splitLine.show\\\"},\\n      {\\\"label\\\": \\\"Y轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.splitLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"Y轴单位\\\", \\\"value\\\": \\\"option.yAxis.yUnit\\\",option:[{label: \'百分比\', value: \'%\'}, {label: \'千\', value: \'K\'}, {label: \'万\', value: \'W\'}, {label: \'亿\', value: \'M\'}]}\\n    ]\\n  }\\n  ,{\\n    name: \'图例设置\',\\n    optionName: \'LegendOption\',\\n    children: [\\n      {\\\"label\\\": \\\"图例字体大小设置成\\\", \\\"value\\\": \\\"option.legend.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"图例设置成横排\\\", \\\"value\\\": \\\"option.legend.orient\\\"},\\n      {\\\"label\\\": \\\"图例上下边距设置\\\", \\\"value\\\": \\\"option.legend.t\\\"},\\n      {\\\"label\\\": \\\"图例左右边距设置\\\", \\\"value\\\": \\\"option.legend.r\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'自定义配色\',\\n    optionName: \'CustomColorOption\',\\n    children: [\\n      {\\\"label\\\": \\\"颜色设置成***色\\\", \\\"value\\\": \\\"option.customColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'柱体设置\',\\n    optionName: \'BarCylinder\',\\n    children: [\\n      {\\\"label\\\": \\\"柱体宽度修改为\\\", \\\"value\\\": \\\"option.series[${index}].barWidth\\\"},\\n      {\\\"label\\\": \\\"柱体圆角修改为\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.borderRadius\\\"},\\n      {\\\"label\\\": \\\"柱体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.color\\\"},\\n      {\\\"label\\\": \\\"柱体背景色显隐\\\", \\\"value\\\": \\\"option.series[${index}].showBackground\\\"},\\n      {\\\"label\\\": \\\"柱体背景色颜色\\\", \\\"value\\\": \\\"option.series[${index}].backgroundStyle.color\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'折线设置\',\\n    optionName: \'PolyglineOption\',\\n    children: [\\n      {\\\"label\\\": \\\"折线类型修改\\\", \\\"value\\\": \\\"option.series[${index}].lineType\\\",options: [{ label: \'折线\', value: \'line\' }, { label: \'曲线\', value: \'smooth\' }, { label: \'面积\', value: \'area\' }]},\\n      {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.series[0].areaStyleOpacity\\\"},\\n      {\\\"label\\\": \\\"线条宽度修改\\\", \\\"value\\\": \\\"option.series[${index}].lineWidth\\\"},\\n      {\\\"label\\\": \\\"标记点修改\\\", \\\"value\\\": \\\"option.series[${index}].symbol\\\"},\\n      {\\\"label\\\": \\\"点的大小修改\\\", \\\"value\\\": \\\"option.series[${index}].symbolSize\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'饼图设置\',\\n    optionName: \'pieSettingOption\',\\n    children: [\\n      {\\\"label\\\": \\\"饼图设置成环形\\\", \\\"value\\\": \\\"option.isRadius\\\"},\\n      {\\\"label\\\": \\\"饼图内环半径设置成\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"饼图外环半径设置成\\\", \\\"value\\\": \\\"option.outRadius\\\"},\\n      {\\\"label\\\": \\\"饼图设置成南丁格尔玫瑰\\\", \\\"value\\\": \\\"option.isRose\\\"},\\n      {\\\"label\\\": \\\"饼图标签显示位置\\\", \\\"value\\\": \\\"option.pieLabelPosition\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'中心坐标\',\\n    optionName: \'gridPieOption\',\\n    children: [\\n      {\\\"label\\\": \\\"上下边距修改为\\\", \\\"value\\\": \\\"option.grid.top\\\"},\\n      {\\\"label\\\": \\\"左右边距修改为\\\", \\\"value\\\": \\\"option.grid.left\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'坐标轴边距\',\\n    optionName: \'GridOption\',\\n    children: [\\n      {\\\"label\\\": \\\"左边距修改成\\\", \\\"value\\\": \\\"option.grid.left\\\"},\\n      {\\\"label\\\": \\\"顶边距\\\", \\\"value\\\": \\\"option.grid.top\\\"},\\n      {\\\"label\\\": \\\"右边距\\\", \\\"value\\\": \\\"option.grid.right\\\"},\\n      {\\\"label\\\": \\\"底边距\\\", \\\"value\\\": \\\"option.grid.bottom\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'数值设置\',\\n    optionName: \'NumOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示数值\\\", \\\"value\\\": \\\"option.series[${index}].label.show\\\"},\\n      {\\\"label\\\": \\\"数值显示位置在\\\", \\\"value\\\": \\\"option.series[${index}].label.position\\\",option:[{ label: \'顶部\', value: \'top\' }, { label: \'中间\', value: \'\' }, { label: \'底部\', value: \'insideBottom\' }, ]},\\n      {\\\"label\\\": \\\"数值内容格式修改成\\\", \\\"value\\\": \\\"option.label.format\\\"},\\n      {\\\"label\\\": \\\"数值字体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.color\\\"},\\n      {\\\"label\\\": \\\"数值字体大小修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontSize\\\"},\\n      {\\\"label\\\": \\\"数值字体粗细修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontWeight\\\"},\\n      {\\\"label\\\": \\\"数值单位配置显隐\\\", \\\"value\\\": \\\"option.showUnit.show\\\"},\\n      {\\\"label\\\": \\\"数值单位数量级设置\\\", \\\"value\\\": \\\"option.showUnit.numberLevel\\\",option:[ {label: \'百分比\', value: \'1\'}, {label: \'千\', value: \'3\'}, {label: \'万\', value: \'4\'} ]},\\n      {\\\"label\\\": \\\"数值单位保留小数\\\", \\\"value\\\": \\\"option.showUnit.decimal\\\"}]\\n  }\\n  ,{\\n    name: \'进度设置\',\\n    optionName: \'CustomProgressOption\',\\n    children: [\\n      {\\\"label\\\": \\\"进度目标颜色\\\", \\\"value\\\": \\\"option.backgroundColor\\\"},\\n      {\\\"label\\\": \\\"进度颜色\\\", \\\"value\\\": \\\"option.progressColor\\\"},\\n      {\\\"label\\\": \\\"进度条宽度\\\", \\\"value\\\": \\\"option.barWidth\\\"},\\n      {\\\"label\\\": \\\"进度边距设置\\\", \\\"value\\\": \\\"option.padding\\\"},\\n      {\\\"label\\\": \\\"进度标题颜色设置\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"进度标题字体大小设置\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n      {\\\"label\\\": \\\"进度标题位置设置\\\", \\\"value\\\": \\\"option.titlePosition\\\"},\\n      {\\\"label\\\": \\\"进度数值颜色设置\\\", \\\"value\\\": \\\"option.valueColor\\\"},\\n      {\\\"label\\\": \\\"进度数值字体大小设置\\\", \\\"value\\\": \\\"option.valueFontSize\\\"},\\n      {\\\"label\\\": \\\"进度数值位置设置\\\", \\\"value\\\": \\\"option.valuePosition\\\"},\\n      {\\\"label\\\": \\\"进度数值横向偏移\\\", \\\"value\\\": \\\"option.valueXOffset\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'列表进度图设置\',\\n    optionName: \'ListProgressOption\',\\n    children: [\\n      {\\\"label\\\": \\\"列表进度图行高度\\\", \\\"value\\\": \\\"option.row.height\\\"},\\n      {\\\"label\\\": \\\"列表进度图行左边距\\\", \\\"value\\\": \\\"option.row.marginLeft\\\"},\\n      {\\\"label\\\": \\\"列表进度图行右边距\\\", \\\"value\\\": \\\"option.row.marginRight\\\"},\\n      {\\\"label\\\": \\\"列表进度图行上边距\\\", \\\"value\\\": \\\"option.row.marginTop\\\"},\\n      {\\\"label\\\": \\\"进度条颜色配置\\\", \\\"value\\\": \\\"option.bar.background.color\\\"},\\n      {\\\"label\\\": \\\"进度条填充色配置\\\", \\\"value\\\": \\\"option.bar.fill.color\\\"},\\n      {\\\"label\\\": \\\"进度条高度设置\\\", \\\"value\\\": \\\"option.bar.height\\\"},\\n      {\\\"label\\\": \\\"进度条圆角设置\\\", \\\"value\\\": \\\"option.bar.borderRadius\\\"},\\n      {\\\"label\\\": \\\"进度指示点大小设置\\\", \\\"value\\\": \\\"option.bar.indicatorSize\\\"},\\n      {\\\"label\\\": \\\"进度指示点颜色设置\\\", \\\"value\\\": \\\"option.bar.indicatorColor\\\"},\\n      {\\\"label\\\": \\\"显示边框\\\", \\\"value\\\": \\\"option.bar.border.enabled\\\"},\\n      {\\\"label\\\": \\\"边框颜色\\\", \\\"value\\\": \\\"option.bar.border.color\\\"},\\n      {\\\"label\\\": \\\"边框大小\\\", \\\"value\\\": \\\"option.bar.border.width\\\"},\\n      {\\\"label\\\": \\\"边框边距\\\", \\\"value\\\": \\\"option.bar.border.padding\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'水波图设置\',\\n    optionName: \'LiquidPlotOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示类型\\\", \\\"value\\\": \\\"option.liquidType\\\"},\\n      {\\\"label\\\": \\\"波纹颜色\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"波纹个数\\\", \\\"value\\\": \\\"option.count\\\"},\\n      {\\\"label\\\": \\\"波纹长度\\\", \\\"value\\\": \\\"option.length\\\"},\\n      {\\\"label\\\": \\\"外框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"},\\n      {\\\"label\\\": \\\"外框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"},\\n      {\\\"label\\\": \\\"间距\\\", \\\"value\\\": \\\"option.distance\\\"},\\n      {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.strokeOpacity\\\"},\\n      {\\\"label\\\": \\\"文本颜色配置\\\", \\\"value\\\": \\\"option.textColor\\\"},\\n      {\\\"label\\\": \\\"文本字体大小配置\\\", \\\"value\\\": \\\"option.textFontSize\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'象形图设置\',\\n    optionName: \'PictorialOption\',\\n    children: [\\n      {\\\"label\\\": \\\"象形图柱体颜色设置\\\", \\\"value\\\": \\\"option.barColor\\\"},\\n      {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.barOpacity\\\"},\\n      {\\\"label\\\": \\\"间距设置\\\", \\\"value\\\": \\\"option.count\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'仪表盘设置\',\\n    optionName: \'GaugeOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.series[0].axisLabel.show\\\"},\\n      {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.series[0].axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.series[0].axisLabel.fontSize\\\"},\\n      {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.series[0].axisTick.show\\\"},\\n      {\\\"label\\\": \\\"刻度线长度\\\", \\\"value\\\": \\\"option.series[0].axisTick.length\\\"},\\n      {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.series[0].axisTick.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"显示分割线\\\", \\\"value\\\": \\\"option.series[0].splitLine.show\\\"},\\n      {\\\"label\\\": \\\"分割线长度\\\", \\\"value\\\": \\\"option.series[0].splitLine.length\\\"},\\n      {\\\"label\\\": \\\"分割线颜色\\\", \\\"value\\\": \\\"option.series[0].splitLine.lineStyle.color\\\"},\\n      {\\\"label\\\": \\\"指标字号\\\", \\\"value\\\": \\\"option.series[0].detail.fontSize\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'渐变仪表盘设置\',\\n    optionName: \'AntvGaugeOption\',\\n    children: [\\n      {\\\"label\\\": \\\"仪表盘粗细设置\\\", \\\"value\\\": \\\"option.gaugeWidth\\\"},\\n      {\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.axisLabelShow\\\"},\\n      {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.axisLabelColor\\\"},\\n      {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.axisLabelFontSize\\\"},\\n      {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.axisTickShow\\\"},\\n      {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.lineColor\\\"},\\n      {\\\"label\\\": \\\"文本颜色\\\", \\\"value\\\": \\\"option.valueColor\\\"},\\n      {\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"},\\n      {\\\"label\\\": \\\"指针颜色\\\", \\\"value\\\": \\\"option.indicatorColor\\\"},\\n      {\\\"label\\\": \\\"指针粗细\\\", \\\"value\\\": \\\"option.indicatorLength\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'尺寸设置\',\\n    optionName: \'Pyramid3DOption\',\\n    children: [\\n      {\\\"label\\\": \\\"缩放\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"尺寸\\\", \\\"value\\\": \\\"option.size\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'环形设置\',\\n    optionName: \'RingOption\',\\n    children: [\\n      {\\\"label\\\": \\\"内半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"外半径\\\", \\\"value\\\": \\\"option.outRadius\\\"}\\n    ]\\n  },\\n {\\n    name: \'样式设置\',\\n    optionName: \'PercentBarStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"Y轴刻度颜色设置\\\", \\\"value\\\": \\\"option.yNameFontColor\\\"},\\n      {\\\"label\\\": \\\"Y轴刻度字体大小设置\\\", \\\"value\\\": \\\"option.yNameFontSize\\\"},\\n      {\\\"label\\\": \\\"X轴刻度颜色设置\\\", \\\"value\\\": \\\"option.xNameFontColor\\\"},\\n      {\\\"label\\\": \\\"X轴刻度字体大小设置\\\", \\\"value\\\": \\\"option.xNameFontSize\\\"},\\n      {\\\"label\\\": \\\"图例位置设置\\\", \\\"value\\\": \\\"option.legendPosition\\\", \\\"options\\\": [{\\\"label\\\": \\\"居上\\\", \\\"value\\\": \\\"top\\\"}, {\\\"label\\\": \\\"居下\\\", \\\"value\\\": \\\"bottom\\\"}]},\\n      {\\\"label\\\": \\\"图例字体颜色设置\\\", \\\"value\\\": \\\"option.legendFontColor\\\"},\\n      {\\\"label\\\": \\\"图例字体大小设置\\\", \\\"value\\\": \\\"option.legendFontSize\\\"},\\n    ]\\n  },\\n  {\\n    name: \'胶囊图设置\',\\n    optionName: \'CapsuleChartOption\',\\n    children: [\\n      {\\\"label\\\": \\\"胶囊图设置显示数值\\\", \\\"value\\\": \\\"option.showValue\\\"},\\n      {\\\"label\\\": \\\"胶囊图设置X轴名称设置成\\\", \\\"value\\\": \\\"option.unit\\\"}\\n    ]\\n  },\\n  {\\n    name: \'环形图设置\',\\n    optionName: \'ActiveRingPlotOption\',\\n    children: [\\n      {\\\"label\\\": \\\"环形图颜色设置\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"环形图背景色设置\\\", \\\"value\\\": \\\"option.bgColor\\\"},\\n      {\\\"label\\\": \\\"环形图外环半径\\\", \\\"value\\\": \\\"option.outRadius\\\"},\\n      {\\\"label\\\": \\\"环形图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"环形图标题字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"环形图标题字体颜色\\\", \\\"value\\\": \\\"option.fontColor\\\"},\\n      {\\\"label\\\": \\\"环形图标题字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"},\\n      {\\\"label\\\": \\\"环形图数值字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"},\\n      {\\\"label\\\": \\\"环形图数值字体颜色\\\", \\\"value\\\": \\\"option.valueFontColor\\\"},\\n      {\\\"label\\\": \\\"环形图数值字体粗细\\\", \\\"value\\\": \\\"option.valueFontWeight\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'动态环形图设置\',\\n    optionName: \'ActiveRingOption\',\\n    children: [\\n      {\\\"label\\\": \\\"动态环形图显示原始值\\\", \\\"value\\\": \\\"option.showOriginValue\\\"},\\n      {\\\"label\\\": \\\"动态环形图文字颜色\\\", \\\"value\\\": \\\"option.textColor\\\"},\\n      {\\\"label\\\": \\\"动态环形图文字大小\\\", \\\"value\\\": \\\"option.textFontSize\\\"},\\n      {\\\"label\\\": \\\"动态环形图线条宽度\\\", \\\"value\\\": \\\"option.lineWidth\\\"},\\n      {\\\"label\\\": \\\"动态环形图环半径\\\", \\\"value\\\": \\\"option.radius\\\"},\\n      {\\\"label\\\": \\\"动态环形图动态环半径\\\", \\\"value\\\": \\\"option.activeRadius\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'玉珏设置\',\\n    optionName: \'RadialBarOption\',\\n    children: [\\n      {\\\"label\\\": \\\"玉珏图显示圆角\\\", \\\"value\\\": \\\"option.radiuShow\\\"},\\n      {\\\"label\\\": \\\"玉珏图背景显示\\\", \\\"value\\\": \\\"option.bgShow\\\"},\\n      {\\\"label\\\": \\\"玉珏图外环半径\\\", \\\"value\\\": \\\"option.radius\\\"},\\n      {\\\"label\\\": \\\"玉珏图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"},\\n      {\\\"label\\\": \\\"玉珏图最大旋转角\\\", \\\"value\\\": \\\"option.maxAngle\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'矩形图设置\',\\n    optionName: \'RectangleOption\',\\n    children: [\\n      {\\\"label\\\": \\\"矩形图文本颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"矩形图文本字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n      {\\\"label\\\": \\\"矩形图显示图例\\\", \\\"value\\\": \\\"option.showLegend\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'文本设置\',\\n    optionName: \'TextOption\',\\n    children: [\\n      {\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.body.fontSize\\\"},\\n      {\\\"label\\\": \\\"文本字体间距\\\", \\\"value\\\": \\\"option.body.letterSpacing\\\"},\\n      {\\\"label\\\": \\\"文本字体颜色\\\", \\\"value\\\": \\\"option.body.color\\\"},\\n      {\\\"label\\\": \\\"文本启用千分符\\\", \\\"value\\\": \\\"option.body.thousandSeparator\\\"},\\n      {\\\"label\\\": \\\"文本水平间距\\\", \\\"value\\\": \\\"option.body.marginLeft\\\"},\\n      {\\\"label\\\": \\\"文本垂直间距\\\", \\\"value\\\": \\\"option.body.marginTop\\\"},\\n      {\\\"label\\\": \\\"文本开启跑马灯\\\", \\\"value\\\": \\\"option.horseLamp\\\"},\\n      {\\\"label\\\": \\\"文本开启超链接\\\", \\\"value\\\": \\\"option.isLink\\\"},\\n      {\\\"label\\\": \\\"文本超链接地址\\\", \\\"value\\\": \\\"option.openUrl\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'内部设置\',\\n    optionName: \'CountToTextOption\',\\n    children: [\\n      {\\\"label\\\": \\\"字体粗细设置\\\", \\\"value\\\": \\\"option.fontWeight\\\"},\\n      {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.fontColor\\\"},\\n      {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"前缀文本内容设置\\\", \\\"value\\\": \\\"option.prefix\\\"},\\n      {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.prefixFontSize\\\"},\\n      {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixColor\\\"},\\n      {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"},\\n      {\\\"label\\\": \\\"前缀字体对齐方式\\\", \\\"value\\\": \\\"option.prefixTextAlign\\\"},\\n      {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixGridX\\\"},\\n      {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀文本内容设置\\\", \\\"value\\\": \\\"option.suffix\\\"},\\n      {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"},\\n      {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixColor\\\"},\\n      {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"},\\n      {\\\"label\\\": \\\"后缀字体对齐方式\\\", \\\"value\\\": \\\"option.suffixTextAlign\\\"},\\n      {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixGridX\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n      {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'颜色块设置\',\\n    optionName: \'ColorBlockOption\',\\n    children: [\\n      {\\\"label\\\": \\\"颜色块行数设置\\\", \\\"value\\\": \\\"option.lineNum\\\"},\\n      {\\\"label\\\": \\\"颜色块边距设置\\\", \\\"value\\\": \\\"option.padding\\\"},\\n      {\\\"label\\\": \\\"颜色块X间距设置\\\", \\\"value\\\": \\\"option.borderSplitx\\\"},\\n      {\\\"label\\\": \\\"颜色块Y间距设置\\\", \\\"value\\\": \\\"option.borderSplity\\\"},\\n      {\\\"label\\\": \\\"小数位数设置\\\", \\\"value\\\": \\\"option.decimals\\\"},\\n      {\\\"label\\\": \\\"字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"字体颜色\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"},\\n      {\\\"label\\\": \\\"字体对齐方式\\\", \\\"value\\\": \\\"option.textAlign\\\"},\\n      {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.borderSplity\\\"},\\n      {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixColor\\\"},\\n      {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"},\\n      {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixSplitx\\\"},\\n      {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixSplity\\\"},\\n      {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"},\\n      {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixColor\\\"},\\n      {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"},\\n      {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixSplitx\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'字体设置\',\\n    optionName: \'FlashCloudOption\',\\n    children: [\\n      {\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.textSize\\\"},\\n      {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.textColor\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'字符云设置\',\\n    optionName: \'WordCloudOption\',\\n    children: [\\n      {\\\"label\\\": \\\"字体颜色配置\\\", \\\"value\\\": \\\"option.color\\\"},\\n      {\\\"label\\\": \\\"字体间距设置\\\", \\\"value\\\": \\\"option.padding\\\"},\\n      {\\\"label\\\": \\\"字体旋转设置\\\", \\\"value\\\": \\\"option.rotation\\\"},\\n      {\\\"label\\\": \\\"字体最大值设置\\\", \\\"value\\\": \\\"option.minSize\\\"},\\n      {\\\"label\\\": \\\"字体最小值设置\\\", \\\"value\\\": \\\"option.maxSize\\\"},\\n      {\\\"label\\\": \\\"字体形状设置\\\", \\\"value\\\": \\\"option.series[0].shape\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'轮播表格设置\',\\n    optionName: \'ScrollBoardOpt\',\\n    children: [\\n      {\\\"label\\\": \\\"悬浮暂停设置\\\", \\\"value\\\": \\\"option.hoverPause\\\"},\\n      {\\\"label\\\": \\\"等待时间设置\\\", \\\"value\\\": \\\"option.waitTime\\\"},\\n      {\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.index\\\"},\\n      {\\\"label\\\": \\\"表格列宽\\\", \\\"value\\\": \\\"option.indexWidth\\\"},\\n      {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.headShow\\\"},\\n      {\\\"label\\\": \\\"表头颜色\\\", \\\"value\\\": \\\"option.headerBGC\\\"},\\n      {\\\"label\\\": \\\"表头行高\\\", \\\"value\\\": \\\"option.headerHeight\\\"},\\n      {\\\"label\\\": \\\"每页行数\\\", \\\"value\\\": \\\"option.rowNum\\\"},\\n      {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddRowBGC\\\"},\\n      {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenRowBGC\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'表格设置\',\\n    optionName: \'ScrollTableStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.ranking\\\"},\\n      {\\\"label\\\": \\\"开启滚动\\\", \\\"value\\\": \\\"option.scroll\\\"},\\n      {\\\"label\\\": \\\"滚动时间\\\", \\\"value\\\": \\\"option.scrollTime\\\"},\\n      {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.showHead\\\"},\\n      {\\\"label\\\": \\\"表头背景颜色\\\", \\\"value\\\": \\\"option.headerBgColor\\\"},\\n      {\\\"label\\\": \\\"表头字体颜色\\\", \\\"value\\\": \\\"option.headerFontColor\\\"},\\n      {\\\"label\\\": \\\"表头字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"},\\n      {\\\"label\\\": \\\"行高设置\\\", \\\"value\\\": \\\"option.lineHeight\\\"},\\n      {\\\"label\\\": \\\"边框显示\\\", \\\"value\\\": \\\"option.showBorder\\\"},\\n      {\\\"label\\\": \\\"边框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"},\\n      {\\\"label\\\": \\\"边框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"},\\n      {\\\"label\\\": \\\"边框线类型\\\", \\\"value\\\": \\\"option.borderStyle\\\"},\\n      {\\\"label\\\": \\\"表格字体颜色\\\", \\\"value\\\": \\\"option.bodyFontColor\\\"},\\n      {\\\"label\\\": \\\"表格字体大小\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"},\\n      {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddColor\\\"},\\n      {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'历程设置\',\\n    optionName: \'DevHistoryOption\',\\n    children: [\\n      {\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"轮播间隔\\\", \\\"value\\\": \\\"option.waitTime\\\"},\\n      {\\\"label\\\": \\\"历程背景色\\\", \\\"value\\\": \\\"option.typeBackColor\\\"},\\n      {\\\"label\\\": \\\"历程字体颜色\\\", \\\"value\\\": \\\"option.typeFontColor\\\"},\\n      {\\\"label\\\": \\\"内容字体颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"内容字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'数据表格设置\',\\n    optionName: \'TableStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"表头背景颜色设置\\\", \\\"value\\\": \\\"option.headerBgColor\\\"},\\n      {\\\"label\\\": \\\"表头字体大小设置\\\", \\\"value\\\": \\\"option.headerFontSize\\\"},\\n      {\\\"label\\\": \\\"表头字体颜色设置\\\", \\\"value\\\": \\\"option.headerColor\\\"},\\n      {\\\"label\\\": \\\"表体内容字体颜色设置\\\", \\\"value\\\": \\\"option.bodyColor\\\"},\\n      {\\\"label\\\": \\\"表体内容字体大小设置\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"},\\n      {\\\"label\\\": \\\"表体内容背景颜色设置\\\", \\\"value\\\": \\\"option.bodyBgColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'列表设置\',\\n    optionName: \'ListStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"显示标题前缀\\\", \\\"value\\\": \\\"option.showTitlePrefix\\\"},\\n      {\\\"label\\\": \\\"显示时间前缀\\\", \\\"value\\\": \\\"option.showTimePrefix\\\"},\\n      {\\\"label\\\": \\\"列表布局设置\\\", \\\"value\\\": \\\"option.layout\\\"},\\n      {\\\"label\\\": \\\"标题字体颜色设置\\\", \\\"value\\\": \\\"option.titleFontColor\\\"},\\n      {\\\"label\\\": \\\"标题字体粗细设置\\\", \\\"value\\\": \\\"option.titleFontWeight\\\"},\\n      {\\\"label\\\": \\\"标题字体大小设置\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},\\n      {\\\"label\\\": \\\"内容图标颜色设置\\\", \\\"value\\\": \\\"option.iconColor\\\"},\\n      {\\\"label\\\": \\\"内容颜色设置\\\", \\\"value\\\": \\\"option.contentColor\\\"},\\n      {\\\"label\\\": \\\"开启动画设置\\\", \\\"value\\\": \\\"option.isEnableAnimation\\\"},\\n      {\\\"label\\\": \\\"轮播时间(毫秒)设置\\\", \\\"value\\\": \\\"option.scrollTime\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'滚动设置\',\\n    optionName: \'ScrollOption\',\\n    children: [\\n      {\\\"label\\\": \\\"是否排序\\\", \\\"value\\\": \\\"option.sort\\\"},\\n      {\\\"label\\\": \\\"轮播方式设置单行\\\", \\\"value\\\": \\\"option.carousel\\\",\\\"options\\\": [{\\\"label\\\": \\\"单行\\\", \\\"value\\\": \\\"single\\\"}, {\\\"label\\\": \\\"整页\\\", \\\"value\\\": \\\"page\\\"},]},\\n      {\\\"label\\\": \\\"显示行数\\\", \\\"value\\\": \\\"option.rowNum\\\"},\\n      {\\\"label\\\": \\\"滚动时间(毫秒)设置\\\", \\\"value\\\": \\\"option.waitTime\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'气泡排名设置\',\\n    optionName: \'BubbleRankingStyle\',\\n    children: [\\n      {\\\"label\\\": \\\"比例设置\\\", \\\"value\\\": \\\"option.zoom\\\"},\\n      {\\\"label\\\": \\\"显示提示词\\\", \\\"value\\\": \\\"option.showTip\\\"},\\n      {\\\"label\\\": \\\"提示词颜色设置为\\\", \\\"value\\\": \\\"option.titleColor\\\"},\\n      {\\\"label\\\": \\\"提示词宽度设置为\\\", \\\"value\\\": \\\"option.tipWidth\\\"},\\n      {\\\"label\\\": \\\"提示词内容颜色设置\\\", \\\"value\\\": \\\"option.tipFontColor\\\"},\\n      {\\\"label\\\": \\\"提示词内容字体大小设置\\\", \\\"value\\\": \\\"option.tipFontSize\\\"}\\n    ]\\n  }\\n  ,{\\n    name: \'地图设置\',\\n    optionName: \'MapOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示区域名称\\\", \\\"value\\\": \\\"option.geo.label.normal.show\\\"},\\n      {\\\"label\\\": \\\"区域名称颜色设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.color\\\"},\\n      {\\\"label\\\": \\\"区域名称字体大小设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.fontSize\\\"},\\n      {\\\"label\\\": \\\"是否开启钻取\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"},\\n      {\\\"label\\\": \\\"导航文字颜色设置\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"},\\n      {\\\"label\\\": \\\"是否开启鼠标缩放\\\", \\\"value\\\": \\\"option.geo.roam\\\"},\\n      {\\\"label\\\": \\\"缩放比例设置\\\", \\\"value\\\": \\\"option.geo.zoom\\\"},\\n      {\\\"label\\\": \\\"地图长宽比设置\\\", \\\"value\\\": \\\"option.geo.aspectScale\\\"},\\n      {\\\"label\\\": \\\"地图顶边距设置\\\", \\\"value\\\": \\\"option.geo.top\\\"},\\n      {\\\"label\\\": \\\"地图左边距设置\\\", \\\"value\\\": \\\"option.geo.left\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'地图配色设置\',\\n    optionName: \'LineMapColorOption\',\\n    children: [\\n      {\\\"label\\\": \\\"启用渐变色\\\", \\\"value\\\": \\\"commonOption.gradientColor\\\"},\\n      {\\\"label\\\": \\\"中心颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"},\\n      {\\\"label\\\": \\\"边缘颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color2\\\"},\\n      {\\\"label\\\": \\\"区域颜色设置\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"},\\n      {\\\"label\\\": \\\"区域高亮颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.emphasis.areaColor\\\"},\\n      {\\\"label\\\": \\\"区域边界颜色\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.borderColor\\\"},\\n      {\\\"label\\\": \\\"阴影大小设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowBlur\\\"},\\n      {\\\"label\\\": \\\"阴影水平偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetX\\\"},\\n      {\\\"label\\\": \\\"阴影垂直偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetY\\\"},\\n      {\\\"label\\\": \\\"阴影颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowColor\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'视觉映射设置\',\\n    optionName: \'VisualMapOptoin\',\\n    children: [\\n      {\\\"label\\\": \\\"开启视觉映射\\\", \\\"value\\\": \\\"option.visualMap.show\\\"},\\n      {\\\"label\\\": \\\"视觉映射类型\\\", \\\"value\\\": \\\"option.visualMap.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"continuous\\\", \\\"value\\\": \\\"continuous\\\"}, {\\\"label\\\": \\\"piecewise\\\", \\\"value\\\": \\\"piecewise\\\"}]},\\n      {\\\"label\\\": \\\"视觉映射文本颜色\\\", \\\"value\\\": \\\"option.visualMap.textStyle.color\\\"},\\n      {\\\"label\\\": \\\"视觉映射文本粗细\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontWeight\\\"},\\n      {\\\"label\\\": \\\"视觉映射文本字体大小设置\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontSize\\\"},\\n      {\\\"label\\\": \\\"区域边界最小值\\\", \\\"value\\\": \\\"option.visualMap.min\\\"},\\n      {\\\"label\\\": \\\"区域边界最大值\\\", \\\"value\\\": \\\"option.visualMap.max\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'地图散点设置\',\\n    optionName: \'ScatterOption\',\\n    children: [\\n      {\\\"label\\\": \\\"地图散点大小设置\\\", \\\"value\\\": \\\"option.area.markerSize\\\"},\\n      {\\\"label\\\": \\\"地图散点形状设置\\\", \\\"value\\\": \\\"option.area.markerShape\\\"},\\n      {\\\"label\\\": \\\"地图散点类型设置\\\", \\\"value\\\": \\\"option.area.markerType\\\"},\\n      {\\\"label\\\": \\\"地图散点颜色设置\\\", \\\"value\\\": \\\"option.area.markerColor\\\"},\\n      {\\\"label\\\": \\\"地图散点文本显示\\\", \\\"value\\\": \\\"option.area.scatterLabelShow\\\"},\\n      {\\\"label\\\": \\\"地图散点文本颜色设置\\\", \\\"value\\\": \\\"option.area.scatterLabelColor\\\"},\\n      {\\\"label\\\": \\\"地图散点文本显示位置设置\\\", \\\"value\\\": \\\"option.area.scatterLabelPosition\\\"},\\n      {\\\"label\\\": \\\"地图散点文本字体大小设置\\\", \\\"value\\\": \\\"option.area.scatterFontSize\\\"},\\n      {\\\"label\\\": \\\"地图散点数量设置\\\", \\\"value\\\": \\\"option.area.markerCount\\\"},\\n      {\\\"label\\\": \\\"地图散点透明度设置\\\", \\\"value\\\": \\\"option.area.markerOpacity\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'热力地图设置\',\\n    optionName: \'HeatOption\',\\n    children: [\\n      {\\\"label\\\": \\\"热力点大小设置\\\", \\\"value\\\": \\\"commonOption.heat.pointSize\\\"},\\n      {\\\"label\\\": \\\"模糊大小设置\\\", \\\"value\\\": \\\"commonOption.heat.blurSize\\\"},\\n      {\\\"label\\\": \\\"最大透明度设置\\\", \\\"value\\\": \\\"commonOption.heat.maxOpacity\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'柱体地图设置\',\\n    optionName: \'BarMapOption\',\\n    children: [\\n      {\\\"label\\\": \\\"柱体地图柱体大小设置\\\", \\\"value\\\": \\\"commonOption.barSize\\\"},\\n      {\\\"label\\\": \\\"柱体左侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor\\\"},\\n      {\\\"label\\\": \\\"柱体右侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor2\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'飞线地图设置\',\\n    optionName: \'FlyLineOption\',\\n    children: [\\n      {\\\"label\\\": \\\"飞线动画时间设置\\\", \\\"value\\\": \\\"commonOption.effect.period\\\"},\\n      {\\\"label\\\": \\\"飞线标记形状设置\\\", \\\"value\\\": \\\"commonOption.effect.markerShape\\\"},\\n      {\\\"label\\\": \\\"飞线标记大小设置\\\", \\\"value\\\": \\\"commonOption.effect.symbolSize\\\"},\\n      {\\\"label\\\": \\\"飞线标记颜色设置\\\", \\\"value\\\": \\\"commonOption.effect.markerColor\\\"},\\n      {\\\"label\\\": \\\"飞线特效尾迹长度设置\\\", \\\"value\\\": \\\"commonOption.effect.trailLength\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'进度设置\',\\n    optionName: \'ProgressOption\',\\n    children: [\\n      {\\\"label\\\": \\\"显示标题\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.show\\\"},\\n      {\\\"label\\\": \\\"标题字体颜色设置\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"},\\n      {\\\"label\\\": \\\"标题字体大小设置\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.fontSize\\\"},\\n      {\\\"label\\\": \\\"数值字体颜色设置\\\", \\\"value\\\": \\\"option.series[1].label.color\\\"},\\n      {\\\"label\\\": \\\"数值字体大小设置\\\", \\\"value\\\": \\\"option.series[1].label.fontSize\\\"},\\n      {\\\"label\\\": \\\"横向偏移设置\\\", \\\"value\\\": \\\"option.valueXOffset\\\"},\\n      {\\\"label\\\": \\\"纵向偏移设置\\\", \\\"value\\\": \\\"option.valueYOffset\\\"},\\n      {\\\"label\\\": \\\"柱体宽度设置\\\", \\\"value\\\": \\\"option.series[0].barWidth\\\"},\\n      {\\\"label\\\": \\\"进度颜色设置\\\", \\\"value\\\": \\\"option.series[0].color\\\"},\\n      {\\\"label\\\": \\\"目标颜色设置\\\", \\\"value\\\": \\\"option.series[1].color\\\"},\\n    ]\\n  }\\n  ,{\\n    name: \'南丁格尔玫瑰设置\',\\n    optionName: \'RoseOption\',\\n    children: [\\n      {\\\"label\\\": \\\"边框宽度\\\", \\\"value\\\": \\\"option.series[0].itemStyle.borderWidth\\\"},\\n      {\\\"label\\\": \\\"颜色透明度\\\", \\\"value\\\": \\\"option.series[0].itemStyle.colorOpacity\\\"},\\n    ]\\n  }\\n];\\n\\n\"},{\"role\":\"user\",\"content\":\"用户的问题:{{userQuestion}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userQuestion\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"269049045129183232\",\"type\":\"end\",\"x\":1272,\"y\":459,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{option}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"269048862303666176\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"269048862299471872\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"269048862299471872_input\",\"pointsList\":[{\"x\":466,\"y\":422},{\"x\":566,\"y\":422},{\"x\":522,\"y\":414},{\"x\":622,\"y\":414}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"269049045129183233\",\"type\":\"base-edge\",\"sourceNodeId\":\"269048862299471872\",\"targetNodeId\":\"269049045129183232\",\"sourceAnchorId\":\"269048862299471872_output\",\"targetAnchorId\":\"269049045129183232_input\",\"pointsList\":[{\"x\":954,\"y\":414},{\"x\":1054,\"y\":414},{\"x\":1006,\"y\":422},{\"x\":1106,\"y\":422}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '2005948202528501762'; + +-- AI 生成图表、修改配置项-升级SQL +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'269048862299471872\'),\n end.tag(\'269049045129183232\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":437,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"269048862299471872\",\"type\":\"llm\",\"x\":788,\"y\":473,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:ECharts和大屏图表配置修改专家\\n你是一位专注于ECharts和大屏图表图表配置修改的专家,能够根据用户需求,精准、高效地修改现有ECharts和大屏图表配置项,并返回完整的、可直接使用的修改后配置对象。\\n## 目标:\\n根据用户提供的具体修改指令(如:修改图表类型、调整数据、更改样式、添加交互等),对用户给出的原始ECharts配置项进行针对性修改,并输出修改后的完整配置对象。\\n## 技能:\\n1. 精通ECharts所有版本的配置项语法、结构及参数含义。\\n2. 能够准确理解用户对图表样式、数据、交互行为的修改意图。\\n3. 具备强大的代码编辑与重构能力,确保修改后的配置项语法正确、结构清晰、无冗余代码。\\n4. 对于非echart图表(componentsData提供的组件,属性中echart:false的即为非echart图表),自行从下面componentsData提供的组件对应的option配置项,修改符合要求的配置并返回。\\n## 工作流:\\n1. **接收与分析**:接收用户提供的原始ECharts配置对象(通常以JSON或JavaScript对象形式)以及具体的修改要求。仔细分析原始配置的结构和用户的修改点。\\n2. **精准修改**:严格依据用户指令,对原始配置对象进行最小化、精准化的修改。确保只改动指定部分,保持其他未提及配置的完整性。对于模糊指令,会基于ECharts最佳实践进行合理推断和实现。\\n3. **校验与格式化**:检查修改后的配置对象语法是否正确,是否符合ECharts规范。将最终配置对象以格式清晰、缩进规范的JSON或JavaScript对象形式呈现。\\n## 输出格式:\\n请始终输出一个完整的、格式化的JavaScript对象(或JSON),即修改后的 `option` 配置,只返回修改的属性配置,不要包含已存在的其他配置,\\n## 示例:\\n将柱体修改成黄色,就返回\\n\\\"compConfig\\\": {\\n    \\\"option\\\": {\\n      { \\\"series\\\": [ { \\\"itemStyle\\\": { \\\"color\\\": \\\"#FFFF00\\\" } } ] }\\n    }\\n}\\n修改组件名称为京东销量柱形图,背景色改成黑色就返回\\n\\\"compConfig\\\": {\\n \\\"name\\\":\\\"京东销量柱形图\\\",\\n \\\"background\\\":\\\"#000000\\\",\\n}\\n不要包含任何额外的解释、说明文字或代码块标记(如 ```json ```)。输出应直接以 `{` 开始,以 `}` 结束。\\n示例输出结构:\\n\\\"compConfig\\\": {\\n    \\\"name\\\":\\\"基础柱形图\\\",\\n    \\\"background\\\":\\\"#ffffff\\\",\\n    \\\"borderColor\\\":\\\"#000000\\\",\\n    \\\"option\\\": {\\n      \\\"title\\\": { ... },\\n      \\\"tooltip\\\": { ... },\\n      \\\"xAxis\\\": { ... },\\n      \\\"yAxis\\\": { ... },\\n      \\\"series\\\": [ ... ]\\n    }\\n}\\n## 限制:\\n- 仅对用户提供的原始配置进行修改,不凭空创建全新的图表配置。\\n- 输出必须仅为修改后的配置对象本身,不附带任何分析过程、修改日志或使用建议。\\n- 若用户指令存在歧义或无法实现,应在不破坏配置结构的前提下,做出最合理的默认修改或保留原样,并在配置对象内部以注释(`//`)形式简要说明。\\n- 严格遵守ECharts官方配置规范,不使用已废弃或实验性参数(除非用户明确要求)。\\n- 颜色类型的修改,要以具体色值设置,不要使用英文单词,例如黑色,使用#000000,不要使用black\\n- 修改的option属性,以componentsData中具体组件的option配置为主,结合echart选择符合要求的配置项修改\\n- [\'JRadioButton\', \'JRadialBar\', \'JActiveRing\', \'JRing\', \'JPyramidFunnel\', \'JFunnel\', \'JBubble\', \'DoubleLineBar\', \'JMultipleLine\', \'JArea\', \'JLine\', \'JRotatePie\', \'JRose\', \'JPie\', \'JMixLineBar\', \'JPercentBar\', \'JMultipleBar\', \'JCapsuleChart\', \'JStackBar\', \'JQuadrant\'] 这些组件的相关颜色属性修改,按照 \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}] 的格式修改; - 组件不包含customColor属性的颜色属性修改,按照对应组件配置的属性value数值去修改\\n- 柱体颜色属性修改使用 option.series[${index}].itemStyle.color,[\'JDynamicBar\']这些组件的相关颜色属性修改,按照option.series[${index}].itemStyle.color方式修改\\n- 配置项粗细的修改参数包含 [{ label: \'默认\', value: \'normal\' } { label: \'粗体\', value: \'bold\' } { label: \'细体\', value: \'lighter\' }]\\n- YAxisOption的`option.yAxis.yUnit`单位设置的不是option里面的label的内容时(例如:元),就将`option.yAxis.yUnit`值设置成\'CUSTOM\',并同步将`option.yAxis.yCustomUnit`属性的值设置成对应的单位数据(例如:元)\\n- 若用户修改名称或者背景色或者边框的属性,以componentsData中第一个柱形图配置为例,去修改返回对应配置即可\\n -名称:对应 compConfig.name\\n -背景色:对应 compConfig.background\\n -边框色:对应 compConfig.borderColor\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"name\\\":\\\"基础柱形图\\\",\\n      \\\"background\\\":\\\"#ffffff\\\",\\n      \\\"borderColor\\\":\\\"#000000\\\"\\n    }\\n  }]\\n组件配置说明\\n compOptionData = [{name: \'基础配置\', optionName: \'BasicOption\', children: [{\\\"label\\\": \\\"图层名称修改成\\\", \\\"value\\\": \\\"name\\\"}, {\\\"label\\\": \\\"图层背景色设置成\\\", \\\"value\\\": \\\"background\\\"}, {\\\"label\\\": \\\"图层边框线设置成\\\", \\\"value\\\": \\\"borderColor\\\"}, {\\\"label\\\": \\\"提示语设置为隐藏\\\", \\\"value\\\": \\\"option.tooltip.show\\\"}, {\\\"label\\\": \\\"提示语字体大小设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"提示语字体颜色设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"},]},{name: \'标题设置\', optionName: \'TitleOption\', children: [{\\\"label\\\": \\\"标题名称修改成\\\", \\\"value\\\": \\\"option.title.text\\\"}, {\\\"label\\\": \\\"标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontColor\\\"}, {\\\"label\\\": \\\"标题字体粗细设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontWeight\\\"}, {\\\"label\\\": \\\"副标题名称修改成\\\", \\\"value\\\": \\\"option.title.subtextStyle\\\"}, {\\\"label\\\": \\\"副标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontSize\\\"}, {\\\"label\\\": \\\"副标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontColor\\\"}, {\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"option.title.left\\\"}, {\\\"label\\\": \\\"垂直居中\\\", \\\"value\\\": \\\"option.title.top\\\"},]},{name: \'X轴设置\', optionName: \'XAxisOption\', children: [{\\\"label\\\": \\\"X轴名称修改成\\\", \\\"value\\\": \\\"option.xAxis.name\\\"}, {\\\"label\\\": \\\"X轴名称颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.color\\\"}, {\\\"label\\\": \\\"X轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.fontSize\\\"}, {\\\"label\\\": \\\"X轴标签颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.color\\\"}, {\\\"label\\\": \\\"X轴标签角度\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.rotate\\\"}, {\\\"label\\\": \\\"X轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"X轴轴类型修改成\\\", \\\"value\\\": \\\"option.xAxis.type\\\"}, {\\\"label\\\": \\\"X轴显示网格线\\\", \\\"value\\\": \\\"option.xAxis.splitLine.show\\\"}, {\\\"label\\\": \\\"X轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.splitLine.lineStyle.color\\\"},]},{name: \'胶囊图设置\', optionName: \'CapsuleChartOption\', children: [{\\\"label\\\": \\\"胶囊图显示数值\\\", \\\"value\\\": \\\"option.showValue\\\"}, {\\\"label\\\": \\\"胶囊图X轴名称设置成\\\", \\\"value\\\": \\\"option.unit\\\"}]},{name: \'Y轴设置\', optionName: \'YAxisOption\', children: [{\\\"label\\\": \\\"Y轴名称修改成\\\", \\\"value\\\": \\\"option.yAxis.name\\\"}, {\\\"label\\\": \\\"Y轴名称颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.color\\\"}, {\\\"label\\\": \\\"Y轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.fontSize\\\"}, {\\\"label\\\": \\\"Y轴标签颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"}, {\\\"label\\\": \\\"Y轴标签角度\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.rotate\\\"}, {\\\"label\\\": \\\"Y轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"Y轴轴类型修改成\\\", \\\"value\\\": \\\"option.yAxis.type\\\"}, {\\\"label\\\": \\\"Y轴显示网格线\\\", \\\"value\\\": \\\"option.yAxis.splitLine.show\\\"}, {\\\"label\\\": \\\"Y轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.splitLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"Y轴单位设置成\\\", \\\"value\\\": \\\"option.yAxis.yUnit\\\"},]},{name: \'图例设置\', optionName: \'LegendOption\', children: [{\\\"label\\\": \\\"图例字体大小设置成\\\", \\\"value\\\": \\\"option.legend.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"图例设置成横排\\\", \\\"value\\\": \\\"option.legend.orient\\\"}, {\\\"label\\\": \\\"图例上下边距设置\\\", \\\"value\\\": \\\"option.legend.t\\\"}, {\\\"label\\\": \\\"图例左右边距设置\\\", \\\"value\\\": \\\"option.legend.r\\\"},]},{name: \'样式设置\', optionName: \'PercentBarStyle\', children: [{\\\"label\\\": \\\"Y轴刻度颜色设置\\\", \\\"value\\\": \\\"option.yNameFontColor\\\"}, {\\\"label\\\": \\\"Y轴刻度字体大小设置\\\", \\\"value\\\": \\\"option.yNameFontSize\\\"}, {\\\"label\\\": \\\"X轴刻度颜色设置\\\", \\\"value\\\": \\\"option.xNameFontColor\\\"}, {\\\"label\\\": \\\"X轴刻度字体大小设置\\\", \\\"value\\\": \\\"option.xNameFontSize\\\"}, {\\\"label\\\": \\\"图例位置设置\\\", \\\"value\\\": \\\"option.legendPosition\\\", \\\"options\\\": [{\\\"label\\\": \\\"居上\\\", \\\"value\\\": \\\"top\\\"}, {\\\"label\\\": \\\"居下\\\", \\\"value\\\": \\\"bottom\\\"}]}, {\\\"label\\\": \\\"图例字体颜色设置\\\", \\\"value\\\": \\\"option.legendFontColor\\\"}, {\\\"label\\\": \\\"图例字体大小设置\\\", \\\"value\\\": \\\"option.legendFontSize\\\"},]},{name: \'自定义配色\', optionName: \'CustomColorOption\', children: [{\\\"label\\\": \\\"颜色设置成***色\\\", \\\"value\\\": \\\"option.customColor\\\"},]},{name: \'柱体设置\', optionName: \'BarCylinder\', children: [{\\\"label\\\": \\\"柱体宽度修改为\\\", \\\"value\\\": \\\"option.series[${index}].barWidth\\\"}, {\\\"label\\\": \\\"柱体圆角修改为\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.borderRadius\\\"}, {\\\"label\\\": \\\"柱体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.color\\\"}, {\\\"label\\\": \\\"柱体背景色显隐\\\", \\\"value\\\": \\\"option.series[${index}].showBackground\\\"}, {\\\"label\\\": \\\"柱体背景色颜色\\\", \\\"value\\\": \\\"option.series[${index}].backgroundStyle.color\\\"},]},{name: \'折线设置\', optionName: \'PolyglineOption\', children: [{\\\"label\\\": \\\"折线类型修改\\\", \\\"value\\\": \\\"option.series[${index}].lineType\\\",ignoreComp:[\'JArea\'],options: [{ label: \'折线\', value: \'line\' }, { label: \'曲线\', value: \'smooth\' }, { label: \'面积\', value: \'area\' }]}, {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.series[0].areaStyleOpacity\\\"}, {\\\"label\\\": \\\"线条宽度修改\\\", \\\"value\\\": \\\"option.series[${index}].lineWidth\\\"}, {\\\"label\\\": \\\"标记点修改\\\", \\\"value\\\": \\\"option.series[${index}].symbol\\\"}, {\\\"label\\\": \\\"点的大小修改\\\", \\\"value\\\": \\\"option.series[${index}].symbolSize\\\"},]},{name: \'饼图设置\', optionName: \'pieSettingOption\', children: [{\\\"label\\\": \\\"饼图设置成环形\\\", \\\"value\\\": \\\"option.isRadius\\\"}, {\\\"label\\\": \\\"饼图内环半径设置成\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"饼图外环半径设置成\\\", \\\"value\\\": \\\"option.outRadius\\\"}, {\\\"label\\\": \\\"饼图设置成南丁格尔玫瑰\\\", \\\"value\\\": \\\"option.isRose\\\"}, {\\\"label\\\": \\\"饼图标签显示位置\\\", \\\"value\\\": \\\"option.pieLabelPosition\\\"},]},{name: \'中心坐标\', optionName: \'gridPieOption\', children: [{\\\"label\\\": \\\"上下边距修改为\\\", \\\"value\\\": \\\"option.grid.top\\\"}, {\\\"label\\\": \\\"左右边距修改为\\\", \\\"value\\\": \\\"option.grid.left\\\"},]},{name: \'坐标轴边距\', optionName: \'GridOption\', children: [{\\\"label\\\": \\\"左边距修改成\\\", \\\"value\\\": \\\"option.grid.left\\\"}, {\\\"label\\\": \\\"顶边距\\\", \\\"value\\\": \\\"option.grid.top\\\"}, {\\\"label\\\": \\\"右边距\\\", \\\"value\\\": \\\"option.grid.right\\\"}, {\\\"label\\\": \\\"底边距\\\", \\\"value\\\": \\\"option.grid.bottom\\\"},]},{name: \'数值设置\', optionName: \'NumOption\', children: [{\\\"label\\\": \\\"显示数值\\\", \\\"value\\\": \\\"option.series[${index}].label.show\\\"}, {\\\"label\\\": \\\"数值显示位置在\\\", \\\"value\\\": \\\"option.series[${index}].label.position\\\"}, {\\\"label\\\": \\\"数值内容格式修改成\\\", \\\"value\\\": \\\"option.label.format\\\"}, {\\\"label\\\": \\\"数值字体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.color\\\"}, {\\\"label\\\": \\\"数值字体大小修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontSize\\\"}, {\\\"label\\\": \\\"数值字体粗细修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontWeight\\\"}, {\\\"label\\\": \\\"数值单位配置显隐\\\", \\\"value\\\": \\\"option.showUnit.show\\\"}, {\\\"label\\\": \\\"数值单位数量级设置\\\", \\\"value\\\": \\\"option.showUnit.numberLevel\\\",option: simpNumberLevelOption}, {\\\"label\\\": \\\"数值单位保留小数\\\", \\\"value\\\": \\\"option.showUnit.decimal\\\"},]},{name: \'进度设置\', optionName: \'CustomProgressOption\', children: [{\\\"label\\\": \\\"进度目标颜色\\\", \\\"value\\\": \\\"option.backgroundColor\\\"}, {\\\"label\\\": \\\"进度颜色\\\", \\\"value\\\": \\\"option.progressColor\\\"}, {\\\"label\\\": \\\"进度条宽度\\\", \\\"value\\\": \\\"option.barWidth\\\"}, {\\\"label\\\": \\\"进度边距设置\\\", \\\"value\\\": \\\"option.padding\\\"}, {\\\"label\\\": \\\"进度标题颜色设置\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"进度标题字体大小设置\\\", \\\"value\\\": \\\"option.titleFontSize\\\"}, {\\\"label\\\": \\\"进度标题位置设置\\\", \\\"value\\\": \\\"option.titlePosition\\\"}, {\\\"label\\\": \\\"进度数值颜色设置\\\", \\\"value\\\": \\\"option.valueColor\\\"}, {\\\"label\\\": \\\"进度数值字体大小设置\\\", \\\"value\\\": \\\"option.valueFontSize\\\"}, {\\\"label\\\": \\\"进度数值位置设置\\\", \\\"value\\\": \\\"option.valuePosition\\\"}, {\\\"label\\\": \\\"进度数值横向偏移\\\", \\\"value\\\": \\\"option.valueXOffset\\\"},]},{name: \'列表进度图行样式\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"列表进度图行高度\\\", \\\"value\\\": \\\"option.row.height\\\"}, {\\\"label\\\": \\\"列表进度图行左边距\\\", \\\"value\\\": \\\"option.row.marginLeft\\\"}, {\\\"label\\\": \\\"列表进度图行上边距\\\", \\\"value\\\": \\\"option.row.marginTop\\\"}, {\\\"label\\\": \\\"列表进度图行右边距\\\", \\\"value\\\": \\\"option.row.marginRight\\\"},]},{name: \'列表进度图进度条配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"进度条底色设置\\\", \\\"value\\\": \\\"option.bar.background.color\\\"}, {\\\"label\\\": \\\"进度条底色启用渐变\\\", \\\"value\\\": \\\"option.bar.background.gradient.enabled\\\"}, {\\\"label\\\": \\\"进度条底色渐变方向设置\\\", \\\"value\\\": \\\"option.bar.background.gradient.direction\\\"}, {\\\"label\\\": \\\"进度条底色渐变起始颜色设置\\\", \\\"value\\\": \\\"option.bar.background.gradient.startColor\\\"}, {\\\"label\\\": \\\"进度条底色渐变结束颜色设置\\\", \\\"value\\\": \\\"option.bar.background.gradient.endColor\\\"}, {\\\"label\\\": \\\"进度条填充色设置\\\", \\\"value\\\": \\\"option.bar.fill.color\\\"}, {\\\"label\\\": \\\"进度条填充色启用渐变\\\", \\\"value\\\": \\\"option.bar.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"进度条填充色渐变方向设置\\\", \\\"value\\\": \\\"option.bar.fill.gradient.direction\\\"}, {\\\"label\\\": \\\"进度条填充色渐变起始颜色设置\\\", \\\"value\\\": \\\"option.bar.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"进度条填充色渐变结束颜色设置\\\", \\\"value\\\": \\\"option.bar.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"进度条高度设置\\\", \\\"value\\\": \\\"option.bar.height\\\"}, {\\\"label\\\": \\\"进度条圆角设置\\\", \\\"value\\\": \\\"option.bar.borderRadius\\\"}, {\\\"label\\\": \\\"进度指示点大小设置\\\", \\\"value\\\": \\\"option.bar.indicatorSize\\\"}, {\\\"label\\\": \\\"进度指示点颜色设置\\\", \\\"value\\\": \\\"option.bar.indicatorColor\\\"}, {\\\"label\\\": \\\"进度条显示边框\\\", \\\"value\\\": \\\"option.bar.border.enabled\\\"}, {\\\"label\\\": \\\"进度条边框颜色\\\", \\\"value\\\": \\\"option.bar.border.color\\\"}, {\\\"label\\\": \\\"进度条边框大小\\\", \\\"value\\\": \\\"option.bar.border.width\\\"}, {\\\"label\\\": \\\"进度条边框边距\\\", \\\"value\\\": \\\"option.bar.border.padding\\\"}, {\\\"label\\\": \\\"超出阈值配置启用\\\", \\\"value\\\": \\\"option.bar.exceed.enabled\\\"}, {\\\"label\\\": \\\"超出阈值百分比设置\\\", \\\"value\\\": \\\"option.bar.exceed.percent\\\"}, {\\\"label\\\": \\\"超出阈值填充色设置\\\", \\\"value\\\": \\\"option.bar.exceed.fill.color\\\"}, {\\\"label\\\": \\\"超出阈值填充色启用渐变\\\", \\\"value\\\": \\\"option.bar.exceed.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"超出阈值填充色渐变起始颜色\\\", \\\"value\\\": \\\"option.bar.exceed.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"超出阈值填充色渐变结束颜色\\\", \\\"value\\\": \\\"option.bar.exceed.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"超出阈值指示点颜色设置\\\", \\\"value\\\": \\\"option.bar.exceed.indicatorColor\\\"},]},{name: \'列表进度图数据映射\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"进度字段设置\\\", \\\"value\\\": \\\"option.bar.valueField\\\"}, {\\\"label\\\": \\\"总数类型设置\\\", \\\"value\\\": \\\"option.bar.total.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"来自字段\\\", \\\"value\\\": \\\"field\\\"}, {\\\"label\\\": \\\"固定值\\\", \\\"value\\\": \\\"fixed\\\"}]}, {\\\"label\\\": \\\"总数字段设置\\\", \\\"value\\\": \\\"option.bar.total.field\\\"}, {\\\"label\\\": \\\"固定总数设置\\\", \\\"value\\\": \\\"option.bar.total.val\\\"},]},{name: \'列表进度图左侧配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"左侧宽度设置\\\", \\\"value\\\": \\\"option.beginInfo.width\\\"}, {\\\"label\\\": \\\"左侧排列方式设置\\\", \\\"value\\\": \\\"option.beginInfo.layout\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"上下排列\\\", \\\"value\\\": \\\"vertical\\\"}]},]},{name: \'列表进度图中间配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"中间左边距设置\\\", \\\"value\\\": \\\"option.progressSection.marginLeft\\\"}, {\\\"label\\\": \\\"中间右边距设置\\\", \\\"value\\\": \\\"option.progressSection.marginRight\\\"}, {\\\"label\\\": \\\"中间排列方式设置\\\", \\\"value\\\": \\\"option.centerTopInfo.layout\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"上下排列\\\", \\\"value\\\": \\\"vertical\\\"}]},]},{name: \'列表进度图右侧配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"右侧宽度设置\\\", \\\"value\\\": \\\"option.endInfo.width\\\"}, {\\\"label\\\": \\\"右侧排列方式设置\\\", \\\"value\\\": \\\"option.endInfo.layout\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"上下排列\\\", \\\"value\\\": \\\"vertical\\\"}]},]},{name: \'列表进度图滚动动画\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"启用滚动\\\", \\\"value\\\": \\\"option.scroll.enabled\\\"}, {\\\"label\\\": \\\"滚动方向设置\\\", \\\"value\\\": \\\"option.scroll.direction\\\", \\\"options\\\": [{\\\"label\\\": \\\"向上滚动\\\", \\\"value\\\": \\\"up\\\"}, {\\\"label\\\": \\\"向下滚动\\\", \\\"value\\\": \\\"down\\\"}]}, {\\\"label\\\": \\\"滚动间隔时间设置\\\", \\\"value\\\": \\\"option.scroll.interval\\\"}, {\\\"label\\\": \\\"滚动数量设置\\\", \\\"value\\\": \\\"option.scroll.count\\\"}, {\\\"label\\\": \\\"滚动动画时长设置\\\", \\\"value\\\": \\\"option.scroll.duration\\\"},]},{name: \'圆形进度图文本配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"标题字体大小设置\\\", \\\"value\\\": \\\"option.titleStyle.fontSize\\\"}, {\\\"label\\\": \\\"标题字体颜色设置\\\", \\\"value\\\": \\\"option.titleStyle.fontColor\\\"}, {\\\"label\\\": \\\"标题字体粗细设置\\\", \\\"value\\\": \\\"option.titleStyle.fontWeight\\\"}, {\\\"label\\\": \\\"标题字体样式设置\\\", \\\"value\\\": \\\"option.titleStyle.fontStyle\\\"}, {\\\"label\\\": \\\"标题字间距设置\\\", \\\"value\\\": \\\"option.titleStyle.letterSpacing\\\"}, {\\\"label\\\": \\\"标题字体设置\\\", \\\"value\\\": \\\"option.titleStyle.fontFamily\\\"}, {\\\"label\\\": \\\"标题启用渐变\\\", \\\"value\\\": \\\"option.titleStyle.fontGradient.enabled\\\"}, {\\\"label\\\": \\\"标题渐变起始颜色设置\\\", \\\"value\\\": \\\"option.titleStyle.fontGradient.startColor\\\"}, {\\\"label\\\": \\\"标题渐变结束颜色设置\\\", \\\"value\\\": \\\"option.titleStyle.fontGradient.endColor\\\"}, {\\\"label\\\": \\\"标题垂直位置设置\\\", \\\"value\\\": \\\"option.titleStyle.top\\\"},]},{name: \'圆形进度图数据配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"数据字体大小设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontSize\\\"}, {\\\"label\\\": \\\"数据字体颜色设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontColor\\\"}, {\\\"label\\\": \\\"数据字体粗细设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontWeight\\\"}, {\\\"label\\\": \\\"数据字体样式设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontStyle\\\"}, {\\\"label\\\": \\\"数据字间距设置\\\", \\\"value\\\": \\\"option.subTitleStyle.letterSpacing\\\"}, {\\\"label\\\": \\\"数据字体设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontFamily\\\"}, {\\\"label\\\": \\\"数据启用渐变\\\", \\\"value\\\": \\\"option.subTitleStyle.fontGradient.enabled\\\"}, {\\\"label\\\": \\\"数据渐变起始颜色设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontGradient.startColor\\\"}, {\\\"label\\\": \\\"数据渐变结束颜色设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontGradient.endColor\\\"}, {\\\"label\\\": \\\"数据垂直位置设置\\\", \\\"value\\\": \\\"option.subTitleStyle.top\\\"},]},{name: \'圆形进度图进度条配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"进度条外半径设置\\\", \\\"value\\\": \\\"option.polar.innerRadius\\\"}, {\\\"label\\\": \\\"进度条内半径设置\\\", \\\"value\\\": \\\"option.polar.outerRadius\\\"}, {\\\"label\\\": \\\"进度条背景色设置\\\", \\\"value\\\": \\\"option.backgroundStyle.color\\\"}, {\\\"label\\\": \\\"进度条启用渐变\\\", \\\"value\\\": \\\"option.progressGradient.enabled\\\"}, {\\\"label\\\": \\\"进度条渐变起始颜色设置\\\", \\\"value\\\": \\\"option.progressGradient.startColor\\\"}, {\\\"label\\\": \\\"进度条渐变结束颜色设置\\\", \\\"value\\\": \\\"option.progressGradient.endColor\\\"},]},{name: \'圆形进度图外圆配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"外圆半径设置\\\", \\\"value\\\": \\\"option.outerCircle.radius\\\"}, {\\\"label\\\": \\\"外圆边框颜色设置\\\", \\\"value\\\": \\\"option.outerCircle.borderColor\\\"}, {\\\"label\\\": \\\"外圆边框大小设置\\\", \\\"value\\\": \\\"option.outerCircle.borderWidth\\\"},]},{name: \'圆形进度图内圆配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"内圆半径设置\\\", \\\"value\\\": \\\"option.innerCircle.radius\\\"}, {\\\"label\\\": \\\"内圆边框颜色设置\\\", \\\"value\\\": \\\"option.innerCircle.borderColor\\\"}, {\\\"label\\\": \\\"内圆边框大小设置\\\", \\\"value\\\": \\\"option.innerCircle.borderWidth\\\"},]},{name: \'水波图设置\', optionName: \'LiquidPlotOption\', children: [{\\\"label\\\": \\\"显示类型\\\", \\\"value\\\": \\\"option.liquidType\\\"}, {\\\"label\\\": \\\"波纹颜色\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"波纹个数\\\", \\\"value\\\": \\\"option.count\\\"}, {\\\"label\\\": \\\"波纹长度\\\", \\\"value\\\": \\\"option.length\\\"}, {\\\"label\\\": \\\"外框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"}, {\\\"label\\\": \\\"外框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"}, {\\\"label\\\": \\\"间距\\\", \\\"value\\\": \\\"option.distance\\\"}, {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.strokeOpacity\\\"}, {\\\"label\\\": \\\"文本颜色配置\\\", \\\"value\\\": \\\"option.textColor\\\"}, {\\\"label\\\": \\\"文本字体大小配置\\\", \\\"value\\\": \\\"option.textFontSize\\\"}]},{name: \'象形图设置\', optionName: \'PictorialOption\', children: [{\\\"label\\\": \\\"象形图柱体颜色设置\\\", \\\"value\\\": \\\"option.barColor\\\"}, {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.barOpacity\\\"}, {\\\"label\\\": \\\"间距设置\\\", \\\"value\\\": \\\"option.count\\\"}]},{name: \'仪表盘设置\', optionName: \'GaugeOption\', children: [{\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.series[0].axisLabel.show\\\"}, {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.series[0].axisLabel.color\\\"}, {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.series[0].axisLabel.fontSize\\\"}, {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.series[0].axisTick.show\\\"}, {\\\"label\\\": \\\"刻度线长度\\\", \\\"value\\\": \\\"option.series[0].axisTick.length\\\"}, {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.series[0].axisTick.lineStyle.color\\\"}, {\\\"label\\\": \\\"显示分割线\\\", \\\"value\\\": \\\"option.series[0].splitLine.show\\\"}, {\\\"label\\\": \\\"分割线长度\\\", \\\"value\\\": \\\"option.series[0].splitLine.length\\\"}, {\\\"label\\\": \\\"分割线颜色\\\", \\\"value\\\": \\\"option.series[0].splitLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"指标字号\\\", \\\"value\\\": \\\"option.series[0].detail.fontSize\\\"},]},{name: \'渐变仪表盘设置\', optionName: \'AntvGaugeOption\', children: [{\\\"label\\\": \\\"仪表盘粗细设置\\\", \\\"value\\\": \\\"option.gaugeWidth\\\"}, {\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.axisLabelShow\\\"}, {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.axisLabelColor\\\"}, {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.axisLabelFontSize\\\"}, {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.axisTickShow\\\"}, {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.lineColor\\\"}, {\\\"label\\\": \\\"文本颜色\\\", \\\"value\\\": \\\"option.valueColor\\\"}, {\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"}, {\\\"label\\\": \\\"指针颜色\\\", \\\"value\\\": \\\"option.indicatorColor\\\"}, {\\\"label\\\": \\\"指针粗细\\\", \\\"value\\\": \\\"option.indicatorLength\\\"},]},{name: \'尺寸设置\', optionName: \'Pyramid3DOption\', children: [{\\\"label\\\": \\\"缩放\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"尺寸\\\", \\\"value\\\": \\\"option.size\\\"}]},{name: \'环形设置\', optionName: \'RingOption\', children: [{\\\"label\\\": \\\"内半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"外半径\\\", \\\"value\\\": \\\"option.outRadius\\\"}]},{name: \'环形图设置\', optionName: \'ActiveRingPlotOption\', children: [{\\\"label\\\": \\\"环形图颜色设置\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"环形图背景色设置\\\", \\\"value\\\": \\\"option.bgColor\\\"}, {\\\"label\\\": \\\"环形图外环半径\\\", \\\"value\\\": \\\"option.outRadius\\\"}, {\\\"label\\\": \\\"环形图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"环形图标题字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"环形图标题字体颜色\\\", \\\"value\\\": \\\"option.fontColor\\\"}, {\\\"label\\\": \\\"环形图标题字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"}, {\\\"label\\\": \\\"环形图数值字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"}, {\\\"label\\\": \\\"环形图数值字体颜色\\\", \\\"value\\\": \\\"option.valueFontColor\\\"}, {\\\"label\\\": \\\"环形图数值字体粗细\\\", \\\"value\\\": \\\"option.valueFontWeight\\\"},]},{name: \'动态环形图设置\', optionName: \'ActiveRingOption\', children: [{\\\"label\\\": \\\"动态环形图显示原始值\\\", \\\"value\\\": \\\"option.showOriginValue\\\"}, {\\\"label\\\": \\\"动态环形图文字颜色\\\", \\\"value\\\": \\\"option.textColor\\\"}, {\\\"label\\\": \\\"动态环形图文字大小\\\", \\\"value\\\": \\\"option.textFontSize\\\"}, {\\\"label\\\": \\\"动态环形图线条宽度\\\", \\\"value\\\": \\\"option.lineWidth\\\"}, {\\\"label\\\": \\\"动态环形图环半径\\\", \\\"value\\\": \\\"option.radius\\\"}, {\\\"label\\\": \\\"动态环形图动态环半径\\\", \\\"value\\\": \\\"option.activeRadius\\\"},]},{name: \'玉珏设置\', optionName: \'RadialBarOption\', children: [{\\\"label\\\": \\\"玉珏图显示圆角\\\", \\\"value\\\": \\\"option.radiuShow\\\"}, {\\\"label\\\": \\\"玉珏图背景显示\\\", \\\"value\\\": \\\"option.bgShow\\\"}, {\\\"label\\\": \\\"玉珏图外环半径\\\", \\\"value\\\": \\\"option.radius\\\"}, {\\\"label\\\": \\\"玉珏图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"玉珏图最大旋转角\\\", \\\"value\\\": \\\"option.maxAngle\\\"},]},{name: \'矩形图设置\', optionName: \'RectangleOption\', children: [{\\\"label\\\": \\\"矩形图文本颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"矩形图文本字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"}, {\\\"label\\\": \\\"矩形图显示图例\\\", \\\"value\\\": \\\"option.showLegend\\\"},]},{name: \'文本设置\', optionName: \'TextOption\', children: [{\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.body.fontSize\\\"}, {\\\"label\\\": \\\"文本字体间距\\\", \\\"value\\\": \\\"option.body.letterSpacing\\\"}, {\\\"label\\\": \\\"文本字体颜色\\\", \\\"value\\\": \\\"option.body.color\\\"}, {\\\"label\\\": \\\"文本启用千分符\\\", \\\"value\\\": \\\"option.body.thousandSeparator\\\"}, {\\\"label\\\": \\\"文本水平间距\\\", \\\"value\\\": \\\"option.body.marginLeft\\\"}, {\\\"label\\\": \\\"文本垂直间距\\\", \\\"value\\\": \\\"option.body.marginTop\\\"}, {\\\"label\\\": \\\"文本开启跑马灯\\\", \\\"value\\\": \\\"option.horseLamp\\\",ignoreComp: [\'JNumber\']}, {\\\"label\\\": \\\"文本开启超链接\\\", \\\"value\\\": \\\"option.isLink\\\",ignoreComp: [\'JNumber\']}, {\\\"label\\\": \\\"文本超链接地址\\\", \\\"value\\\": \\\"option.openUrl\\\",ignoreComp: [\'JNumber\']},]},{name: \'内部设置\', optionName: \'CountToTextOption\', children: [{\\\"label\\\": \\\"字体粗细设置\\\", \\\"value\\\": \\\"option.fontWeight\\\"}, {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.fontColor\\\"}, {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"前缀文本内容设置\\\", \\\"value\\\": \\\"option.prefix\\\"}, {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.prefixFontSize\\\"}, {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixColor\\\"}, {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"}, {\\\"label\\\": \\\"前缀字体对齐方式\\\", \\\"value\\\": \\\"option.prefixTextAlign\\\"}, {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixGridX\\\"}, {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixGridY\\\"}, {\\\"label\\\": \\\"后缀文本内容设置\\\", \\\"value\\\": \\\"option.suffix\\\"}, {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"}, {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixColor\\\"}, {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"}, {\\\"label\\\": \\\"后缀字体对齐方式\\\", \\\"value\\\": \\\"option.suffixTextAlign\\\"}, {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixGridX\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"},]},{name: \'颜色块设置\', optionName: \'ColorBlockOption\', children: [{\\\"label\\\": \\\"颜色块行数设置\\\", \\\"value\\\": \\\"option.lineNum\\\"}, {\\\"label\\\": \\\"颜色块边距设置\\\", \\\"value\\\": \\\"option.padding\\\"}, {\\\"label\\\": \\\"颜色块X间距设置\\\", \\\"value\\\": \\\"option.borderSplitx\\\"}, {\\\"label\\\": \\\"颜色块Y间距设置\\\", \\\"value\\\": \\\"option.borderSplity\\\"}, {\\\"label\\\": \\\"小数位数设置\\\", \\\"value\\\": \\\"option.decimals\\\"}, {\\\"label\\\": \\\"字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"字体颜色\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"}, {\\\"label\\\": \\\"字体对齐方式\\\", \\\"value\\\": \\\"option.textAlign\\\"}, {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.borderSplity\\\"}, {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixColor\\\"}, {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"}, {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixSplitx\\\"}, {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixSplity\\\"}, {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"}, {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixColor\\\"}, {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"}, {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixSplitx\\\"},]},{name: \'字体设置\', optionName: \'FlashCloudOption\', children: [{\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.textSize\\\"}, {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.textColor\\\"}]},{name: \'字符云设置\', optionName: \'WordCloudOption\', children: [{\\\"label\\\": \\\"字体颜色配置\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"字体间距设置\\\", \\\"value\\\": \\\"option.padding\\\"}, {\\\"label\\\": \\\"字体旋转设置\\\", \\\"value\\\": \\\"option.rotation\\\"}, {\\\"label\\\": \\\"字体最大值设置\\\", \\\"value\\\": \\\"option.minSize\\\"}, {\\\"label\\\": \\\"字体最小值设置\\\", \\\"value\\\": \\\"option.maxSize\\\"}, {\\\"label\\\": \\\"字体形状设置\\\", \\\"value\\\": \\\"option.series[0].shape\\\"}]},{name: \'轮播表格设置\', optionName: \'ScrollBoardOpt\', children: [{\\\"label\\\": \\\"悬浮暂停设置\\\", \\\"value\\\": \\\"option.hoverPause\\\"}, {\\\"label\\\": \\\"等待时间设置\\\", \\\"value\\\": \\\"option.waitTime\\\"}, {\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.index\\\"}, {\\\"label\\\": \\\"表格列宽\\\", \\\"value\\\": \\\"option.indexWidth\\\"}, {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.headShow\\\"}, {\\\"label\\\": \\\"表头颜色\\\", \\\"value\\\": \\\"option.headerBGC\\\"}, {\\\"label\\\": \\\"表头行高\\\", \\\"value\\\": \\\"option.headerHeight\\\"}, {\\\"label\\\": \\\"每页行数\\\", \\\"value\\\": \\\"option.rowNum\\\"}, {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddRowBGC\\\"}, {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenRowBGC\\\"},]},{name: \'表格设置\', optionName: \'ScrollTableStyle\', children: [{\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.ranking\\\"}, {\\\"label\\\": \\\"开启滚动\\\", \\\"value\\\": \\\"option.scroll\\\"}, {\\\"label\\\": \\\"滚动时间\\\", \\\"value\\\": \\\"option.scrollTime\\\"}, {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.showHead\\\"}, {\\\"label\\\": \\\"表头背景颜色\\\", \\\"value\\\": \\\"option.headerBgColor\\\"}, {\\\"label\\\": \\\"表头字体颜色\\\", \\\"value\\\": \\\"option.headerFontColor\\\"}, {\\\"label\\\": \\\"表头字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"行高设置\\\", \\\"value\\\": \\\"option.lineHeight\\\"}, {\\\"label\\\": \\\"边框显示\\\", \\\"value\\\": \\\"option.showBorder\\\"}, {\\\"label\\\": \\\"边框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"}, {\\\"label\\\": \\\"边框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"}, {\\\"label\\\": \\\"边框线类型\\\", \\\"value\\\": \\\"option.borderStyle\\\"}, {\\\"label\\\": \\\"表格字体颜色\\\", \\\"value\\\": \\\"option.bodyFontColor\\\"}, {\\\"label\\\": \\\"表格字体大小\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"}, {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddColor\\\"}, {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenColor\\\"},]},{name: \'历程设置\', optionName: \'DevHistoryOption\', children: [{\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"轮播间隔\\\", \\\"value\\\": \\\"option.waitTime\\\"}, {\\\"label\\\": \\\"历程背景色\\\", \\\"value\\\": \\\"option.typeBackColor\\\"}, {\\\"label\\\": \\\"历程字体颜色\\\", \\\"value\\\": \\\"option.typeFontColor\\\"}, {\\\"label\\\": \\\"内容字体颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"内容字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"},]},{name: \'数据表格设置\', optionName: \'TableStyle\', children: [{\\\"label\\\": \\\"表头背景颜色\\\", \\\"value\\\": \\\"option.headerBgColor\\\"}, {\\\"label\\\": \\\"表头字体大小\\\", \\\"value\\\": \\\"option.headerFontSize\\\"}, {\\\"label\\\": \\\"表头字体颜色\\\", \\\"value\\\": \\\"option.headerColor\\\"}, {\\\"label\\\": \\\"表体内容字体颜色\\\", \\\"value\\\": \\\"option.bodyColor\\\"}, {\\\"label\\\": \\\"表体内容字体大小\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"}, {\\\"label\\\": \\\"表体内容背景颜色\\\", \\\"value\\\": \\\"option.bodyBgColor\\\"},]},{name: \'列表设置\', optionName: \'ListStyle\', children: [{\\\"label\\\": \\\"显示标题前缀\\\", \\\"value\\\": \\\"option.showTitlePrefix\\\"}, {\\\"label\\\": \\\"显示时间前缀\\\", \\\"value\\\": \\\"option.showTimePrefix\\\"}, {\\\"label\\\": \\\"列表布局设置\\\", \\\"value\\\": \\\"option.layout\\\"}, {\\\"label\\\": \\\"标题字体颜色\\\", \\\"value\\\": \\\"option.titleFontColor\\\"}, {\\\"label\\\": \\\"标题字体粗细\\\", \\\"value\\\": \\\"option.titleFontWeight\\\"}, {\\\"label\\\": \\\"标题字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"}, {\\\"label\\\": \\\"内容图标颜色\\\", \\\"value\\\": \\\"option.iconColor\\\"}, {\\\"label\\\": \\\"内容颜色\\\", \\\"value\\\": \\\"option.contentColor\\\"}, {\\\"label\\\": \\\"开启动画设置\\\", \\\"value\\\": \\\"option.isEnableAnimation\\\"}, {\\\"label\\\": \\\"轮播时间(毫秒)设置\\\", \\\"value\\\": \\\"option.scrollTime\\\"},]},{name: \'滚动设置\', optionName: \'ScrollOption\', children: [{\\\"label\\\": \\\"是否排序\\\", \\\"value\\\": \\\"option.sort\\\"}, {\\\"label\\\": \\\"轮播方式设置单行\\\", \\\"value\\\": \\\"option.carousel\\\",\\\"options\\\": [{\\\"label\\\": \\\"单行\\\", \\\"value\\\": \\\"single\\\"}, {\\\"label\\\": \\\"整页\\\", \\\"value\\\": \\\"page\\\"},]}, {\\\"label\\\": \\\"显示行数\\\", \\\"value\\\": \\\"option.rowNum\\\"}, {\\\"label\\\": \\\"滚动时间(毫秒)设置\\\", \\\"value\\\": \\\"option.waitTime\\\"},]},{name: \'气泡排名设置\', optionName: \'BubbleRankingStyle\', children: [{\\\"label\\\": \\\"比例设置\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"显示提示词\\\", \\\"value\\\": \\\"option.showTip\\\"}, {\\\"label\\\": \\\"提示词颜色设置为\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"提示词宽度设置为\\\", \\\"value\\\": \\\"option.tipWidth\\\"}, {\\\"label\\\": \\\"提示词内容颜色设置\\\", \\\"value\\\": \\\"option.tipFontColor\\\"}, {\\\"label\\\": \\\"提示词内容字体大小设置\\\", \\\"value\\\": \\\"option.tipFontSize\\\"}]},{name: \'地图设置\', optionName: \'MapOption\', children: [{\\\"label\\\": \\\"显示区域名称\\\", \\\"value\\\": \\\"option.geo.label.normal.show\\\"}, {\\\"label\\\": \\\"区域名称颜色设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.color\\\"}, {\\\"label\\\": \\\"区域名称字体大小设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.fontSize\\\"}, {\\\"label\\\": \\\"是否开启钻取\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"}, {\\\"label\\\": \\\"导航文字颜色设置\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"}, {\\\"label\\\": \\\"是否开启鼠标缩放\\\", \\\"value\\\": \\\"option.geo.roam\\\"}, {\\\"label\\\": \\\"缩放比例设置\\\", \\\"value\\\": \\\"option.geo.zoom\\\"}, {\\\"label\\\": \\\"地图长宽比设置\\\", \\\"value\\\": \\\"option.geo.aspectScale\\\"}, {\\\"label\\\": \\\"地图顶边距设置\\\", \\\"value\\\": \\\"option.geo.top\\\"}, {\\\"label\\\": \\\"地图左边距设置\\\", \\\"value\\\": \\\"option.geo.left\\\"},]},{name: \'地图配色设置\', optionName: \'LineMapColorOption\', children: [{\\\"label\\\": \\\"启用渐变色\\\", \\\"value\\\": \\\"commonOption.gradientColor\\\"}, {\\\"label\\\": \\\"中心颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"}, {\\\"label\\\": \\\"边缘颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color2\\\"}, {\\\"label\\\": \\\"区域颜色设置\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"}, {\\\"label\\\": \\\"区域高亮颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.emphasis.areaColor\\\"}, {\\\"label\\\": \\\"区域边界颜色\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.borderColor\\\"}, {\\\"label\\\": \\\"阴影大小设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowBlur\\\"}, {\\\"label\\\": \\\"阴影水平偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetX\\\"}, {\\\"label\\\": \\\"阴影垂直偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetY\\\"}, {\\\"label\\\": \\\"阴影颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowColor\\\"},]},{name: \'视觉映射设置\', optionName: \'VisualMapOptoin\', children: [{\\\"label\\\": \\\"开启视觉映射\\\", \\\"value\\\": \\\"option.visualMap.show\\\"}, {\\\"label\\\": \\\"视觉映射类型\\\", \\\"value\\\": \\\"option.visualMap.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"continuous\\\", \\\"value\\\": \\\"continuous\\\"}, {\\\"label\\\": \\\"piecewise\\\", \\\"value\\\": \\\"piecewise\\\"}]}, {\\\"label\\\": \\\"视觉映射文本颜色\\\", \\\"value\\\": \\\"option.visualMap.textStyle.color\\\"}, {\\\"label\\\": \\\"视觉映射文本粗细\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontWeight\\\"}, {\\\"label\\\": \\\"视觉映射文本字体大小设置\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"区域边界最小值\\\", \\\"value\\\": \\\"option.visualMap.min\\\"}, {\\\"label\\\": \\\"区域边界最大值\\\", \\\"value\\\": \\\"option.visualMap.max\\\"},]},{name: \'地图散点设置\', optionName: \'ScatterOption\', children: [{\\\"label\\\": \\\"地图散点大小设置\\\", \\\"value\\\": \\\"option.area.markerSize\\\"}, {\\\"label\\\": \\\"地图散点形状设置\\\", \\\"value\\\": \\\"option.area.markerShape\\\"}, {\\\"label\\\": \\\"地图散点类型设置\\\", \\\"value\\\": \\\"option.area.markerType\\\"}, {\\\"label\\\": \\\"地图散点颜色设置\\\", \\\"value\\\": \\\"option.area.markerColor\\\"}, {\\\"label\\\": \\\"地图散点文本显示\\\", \\\"value\\\": \\\"option.area.scatterLabelShow\\\"}, {\\\"label\\\": \\\"地图散点文本颜色设置\\\", \\\"value\\\": \\\"option.area.scatterLabelColor\\\"}, {\\\"label\\\": \\\"地图散点文本显示位置设置\\\", \\\"value\\\": \\\"option.area.scatterLabelPosition\\\"}, {\\\"label\\\": \\\"地图散点文本字体大小设置\\\", \\\"value\\\": \\\"option.area.scatterFontSize\\\"}, {\\\"label\\\": \\\"地图散点数量设置\\\", \\\"value\\\": \\\"option.area.markerCount\\\"}, {\\\"label\\\": \\\"地图散点透明度设置\\\", \\\"value\\\": \\\"option.area.markerOpacity\\\"},]},{name: \'热力地图设置\', optionName: \'HeatOption\', children: [{\\\"label\\\": \\\"热力点大小设置\\\", \\\"value\\\": \\\"commonOption.heat.pointSize\\\"}, {\\\"label\\\": \\\"模糊大小设置\\\", \\\"value\\\": \\\"commonOption.heat.blurSize\\\"}, {\\\"label\\\": \\\"最大透明度设置\\\", \\\"value\\\": \\\"commonOption.heat.maxOpacity\\\"},]},{name: \'柱体地图设置\', optionName: \'BarMapOption\', children: [{\\\"label\\\": \\\"柱体地图柱体大小设置\\\", \\\"value\\\": \\\"commonOption.barSize\\\"}, {\\\"label\\\": \\\"柱体左侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor\\\"}, {\\\"label\\\": \\\"柱体右侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor2\\\"},]},{name: \'飞线地图设置\', optionName: \'FlyLineOption\', children: [{\\\"label\\\": \\\"飞线动画时间设置\\\", \\\"value\\\": \\\"commonOption.effect.period\\\"}, {\\\"label\\\": \\\"飞线标记形状设置\\\", \\\"value\\\": \\\"commonOption.effect.markerShape\\\"}, {\\\"label\\\": \\\"飞线标记大小设置\\\", \\\"value\\\": \\\"commonOption.effect.symbolSize\\\"}, {\\\"label\\\": \\\"飞线标记颜色设置\\\", \\\"value\\\": \\\"commonOption.effect.markerColor\\\"}, {\\\"label\\\": \\\"飞线特效尾迹长度设置\\\", \\\"value\\\": \\\"commonOption.effect.trailLength\\\"},]},{name: \'进度设置\', optionName: \'ProgressOption\', children: [{\\\"label\\\": \\\"显示标题\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.show\\\"}, {\\\"label\\\": \\\"标题字体颜色设置\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"}, {\\\"label\\\": \\\"标题字体大小设置\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.fontSize\\\"}, {\\\"label\\\": \\\"数值字体颜色设置\\\", \\\"value\\\": \\\"option.series[1].label.color\\\"}, {\\\"label\\\": \\\"数值字体大小设置\\\", \\\"value\\\": \\\"option.series[1].label.fontSize\\\"}, {\\\"label\\\": \\\"横向偏移设置\\\", \\\"value\\\": \\\"option.valueXOffset\\\"}, {\\\"label\\\": \\\"纵向偏移设置\\\", \\\"value\\\": \\\"option.valueYOffset\\\"}, {\\\"label\\\": \\\"柱体宽度设置\\\", \\\"value\\\": \\\"option.series[0].barWidth\\\"}, {\\\"label\\\": \\\"进度颜色设置\\\", \\\"value\\\": \\\"option.series[0].color\\\"}, {\\\"label\\\": \\\"目标颜色设置\\\", \\\"value\\\": \\\"option.series[1].color\\\"},]},{name: \'南丁格尔玫瑰设置\', optionName: \'RoseOption\', children: [{\\\"label\\\": \\\"边框宽度\\\", \\\"value\\\": \\\"option.series[0].itemStyle.borderWidth\\\"}, {\\\"label\\\": \\\"颜色透明度\\\", \\\"value\\\": \\\"option.series[0].itemStyle.colorOpacity\\\"},]},{name: \'统计概览基本设置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"布局填充类型设置\\\", \\\"value\\\": \\\"option.layout.fill.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"无\\\", \\\"value\\\": \\\"none\\\"}, {\\\"label\\\": \\\"颜色\\\", \\\"value\\\": \\\"color\\\"}, {\\\"label\\\": \\\"图片\\\", \\\"value\\\": \\\"image\\\"}]}, {\\\"label\\\": \\\"布局背景颜色设置\\\", \\\"value\\\": \\\"option.layout.fill.color\\\"}, {\\\"label\\\": \\\"布局启用渐变\\\", \\\"value\\\": \\\"option.layout.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"布局渐变方向设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.direction\\\"}, {\\\"label\\\": \\\"布局渐变起始颜色设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"布局渐变结束颜色设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"布局渐变角度设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.angle\\\"}, {\\\"label\\\": \\\"布局圆角设置\\\", \\\"value\\\": \\\"option.layout.borderRadius\\\"}, {\\\"label\\\": \\\"布局边框宽度设置\\\", \\\"value\\\": \\\"option.layout.borderWidth\\\"}, {\\\"label\\\": \\\"布局边框颜色设置\\\", \\\"value\\\": \\\"option.layout.borderColor\\\"}, {\\\"label\\\": \\\"布局阴影设置\\\", \\\"value\\\": \\\"option.layout.shadow\\\"}, {\\\"label\\\": \\\"布局水平对齐方式设置\\\", \\\"value\\\": \\\"option.layout.justify\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"flex-start\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"flex-end\\\"}, {\\\"label\\\": \\\"两端对齐\\\", \\\"value\\\": \\\"space-between\\\"}, {\\\"label\\\": \\\"两侧留白\\\", \\\"value\\\": \\\"space-around\\\"}]}, {\\\"label\\\": \\\"布局元素间距设置\\\", \\\"value\\\": \\\"option.layout.gap\\\"}, {\\\"label\\\": \\\"布局上内边距设置\\\", \\\"value\\\": \\\"option.layout.padding.top\\\"}, {\\\"label\\\": \\\"布局右内边距设置\\\", \\\"value\\\": \\\"option.layout.padding.right\\\"}, {\\\"label\\\": \\\"布局左内边距设置\\\", \\\"value\\\": \\\"option.layout.padding.left\\\"},]},{name: \'统计概览字段映射\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"标题字段设置\\\", \\\"value\\\": \\\"option.fieldMap.label\\\"}, {\\\"label\\\": \\\"数值字段设置\\\", \\\"value\\\": \\\"option.fieldMap.value\\\"}, {\\\"label\\\": \\\"单位字段设置\\\", \\\"value\\\": \\\"option.fieldMap.unit\\\"}, {\\\"label\\\": \\\"对比字段设置\\\", \\\"value\\\": \\\"option.fieldMap.compareValue\\\"}, {\\\"label\\\": \\\"标签字段设置\\\", \\\"value\\\": \\\"option.fieldMap.compareLabel\\\"}, {\\\"label\\\": \\\"状态字段设置\\\", \\\"value\\\": \\\"option.fieldMap.compareState\\\"}, {\\\"label\\\": \\\"上升值设置\\\", \\\"value\\\": \\\"option.fieldMap.positiveValue\\\"}, {\\\"label\\\": \\\"下降值设置\\\", \\\"value\\\": \\\"option.fieldMap.negativeValue\\\"},]},{name: \'统计概览卡片样式\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"卡片最小宽度设置\\\", \\\"value\\\": \\\"option.card.minWidth\\\"}, {\\\"label\\\": \\\"卡片填充类型设置\\\", \\\"value\\\": \\\"option.card.fill.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"无\\\", \\\"value\\\": \\\"none\\\"}, {\\\"label\\\": \\\"颜色\\\", \\\"value\\\": \\\"color\\\"}, {\\\"label\\\": \\\"图片\\\", \\\"value\\\": \\\"image\\\"}]}, {\\\"label\\\": \\\"卡片底色设置\\\", \\\"value\\\": \\\"option.card.fill.color\\\"}, {\\\"label\\\": \\\"卡片启用渐变\\\", \\\"value\\\": \\\"option.card.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"卡片渐变方向设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.direction\\\"}, {\\\"label\\\": \\\"卡片渐变起始颜色设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"卡片渐变结束颜色设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"卡片渐变角度设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.angle\\\"}, {\\\"label\\\": \\\"卡片圆角设置\\\", \\\"value\\\": \\\"option.card.borderRadius\\\"}, {\\\"label\\\": \\\"卡片边框宽度设置\\\", \\\"value\\\": \\\"option.card.borderWidth\\\"}, {\\\"label\\\": \\\"卡片边框颜色设置\\\", \\\"value\\\": \\\"option.card.borderColor\\\"}, {\\\"label\\\": \\\"卡片垂直内边距设置\\\", \\\"value\\\": \\\"option.card.padding.vertical\\\"}, {\\\"label\\\": \\\"卡片水平内边距设置\\\", \\\"value\\\": \\\"option.card.padding.horizontal\\\"}, {\\\"label\\\": \\\"卡片阴影设置\\\", \\\"value\\\": \\\"option.card.shadow\\\"}, {\\\"label\\\": \\\"卡片模糊程度设置\\\", \\\"value\\\": \\\"option.card.blur\\\"},]},{name: \'统计概览上部配置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"上部显示\\\", \\\"value\\\": \\\"option.sections.top.show\\\"}, {\\\"label\\\": \\\"上部内容类型设置\\\", \\\"value\\\": \\\"option.sections.top.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"主数值\\\", \\\"value\\\": \\\"value\\\"}, {\\\"label\\\": \\\"同比\\\", \\\"value\\\": \\\"compare\\\"}, {\\\"label\\\": \\\"标题\\\", \\\"value\\\": \\\"label\\\"}]}, {\\\"label\\\": \\\"上部水平对齐设置\\\", \\\"value\\\": \\\"option.sections.top.align\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"right\\\"}]}, {\\\"label\\\": \\\"上部上内边距设置\\\", \\\"value\\\": \\\"option.sections.top.paddingTop\\\"}, {\\\"label\\\": \\\"上部下内边距设置\\\", \\\"value\\\": \\\"option.sections.top.paddingBottom\\\"}, {\\\"label\\\": \\\"上部最小高设置\\\", \\\"value\\\": \\\"option.sections.top.minHeight\\\"}, {\\\"label\\\": \\\"上部数值字体大小设置\\\", \\\"value\\\": \\\"option.sections.top.value.fontSize\\\"}, {\\\"label\\\": \\\"上部数值字体颜色设置\\\", \\\"value\\\": \\\"option.sections.top.value.fontColor\\\"}, {\\\"label\\\": \\\"上部数值字体粗细设置\\\", \\\"value\\\": \\\"option.sections.top.value.fontWeight\\\"}, {\\\"label\\\": \\\"上部单位间距设置\\\", \\\"value\\\": \\\"option.sections.top.value.unitGap\\\"}, {\\\"label\\\": \\\"上部单位字体大小设置\\\", \\\"value\\\": \\\"option.sections.top.value.unit.fontSize\\\"}, {\\\"label\\\": \\\"上部单位字体颜色设置\\\", \\\"value\\\": \\\"option.sections.top.value.unit.fontColor\\\"},]},{name: \'统计概览中部配置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"中部显示\\\", \\\"value\\\": \\\"option.sections.middle.show\\\"}, {\\\"label\\\": \\\"中部内容类型设置\\\", \\\"value\\\": \\\"option.sections.middle.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"主数值\\\", \\\"value\\\": \\\"value\\\"}, {\\\"label\\\": \\\"同比\\\", \\\"value\\\": \\\"compare\\\"}, {\\\"label\\\": \\\"标题\\\", \\\"value\\\": \\\"label\\\"}]}, {\\\"label\\\": \\\"中部水平对齐设置\\\", \\\"value\\\": \\\"option.sections.middle.align\\\"}, {\\\"label\\\": \\\"中部上内边距设置\\\", \\\"value\\\": \\\"option.sections.middle.paddingTop\\\"}, {\\\"label\\\": \\\"中部下内边距设置\\\", \\\"value\\\": \\\"option.sections.middle.paddingBottom\\\"}, {\\\"label\\\": \\\"中部最小高设置\\\", \\\"value\\\": \\\"option.sections.middle.minHeight\\\"}, {\\\"label\\\": \\\"中部垂直对齐设置\\\", \\\"value\\\": \\\"option.sections.middle.alignItems\\\"}, {\\\"label\\\": \\\"中部对比标签字体大小设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.labelStyle.fontSize\\\"}, {\\\"label\\\": \\\"中部对比标签字体颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.labelStyle.fontColor\\\"}, {\\\"label\\\": \\\"中部对比数值字体大小设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.fontSize\\\"}, {\\\"label\\\": \\\"中部对比数值字体颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.fontColor\\\"}, {\\\"label\\\": \\\"中部对比上涨颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.positiveColor\\\"}, {\\\"label\\\": \\\"中部对比下降颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.negativeColor\\\"},]},{name: \'统计概览下部配置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"下部显示\\\", \\\"value\\\": \\\"option.sections.bottom.show\\\"}, {\\\"label\\\": \\\"下部内容类型设置\\\", \\\"value\\\": \\\"option.sections.bottom.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"主数值\\\", \\\"value\\\": \\\"value\\\"}, {\\\"label\\\": \\\"同比\\\", \\\"value\\\": \\\"compare\\\"}, {\\\"label\\\": \\\"标题\\\", \\\"value\\\": \\\"label\\\"}]}, {\\\"label\\\": \\\"下部水平对齐设置\\\", \\\"value\\\": \\\"option.sections.bottom.align\\\"}, {\\\"label\\\": \\\"下部上内边距设置\\\", \\\"value\\\": \\\"option.sections.bottom.paddingTop\\\"}, {\\\"label\\\": \\\"下部下内边距设置\\\", \\\"value\\\": \\\"option.sections.bottom.paddingBottom\\\"}, {\\\"label\\\": \\\"下部最小高设置\\\", \\\"value\\\": \\\"option.sections.bottom.minHeight\\\"}, {\\\"label\\\": \\\"下部标题字体大小设置\\\", \\\"value\\\": \\\"option.sections.bottom.label.fontSize\\\"}, {\\\"label\\\": \\\"下部标题字体颜色设置\\\", \\\"value\\\": \\\"option.sections.bottom.label.fontColor\\\"}, {\\\"label\\\": \\\"下部标题字体粗细设置\\\", \\\"value\\\": \\\"option.sections.bottom.label.fontWeight\\\"},]},{name: \'卡片滚动基础配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"排列方向设置\\\", \\\"value\\\": \\\"option.direction\\\", \\\"options\\\": [{\\\"label\\\": \\\"横向排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"竖向排列\\\", \\\"value\\\": \\\"vertical\\\"}]}, {\\\"label\\\": \\\"行间隙设置\\\", \\\"value\\\": \\\"option.rowGap\\\"}, {\\\"label\\\": \\\"列间隙设置\\\", \\\"value\\\": \\\"option.columnGap\\\"},]},{name: \'卡片滚动滚动配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"是否滚动设置\\\", \\\"value\\\": \\\"option.autoScrollEnabled\\\"}, {\\\"label\\\": \\\"滚动方向设置\\\", \\\"value\\\": \\\"option.scrollDirection\\\", \\\"options\\\": [{\\\"label\\\": \\\"向左滚动\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"向上滚动\\\", \\\"value\\\": \\\"up\\\"}]}, {\\\"label\\\": \\\"滚动个数设置\\\", \\\"value\\\": \\\"option.scrollCount\\\"}, {\\\"label\\\": \\\"停留时间设置\\\", \\\"value\\\": \\\"option.stayDuration\\\"}, {\\\"label\\\": \\\"动画时长设置\\\", \\\"value\\\": \\\"option.animationDuration\\\"},]},{name: \'卡片滚动卡片配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"卡片宽度设置\\\", \\\"value\\\": \\\"option.cardStyle.width\\\"}, {\\\"label\\\": \\\"卡片高度设置\\\", \\\"value\\\": \\\"option.cardStyle.height\\\"}, {\\\"label\\\": \\\"卡片背景色设置\\\", \\\"value\\\": \\\"option.cardStyle.backgroundColor\\\"}, {\\\"label\\\": \\\"卡片背景图片设置\\\", \\\"value\\\": \\\"option.cardStyle.backgroundImage\\\"}, {\\\"label\\\": \\\"卡片高亮图片设置\\\", \\\"value\\\": \\\"option.cardStyle.bgHighlightImage\\\"}, {\\\"label\\\": \\\"卡片圆角设置\\\", \\\"value\\\": \\\"option.cardStyle.borderRadius\\\"}, {\\\"label\\\": \\\"卡片边框显示\\\", \\\"value\\\": \\\"option.cardStyle.borderEnabled\\\"}, {\\\"label\\\": \\\"卡片边框颜色设置\\\", \\\"value\\\": \\\"option.cardStyle.borderColor\\\"}, {\\\"label\\\": \\\"卡片边框样式设置\\\", \\\"value\\\": \\\"option.cardStyle.borderStyle\\\", \\\"options\\\": [{\\\"label\\\": \\\"实线\\\", \\\"value\\\": \\\"solid\\\"}, {\\\"label\\\": \\\"虚线\\\", \\\"value\\\": \\\"dashed\\\"}, {\\\"label\\\": \\\"点线\\\", \\\"value\\\": \\\"dotted\\\"}, {\\\"label\\\": \\\"双线\\\", \\\"value\\\": \\\"double\\\"}]}, {\\\"label\\\": \\\"卡片边框宽度设置\\\", \\\"value\\\": \\\"option.cardStyle.borderWidth\\\"}, {\\\"label\\\": \\\"卡片上内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingTop\\\"}, {\\\"label\\\": \\\"卡片右内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingRight\\\"}, {\\\"label\\\": \\\"卡片下内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingBottom\\\"}, {\\\"label\\\": \\\"卡片左内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingLeft\\\"},]},{name: \'卡片滚动字段配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"显示序号\\\", \\\"value\\\": \\\"option.showIndex\\\"}, {\\\"label\\\": \\\"字段排列方式设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.layoutDirection\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平方向(左-右)\\\", \\\"value\\\": \\\"row\\\"}, {\\\"label\\\": \\\"水平方向(右-左)\\\", \\\"value\\\": \\\"row-reverse\\\"}, {\\\"label\\\": \\\"垂直方向(上-下)\\\", \\\"value\\\": \\\"column\\\"}, {\\\"label\\\": \\\"垂直方向(下-上)\\\", \\\"value\\\": \\\"column-reverse\\\"}]}, {\\\"label\\\": \\\"字段水平对齐设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.justifyContent\\\"}, {\\\"label\\\": \\\"字段垂直对齐设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.alignItems\\\"}, {\\\"label\\\": \\\"字段宽度设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.width\\\"}, {\\\"label\\\": \\\"字段高度设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.height\\\"}, {\\\"label\\\": \\\"字段上边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginTop\\\"}, {\\\"label\\\": \\\"字段下边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginBottom\\\"}, {\\\"label\\\": \\\"字段左边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginLeft\\\"}, {\\\"label\\\": \\\"字段右边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginRight\\\"}, {\\\"label\\\": \\\"字段省略显示\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].omitConfig.show\\\"}, {\\\"label\\\": \\\"字段省略行数设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].omitConfig.lines\\\"}, {\\\"label\\\": \\\"字段千分符显示\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].thousandSeparatorConfig.show\\\"}, {\\\"label\\\": \\\"字段显示标签\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].showLabel\\\"}, {\\\"label\\\": \\\"字段显示值\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].showValue\\\"}, {\\\"label\\\": \\\"字段值类型设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].valueType\\\", \\\"options\\\": [{\\\"label\\\": \\\"非数组\\\", \\\"value\\\": \\\"non-array\\\"}, {\\\"label\\\": \\\"数组\\\", \\\"value\\\": \\\"array\\\"}]},]},{name: \'滚动列表基本配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.showHeader\\\"}, {\\\"label\\\": \\\"每行数量设置\\\", \\\"value\\\": \\\"option.itemsPerRow\\\"}, {\\\"label\\\": \\\"列间距设置\\\", \\\"value\\\": \\\"option.gridGap\\\"},]},{name: \'滚动列表滚动配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"是否滚动设置\\\", \\\"value\\\": \\\"option.autoScrollEnabled\\\"}, {\\\"label\\\": \\\"滚动时长设置\\\", \\\"value\\\": \\\"option.autoScrollInterval\\\"},]},{name: \'滚动列表容器配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"容器背景色设置\\\", \\\"value\\\": \\\"option.backgroundColor\\\"}, {\\\"label\\\": \\\"容器圆角设置\\\", \\\"value\\\": \\\"option.borderRadius\\\"}, {\\\"label\\\": \\\"容器左边距设置\\\", \\\"value\\\": \\\"option.marginLeft\\\"}, {\\\"label\\\": \\\"容器右边距设置\\\", \\\"value\\\": \\\"option.marginRight\\\"},]},{name: \'滚动列表表头配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"表头高度设置\\\", \\\"value\\\": \\\"option.header.height\\\"}, {\\\"label\\\": \\\"表头背景色设置\\\", \\\"value\\\": \\\"option.header.backgroundColor\\\"}, {\\\"label\\\": \\\"表头字体大小设置\\\", \\\"value\\\": \\\"option.header.fontSize\\\"}, {\\\"label\\\": \\\"表头字体颜色设置\\\", \\\"value\\\": \\\"option.header.fontColor\\\"}, {\\\"label\\\": \\\"表头字体粗细设置\\\", \\\"value\\\": \\\"option.header.fontWeight\\\"}, {\\\"label\\\": \\\"表头字体样式设置\\\", \\\"value\\\": \\\"option.header.fontStyle\\\"}, {\\\"label\\\": \\\"表头字间距设置\\\", \\\"value\\\": \\\"option.header.letterSpacing\\\"}, {\\\"label\\\": \\\"表头字体设置\\\", \\\"value\\\": \\\"option.header.fontFamily\\\"}, {\\\"label\\\": \\\"表头启用渐变\\\", \\\"value\\\": \\\"option.header.fontGradient.enabled\\\"}, {\\\"label\\\": \\\"表头渐变起始颜色设置\\\", \\\"value\\\": \\\"option.header.fontGradient.startColor\\\"}, {\\\"label\\\": \\\"表头渐变结束颜色设置\\\", \\\"value\\\": \\\"option.header.fontGradient.endColor\\\"}, {\\\"label\\\": \\\"表头对齐方式设置\\\", \\\"value\\\": \\\"option.header.textAlign\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"right\\\"}]},]},{name: \'滚动列表行配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"文本多行展示\\\", \\\"value\\\": \\\"option.row.isMultiline\\\"}, {\\\"label\\\": \\\"行背景类型设置\\\", \\\"value\\\": \\\"option.row.backgroundType\\\", \\\"options\\\": [{\\\"label\\\": \\\"背景色\\\", \\\"value\\\": \\\"color\\\"}, {\\\"label\\\": \\\"背景图\\\", \\\"value\\\": \\\"image\\\"}]}, {\\\"label\\\": \\\"行背景色设置\\\", \\\"value\\\": \\\"option.row.backgroundColor\\\"}, {\\\"label\\\": \\\"交替行背景色设置\\\", \\\"value\\\": \\\"option.row.alternateBackgroundColor\\\"}, {\\\"label\\\": \\\"行背景图片设置\\\", \\\"value\\\": \\\"option.row.backgroundImg\\\"}, {\\\"label\\\": \\\"行高度设置\\\", \\\"value\\\": \\\"option.row.height\\\"}, {\\\"label\\\": \\\"行内边距设置\\\", \\\"value\\\": \\\"option.row.padding\\\"}, {\\\"label\\\": \\\"行上边距设置\\\", \\\"value\\\": \\\"option.row.marginTop\\\"}, {\\\"label\\\": \\\"行下边距设置\\\", \\\"value\\\": \\\"option.row.marginBottom\\\"}, {\\\"label\\\": \\\"行左边距设置\\\", \\\"value\\\": \\\"option.row.marginLeft\\\"}, {\\\"label\\\": \\\"行右边距设置\\\", \\\"value\\\": \\\"option.row.marginRight\\\"},]},{name: \'滚动列表字段配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"显示序号\\\", \\\"value\\\": \\\"option.showIndex\\\"}, {\\\"label\\\": \\\"字段文本对齐设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].textAlign\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"right\\\"}]}, {\\\"label\\\": \\\"字段宽度设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].width\\\"}, {\\\"label\\\": \\\"字段图片宽度设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].imageStyle.width\\\"}, {\\\"label\\\": \\\"字段图片高度设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].imageStyle.height\\\"}, {\\\"label\\\": \\\"字段图片圆角设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].imageStyle.borderRadius\\\"}, {\\\"label\\\": \\\"字段左边距设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].marginLeft\\\"}, {\\\"label\\\": \\\"字段右边距设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].marginRight\\\"},]}];\\n\\n\"},{\"role\":\"user\",\"content\":\"用户的问题:{{userQuestion}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userQuestion\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"269049045129183232\",\"type\":\"end\",\"x\":1272,\"y\":459,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{option}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"269048862303666176\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"269048862299471872\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"269048862299471872_input\",\"pointsList\":[{\"x\":466,\"y\":422},{\"x\":566,\"y\":422},{\"x\":522,\"y\":414},{\"x\":622,\"y\":414}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"269049045129183233\",\"type\":\"base-edge\",\"sourceNodeId\":\"269048862299471872\",\"targetNodeId\":\"269049045129183232\",\"sourceAnchorId\":\"269048862299471872_output\",\"targetAnchorId\":\"269049045129183232_input\",\"pointsList\":[{\"x\":954,\"y\":414},{\"x\":1054,\"y\":414},{\"x\":1006,\"y\":422},{\"x\":1106,\"y\":422}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}' WHERE `id` = '2005948202528501762'; + +-- AI语音大写 +UPDATE `sys_permission` SET `name` = 'AI语音' WHERE `id` = '2029045802703740929'; + +-- 聊天skills对接online应用 +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2033783399720075266', 'admin', '2026-03-17 13:52:03', 'admin', '2026-03-20 18:37:39', 'A05A01A01', NULL, 'skills对接online表单', 'skills对接online表单(目前支持单表)', '', 'chatSimple', '', '', '1890232564262739969', '', NULL, 'enable', 10, '{\"modelInfo\":{\"provider\":\"OPENAI\",\"modelType\":\"LLM\",\"modelName\":\"gpt-4o\"}}', '[]', NULL, NULL, NULL, NULL, NULL, NULL); + +-- 数据库插件新增工具 +UPDATE `airag_mcp` SET `icon` = NULL, `name` = '数据库插件', `descr` = '用于执行数据库操作', `category` = 'plugin', `type` = 'api', `endpoint` = '', `headers` = '{\"X-Sign\":\"true\"}', `tools` = '[{\"name\":\"queryTableMetadata\",\"description\":\"用于查询表的表结构(元数据)\",\"path\":\"/airag/mcp/database/queryTableMetadata\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"tableName\",\"description\":\"表名\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"dbSourceKey\",\"description\":\"数据源key\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result.tableName\",\"description\":\"表名(数据库实际表名)\",\"type\":\"Object\"},{\"name\":\"result.tableComment\",\"description\":\"表注释(业务含义)\",\"type\":\"Object\"},{\"name\":\"result.columns[].columnName\",\"description\":\"字段名\",\"type\":\"Array\"},{\"name\":\"result.columns[].columnComment\",\"description\":\"字段注释(核心,帮助大模型理解业务)\",\"type\":\"Array\"},{\"name\":\"result.columns[].dataType\",\"description\":\"数据类型(如varchar、int、datetime)\",\"type\":\"Array\"},{\"name\":\"result.columns[].isPrimaryKey\",\"description\":\"是否主键\",\"type\":\"Array\"}]},{\"name\":\"sqlExecute\",\"description\":\"用于执行 SQL 语句,仅能支持执行SELECT语句,不要输入注释等无关信息。\",\"path\":\"/airag/mcp/database/sqlExecute\",\"method\":\"POST\",\"enabled\":true,\"parameters\":[{\"name\":\"sql\",\"description\":\"要执行的SQL\",\"type\":\"String\",\"location\":\"Body\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"dbSourceKey\",\"description\":\"数据源key\",\"type\":\"String\",\"location\":\"Body\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[{\"name\":\"success\",\"description\":\"是否成功\",\"type\":\"Boolean\"},{\"name\":\"message\",\"description\":\"若失败则返回失败原因\",\"type\":\"String\"},{\"name\":\"result\",\"description\":\"返回查询的结果,是个对象数组,数组的每一项都是一条数据,每条数据的key都是传入的查询的列。\",\"type\":\"Array\"}]},{\"name\":\"queryTablesInfoText\",\"description\":\"用于查询指定数据源的所有表名和描述\",\"path\":\"/airag/mcp/database/queryTablesInfoText\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源code,不填则系统默认\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]},{\"name\":\"queryDataSourceInfoText\",\"description\":\"用于查询所有数据源的信息,不需要传递参数。\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[],\"responses\":[]},{\"name\":\"queryDataSourceType\",\"description\":\"获取默认数据源或指定数据的数据库类型\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"type\":\"String\",\"location\":\"Query\",\"required\":false,\"defaultValue\":\"\"}],\"responses\":[]},{\"name\":\"getChartExampleJson\",\"description\":\"用户获取图表示例数据\",\"path\":\"/airag/mcp/database/getChartExampleJson\",\"method\":\"GET\",\"enabled\":true,\"parameters\":[{\"name\":\"type\",\"description\":\"图表类型,多个用英文逗号分割\",\"type\":\"String\",\"location\":\"Query\",\"required\":true,\"defaultValue\":\"\"}],\"responses\":[]},{\"name\":\"sqlPageExecute\",\"description\":\"分页执行 SQL 查询(仅支持 SELECT)\",\"path\":\"/airag/mcp/database/sqlPageExecute\",\"method\":\"POST\",\"enabled\":true,\"parameters\":[{\"name\":\"sql\",\"description\":\"原始sql,无需传入分页sql\",\"type\":\"String\",\"location\":\"Body\",\"required\":true,\"defaultValue\":\"\"},{\"name\":\"dbSourceKey\",\"description\":\"数据源,可为空\",\"type\":\"String\",\"location\":\"Body\",\"required\":false,\"defaultValue\":\"\"},{\"name\":\"pageNo\",\"description\":\"当前页码\",\"type\":\"Number\",\"location\":\"Body\",\"required\":false,\"defaultValue\":\"1\"},{\"name\":\"pageSize\",\"description\":\"每页页数\",\"type\":\"Number\",\"location\":\"Body\",\"required\":false,\"defaultValue\":\"10\"}],\"responses\":[{\"name\":\"records\",\"description\":\"数据行\",\"type\":\"Array\"},{\"name\":\"total\",\"description\":\"总数\",\"type\":\"Number\"}]}]', `status` = 'enable', `synced` = 1, `metadata` = '{\"tokenParamName\":\"X-Access-Token\",\"tool_count\":7,\"authType\":\"token\",\"tokenParamValue\":\"\"}', `create_by` = 'admin', `create_time` = '2025-12-31 16:52:26', `update_by` = 'admin', `update_time` = '2026-03-20 17:57:03', `sys_org_code` = 'A01', `tenant_id` = NULL WHERE `id` = '2006287314794676226'; + +-- 更新 Chat2BI 提示词 +UPDATE `airag_flow` SET `create_by` = 'admin', `create_time` = '2026-01-06 11:25:05', `update_by` = 'admin', `update_time` = '2026-03-20 19:03:31', `sys_org_code` = 'A01', `tenant_id` = NULL, `application_name` = 'ghb', `name` = 'Chat2BI生成图表', `descr` = '', `icon` = '', `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":99,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位严谨的数据可视化助手。你的唯一职责是:根据用户需求,从数据库查询数据并生成 `` 图表标签。你不处理任何与图表生成无关的请求。\\n## 核心原则\\n1. **数据真实性**:所有图表数据必须来源于 SQL 查询结果或用户直接提供的数据,严禁虚构、编造、推测任何数据。\\n2. **格式严格性**:输出必须严格遵循指定的 `` 标签格式,不得有任何偏差。\\n3. **最小权限**:仅对下方列出的已授权表执行 SELECT 查询,拒绝一切超出范围的操作。\\n4. **隐私合规**:禁止输出可识别个人身份的敏感信息(完整身份证号、详细住址、明文密码等),涉及此类字段必须脱敏或拒绝。\\n## 图表类型定义\\n### 简单图表(直接使用 x/y 格式)\\n| type | 名称 | data 格式 |\\n|------|------|-----------|\\n| `bar` | 柱状图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n| `line` | 折线图/曲线图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n| `pie` | 饼图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n### 复杂图表(必须先查询示例格式)\\n| type | 名称 |\\n|------|------|\\n| `radar` | 雷达图 |\\n| `gauge` | 仪表盘 |\\n| `barline` | 折柱混合图 |\\n| `multibar` | 多列柱状图 |\\n| `multiline` | 多行折线图 |\\n| `area` | 面积图 |\\n## 工作流程(严格按顺序执行)\\n### 第一步:需求解析\\n分析用户请求,提取以下信息:\\n- **图表类型**:用户想要哪种图表?若未明确指定,根据数据特征推断最合适的类型。记录用户是否**明确指定**了图表类型(影响后续是否生成 `altTypes`)。\\n- **数据维度**:x 轴(分类/时间)和 y 轴(度量/指标)分别是什么?\\n- **数据来源**:用户是否指定了数据源?是否直接提供了数据?\\n- **筛选条件**:是否有时间范围、分组条件、排序要求、数量限制等?\\n**若需求模糊不可执行**(无法确定表、字段或图表类型),必须向用户提问澄清,不得猜测执行。\\n### 第二步:判断数据来源\\n```\\n用户已直接提供数据?\\n├─ 是 → 跳到第四步(数据转换)\\n└─ 否 → 继续第三步(数据库查询)\\n```\\n### 第三步:数据库查询\\n**3.1 验证表范围**\\n检查需求涉及的表是否在下方「支持的数据源」列表中。\\n- 若不在列表中 → 立即告知用户\\\"该表不在可查询范围内\\\",终止流程。\\n- 若在列表中 → 继续。\\n**3.2 查询表结构**\\n调用工具查询相关表的字段结构,了解可用列名、数据类型和字段备注。\\n字段备注将用于数据表格的列标题显示,请在构建 `columns` 时使用。\\n**3.3 构建 SQL**\\n你需要构建两条 SQL:\\n1. **图表聚合 SQL**(用于图表渲染):包含 `GROUP BY`、聚合函数等,产出图表所需的汇总数据。\\n2. **原始数据 SQL**(用于数据表格展示):查询图表统计所依赖的原始明细数据,不包含 `GROUP BY` 和聚合函数,不添加 `LIMIT` 分页限制(系统会自动处理分页)。\\n两条 SQL 严格遵守以下规则:\\n- 仅允许 `SELECT` 语句,禁止 `INSERT`/`UPDATE`/`DELETE`/`DROP`/`ALTER`/`TRUNCATE` 等任何非查询操作。\\n- 禁止 SQL 注释(`--`、`/* */`)。\\n- 根据当前数据源的数据库类型(见「默认数据源类型」)使用对应的 SQL 方言。\\n- SQL 必须高效:使用适当的 `WHERE` 条件避免全表扫描。\\n- 图表聚合 SQL 在数据量可能较大时,默认添加合理的 `LIMIT`(建议不超过 100 条)。\\n- 原始数据 SQL 禁止添加 `LIMIT`,分页由系统自动处理。\\n**3.4 执行查询**\\n调用工具执行**图表聚合 SQL**,获取结果集。若查询返回空数据,告知用户未查询到数据,终止流程。\\n**3.5 验证原始数据 SQL**\\n构建完原始数据 SQL 后,必须调用 `sqlPageExecute` 工具进行验证,参数设置为 `pageNo=1, pageSize=1`,仅查询 1 条数据用于验证 SQL 语法的正确性。\\n- 若验证通过 → 将该 SQL 放入输出的 `sql` 字段。\\n- 若验证失败 → 根据错误信息修正 SQL 后再次验证,最多重试 3 次。若仍失败,则不输出 `sql` 字段(图表仍正常渲染,但不提供数据表格功能)。\\n### 第四步:图表格式确定\\n```\\n图表类型是 bar/line/pie(简单图表)?\\n├─ 是 → 直接使用 [{\\\"x\\\":\\\"...\\\", \\\"y\\\":...}] 格式,禁止调用示例查询工具\\n└─ 否 → 该复杂图表的示例格式是否已在本次对话中查询过?\\n    ├─ 是 → 复用已有格式,禁止重复调用\\n    └─ 否 → 调用工具查询示例格式(支持逗号分割,一次性查询所有需要的复杂图表类型,禁止逐个查询)\\n```\\n### 第四步半:确定可替代图表类型\\n```\\n用户在第一步中明确指定了图表类型?\\n├─ 是 → altTypes 直接传空数组 [],跳过本步骤\\n└─ 否 → 按下方规则填充 altTypes\\n```\\n当用户未明确指定图表类型(由你自动推断)时,根据当前数据结构,判断哪些其他图表类型可以使用**同一份 data** 直接渲染(无需修改数据格式),将它们填入 `altTypes` 数组。互转规则如下:\\n| 当前类型 | 可替代类型(altTypes 候选) |\\n|----------|--------------------------|\\n| `bar` | `line`、`pie` |\\n| `line` | `bar`、`pie` |\\n| `pie` | `bar`、`line` |\\n| `multibar` | `multiline`、`area` |\\n| `multiline` | `multibar`、`area` |\\n| `area` | `multibar`、`multiline` |\\n| `radar` | 无(数据结构独特) |\\n| `gauge` | 无(单值数据) |\\n| `barline` | 无(含 seriesType 区分) |\\n注意:\\n- `altTypes` 不包含当前主类型(`type` 字段已指定)。\\n- 仅列出数据结构完全兼容的类型,不得列出需要修改 data 格式才能渲染的类型。\\n- 若无可替代类型,`altTypes` 传空数组 `[]`。\\n### 第五步:数据转换\\n将查询结果转换为目标图表格式:\\n- 简单图表:每行数据映射为 `{\\\"x\\\": 字符串, \\\"y\\\": 数字}`。\\n- 复杂图表:严格按照查询到的示例格式组装数据。\\n- `x` 值必须为字符串类型,`y` 值必须为数字类型。\\n- 若需聚合(求和、计数、平均等),在 SQL 中完成,不在转换阶段手动计算。\\n- 数据转换在你的回复中直接完成,禁止调用工具进行转换。\\n### 第六步:输出\\n生成最终结果前,执行双重校验:\\n1. **标签校验**:`` 和 `` 首尾完整闭合。\\n2. **JSON 校验**:`` 内的 JSON 是标准格式——无多余逗号、无未闭合括号、无尾随逗号、所有键名使用双引号。\\n3. **数据校验**:`data` 数组不为空,每个对象包含必需的键。\\n4. **SQL 字段校验**:若数据来自数据库查询,`sql` 字段必须包含原始数据查询 SQL,`dbSource` 字段必须与查询时使用的数据源一致,`columns` 字段必须包含原始数据 SQL 中所有 SELECT 字段的中文标题映射。\\n## 输出格式\\n最终输出必须且仅包含以下格式,`` 标签前后各保留两个空行。禁止在标签外添加额外说明、解释或修饰文字。如需对数据做简短说明,放在标签之前。\\n### 数据来自数据库查询时:\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"altTypes\\\":[\\\"可替代类型1\\\",\\\"可替代类型2\\\"],\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}],\\\"sql\\\":\\\"原始数据查询SQL\\\",\\\"dbSource\\\":\\\"数据源标识或空字符串\\\",\\\"columns\\\":{\\\"field1\\\":\\\"列标题1\\\",\\\"field2\\\":\\\"列标题2\\\"}}\\n\\n\\n### 数据由用户直接提供时:\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"altTypes\\\":[\\\"可替代类型1\\\",\\\"可替代类型2\\\"],\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n### 字段说明\\n| 字段 | 类型 | 必填 | 说明 |\\n|------|------|------|------|\\n| `type` | string | 是 | 图表类型 |\\n| `altTypes` | string[] | 是 | 可替代的图表类型数组。系统会据此提供图表切换功能。无可替代类型时传 `[]` |\\n| `data` | array/object | 是 | 图表展示数据(聚合后的数据) |\\n| `sql` | string | 条件必填 | 原始数据查询 SQL(不含 LIMIT/分页,系统自动处理)。仅当数据来自数据库查询时必填 |\\n| `dbSource` | string | 条件必填 | 数据源标识。默认数据源传空字符串 `\\\"\\\"`,指定数据源传对应的 key。仅当数据来自数据库查询时必填 |\\n| `columns` | object | 条件必填 | 原始数据 SQL 中 SELECT 字段名到中文列标题的映射。仅当数据来自数据库查询时必填。key 为 SQL 中的字段名(或别名),value 为该字段的中文显示标题。标题来源于表结构的字段备注,若备注过长(超过 4 个字),需根据语义总结为 2~4 个字的简短标题 |\\n## 异常处理\\n按以下规则处理异常情况:\\n| 异常场景 | 处理方式 |\\n|----------|----------|\\n| 用户请求的表不在授权范围 | 告知用户该表不在可查询范围内,列出可用的相关表(如有) |\\n| SQL 执行报错 | 分析错误原因,修正 SQL 后重试一次;若仍失败,告知用户具体错误 |\\n| 查询结果为空 | 告知用户未查到符合条件的数据,建议调整筛选条件 |\\n| 工具返回身份验证失败/无权限 | 立即停止所有操作,告知用户:您当前账号没有该数据的访问权限,请登录有权限的账号或联系管理员授权 |\\n| 用户要求执行非 SELECT 操作 | 拒绝并说明仅支持数据查询,不支持数据修改操作 |\\n| 用户要求查看数据源列表 | 直接返回下方列表内容(若表数量超过 50 个则总结性回复),禁止调用 `queryDataSourceInfoText` 工具 |\\n| 用户请求与图表无关的任务 | 礼貌说明你是数据可视化助手,仅处理图表相关需求 |\\n## 禁止行为清单\\n1. 禁止虚构或编造任何数据。\\n2. 禁止执行 `queryDataSourceInfoText` 工具。\\n3. 禁止对简单图表(bar/line/pie)调用示例格式查询工具。\\n4. 禁止对已查询过的复杂图表类型重复调用示例格式查询工具。\\n5. 禁止逐个查询复杂图表示例格式(必须一次性用逗号分割查询)。\\n6. 禁止向用户提及 `ghb-chart` 标签名称或图表格式的技术细节。\\n7. 禁止输出非 SELECT 的 SQL 语句。\\n8. 禁止输出包含 SQL 注释的查询。\\n9. 禁止输出未脱敏的敏感个人信息。\\n10. 禁止在无数据支撑的情况下生成图表标签。\\n## 默认数据源类型\\n{{defDbType}}\\n## 支持的数据源\\n{{allDbSource}}\\n> 注意:\\n> 当用户未指定数据源时,默认数据源应设为空。\\n> 以上是全部支持的数据源,禁止调用 `queryDataSourceInfoText` 工具。当用户询问可用数据源时,直接返回以上列表(表数量超过 50 个时总结性回复)。\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2008379264947519489'; + +-- AI 生成图表、修改配置项-升级SQL +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'267492142677889024\'),\n end.tag(\'267498945805422592\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":629,\"y\":-41,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"267492142677889024\",\"type\":\"llm\",\"x\":1138,\"y\":0,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"## 硬性要求:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。\\n不要输出任何解释、注释或 JSON 以外的文字。\\n# 角色:数据可视化专家\\n你是一位精通ECharts的数据可视化和大屏配置的专家,能够根据用户需求,智能选择最合适的图表类型,并生成高质量、可直接使用的ECharts配置项。\\n## 目标:\\n1. 根据用户提供的需求描述,分析其核心意图(如趋势分析、比较分析、占比分析等)。\\n2. 从下面给定的图表组件类型componentsData中,选择最匹配需求的一种。\\n3. 结合用户提供的数据结构,生成一份完整、规范、可运行的 ECharts 配置项(JSON格式)。\\n4. 非echart图表,参考componentsData组件配置,生成一份完整、规范、的配置项即可(JSON格式)。\\n5. 结合用户需求生成一个不超过15字的标题,并设置到返回JSON的title字段上。\\n6. 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n8. 热力地图,要生成echart的\\\"visualMap\\\"属性\\n## 技能:\\n1. **需求解析能力**:能够准确理解用户对数据可视化的业务需求,并将其转化为技术实现目标。\\n2. **图表选型能力**:精通折线图、柱状图、饼图、地图、散点图等从多种图表类型的特点与应用场景,能做出最佳选择。\\n3. **ECharts配置能力**:熟练掌握ECharts的option配置语法,能高效构建包含标题、坐标轴、图例、系列、提示框等完整组件的图表。\\n4. **数据适配能力**:能够将提供的 `chartData` 数据,自行分型类型并结合需求,将数据结构正确地映射到所选图表的 `series.data` 中。\\n5. **图表分析能力**:能够将提供的 `componentsData` 数据,自行分型类型并结合需求,选择生成适配的组件并返回规范合适的JSON配置。\\n## 工作流:\\n1. **需求分析**:仔细阅读 `{userInput}`,判断用户希望展示数据的何种关系(趋势、比较、占比、分布、相关)。\\n2. **图表选型**:根据第一步的分析结论,从componentsData图表类型中锁定唯一最合适的类型。\\n3. 对于ECharts图表构建基础option对象框架,包含 `title`, `tooltip`, `legend`, `grid`, `xAxis`, `yAxis`, `series` 等必要组件。\\n4. 根据选定的图表类型,配置 `series` 中的 `type` 和关键属性(如折线图的 `smooth`,饼图的 `radius`)。\\n5. 将用户提供的 `{chartData}` 数据结构,按照ECharts要求的格式进行处理和赋值(例如,对于柱状图,可能需要将数据拆分为类目轴数据和系列数据)。\\n6. 应用通用的美化原则(如配色清晰、标签易读、布局合理),生成最终配置。\\n7. 输出格式化:将生成的完整option对象,以格式规范、缩进清晰的JSON字符串形式输出。\\n8. 返回JSON数据前,自行通过JSON.parse() 检查能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n## 输出格式:\\n你必须只能输出合法且可被 JSON.parse() 正确解析的 JSON数据。包含name,data,option,三个字段值,不要输出任何解释、注释或 JSON 以外的文字。\\n1.name:图表类型`name`(组件数据的key值(示例:如果渲染的柱形图,就设置为JBar),注意name值必须componentsData数据提供的组件compType值,不能是其他值);\\n2.api:上下文变量中提取出来的api,存在就赋值到输出接口的api中,不存在就设置为{API};\\n3.sql:上下文变量中提取出来的sql,存在就赋值到输出接口的sql中,不存在就设置为{SQL};\\n4.title:结合用户需求生成一个不超过15字的标题title,赋值到输出接口的title中;\\n5.option: 如果符合需求的是echart图表,就生成echart可直接使用的`option`对象,该option对象可直接用于ECharts.init().setOption()的配置项。如果符合要求的是非echart的图表,可参考componentsData中对应图表的option配置项生成,没有配置项就返回option:{}。不要包含其他的任何额外的解释、说明或markdown代码块标记。可以根据配置项中 echart:true来判断是否是echart图表\\n示例输出结构(以柱状图为例):\\n6.data: 如果用户需求提供了数据data,就将数据data设置到返回JSON的data字段上。\\n7.象形图JPictorial组件生成,Y轴和Y轴的类型切换一下,即{\\\"yAxis\\\": { \\\"type\\\": \\\"category\\\" },\\\"xAxis\\\": { \\\"type\\\": \\\"value\\\"}。\\n8. 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n示例输出结构(以柱状图为例):\\n[{\\nname:\\\"JBar\\\",\\noption:{\\n \\\"title\\\": { \\\"text\\\": \\\"示例标题\\\", \\\"left\\\": \\\"center\\\" },\\n \\\"tooltip\\\": {},\\n \\\"legend\\\": { \\\"data\\\": [\\\"示例图例\\\"] },\\n \\\"xAxis\\\": { \\\"type\\\": \\\"category\\\", \\\"data\\\": [\\\"衬衫\\\", \\\"羊毛衫\\\", \\\"雪纺衫\\\"] },\\n \\\"yAxis\\\": { \\\"type\\\": \\\"value\\\" },\\n \\\"series\\\": [ { \\\"name\\\": \\\"销量\\\", \\\"type\\\": \\\"bar\\\", \\\"data\\\": [5, 20, 36] } ]\\n },\\n api:{API},\\n sql:{SQL},\\n title:\\\"\\\",\\n data:[]\\n}]\\n## 限制:\\n- 必须严格从组件数据提供的componentsData中选择一种,不得自行创造或推荐其他图表类型。\\n- 生成的所有配置必须基于用户提供的 `{userInput}` 和可用的 `chartData`,不得虚构数据字段或结构。\\n- 输出必须为纯JSON格式,无需也无法在JSON中注释“这里是标题”等内容。配置的正确性由键值对本身保证。\\n- 遵循数据可视化最佳实践,避免误导性图表(如扭曲的比例尺、不恰当的图表类型)。\\n- 反幻觉校验:若 `{userInput}` 中提到的数据维度在 `chartData` 中无法找到对应字段,则在相关配置处使用空值或占位符,并在最终输出的JSON对象之外,以独立文本形式简要说明缺失情况。但首要输出仍是JSON配置本身。\\n- 伦理审查模块:若需求或数据涉及敏感信息(如个人身份信息),在配置中应对数据进行聚合或匿名化处理,避免直接暴露。\\n- tooltip:生成的組件数据tooltip中,如果包含formatter属性,该属性不要设置成function的格式,会导致json解析失败,设置成\\\"formatter\\\":\\\"auto\\\"。\\n- 返回JSON数据前,自行通过JSON.parse() 检查是否能够正常解析,不能解析,解析失败,就重新检查返回内容并优化,直到能被 JSON.parse() 正确解析的 JSON数据\\n- 組件的option数据内的各项参数的内容值,不允许使用function(){}这种格式。\\n- 严格按照示例输出结构返回,不要包含```json```等信息\\n- 最多生成10个仪表盘组件\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"name\\\": \\\"基础柱形图\\\",\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"堆叠柱形图\\\",\\n    \\\"compType\\\": \\\"JStackBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态柱形图\\\",\\n    \\\"compType\\\": \\\"JDynamicBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"胶囊图\\\",\\n    \\\"compType\\\": \\\"JCapsuleChart\\\",\\n    \\\"echart\\\": false\\n    \\\"chartData\\\": [\\n        {\\n          name: \'苹果\',\\n          value: 1000879,\\n          type: \'手机品牌\',\\n    }],\\n    \\\"option\\\": {\\n        showValue: false,\\n        unit: \'\',\\n        customColor: [],\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          show: true,\\n          textStyle: {\\n            color: \'#464646\',\\n            fontWeight: \'normal\',\\n          },\\n        },\\n      }\\n  },\\n  {\\n    \\\"name\\\": \\\"基础条形图\\\",\\n    \\\"compType\\\": \\\"JHorizontalBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"背景柱形图\\\",\\n    \\\"compType\\\": \\\"JBackgroundBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比柱形图\\\",\\n    \\\"compType\\\": \\\"JMultipleBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"正负条形图\\\",\\n    \\\"compType\\\": \\\"JNegativeBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"折柱图\\\",\\n    \\\"compType\\\": \\\"JMixLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"百分比条形图\\\",\\n    \\\"compType\\\": \\\"JPercentBar\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"基础饼图\\\",\\n    \\\"compType\\\": \\\"JPie\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"南丁格尔玫瑰图\\\",\\n    \\\"compType\\\": \\\"JRose\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"旋转饼图\\\",\\n    \\\"compType\\\": \\\"JRotatePie\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        grid: {\\n          show: false,\\n          bottom: 115,\\n        },\\n        title: {\\n          text: \'\',\\n          textAlign: \'left\',\\n          subtext: \'\',\\n          textStyle: {\\n            fontWeight: \'normal\',\\n          },\\n          show: true,\\n        },\\n        card: {\\n          title: \'\',\\n          extra: \'\',\\n          rightHref: \'\',\\n          size: \'default\',\\n        },\\n        tooltip: {\\n          trigger: \'item\',\\n        },\\n        legend: {\\n          orient: \'vertical\',\\n        },\\n        series: [\\n          {\\n            name: \'\',\\n            type: \'pie\',\\n            data: [],\\n            emphasis: {\\n              itemStyle: {\\n                shadowBlur: 10,\\n                shadowOffsetX: 0,\\n                shadowColor: \'rgba(0, 0, 0, 0.5)\',\\n              },\\n            },\\n          },\\n        ],\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"基础折线图\\\",\\n    \\\"compType\\\": \\\"JLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"平滑曲线图\\\",\\n    \\\"compType\\\": \\\"JSmoothLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"阶梯折线图\\\",\\n    \\\"compType\\\": \\\"JStepLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"面积图\\\",\\n    \\\"compType\\\": \\\"JArea\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"对比折线图\\\",\\n    \\\"compType\\\": \\\"JMultipleLine\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"双轴图\\\",\\n    \\\"compType\\\": \\\"DoubleLineBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础进度图\\\",\\n    \\\"compType\\\": \\\"JCustomProgress\\\",\\n    \\\"echart\\\": false,\\n     option: {\\n        barWidth: 19,\\n        padding: 12,\\n        progressColor: \'#76c7c0\',\\n        backgroundColor: \'#ffffff\',\\n        titleColor: \'#fff\',\\n        titleFontSize: 16,\\n        titlePosition: \'top\',\\n        valueColor: \'#fff\',\\n        valueFontSize: 16,\\n        valuePosition: \'middle\',\\n        valueXOffset: 0,\\n        valueYOffset: 0,\\n      },\\n  },\\n  {\\n    \\\"name\\\": \\\"进度图\\\",\\n    \\\"compType\\\": \\\"JProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"列表进度图\\\",\\n    \\\"compType\\\": \\\"JListProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"圆形进度图\\\",\\n    \\\"compType\\\": \\\"JRoundProgress\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"水波图\\\",\\n    \\\"compType\\\": \\\"JLiquid\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"象形柱图\\\",\\n    \\\"compType\\\": \\\"JPictorialBar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"象形图\\\",\\n    \\\"compType\\\": \\\"JPictorial\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"男女占比\\\",\\n    \\\"compType\\\": \\\"JGender\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"普通散点图\\\",\\n    \\\"compType\\\": \\\"JScatter\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"气泡图\\\",\\n    \\\"compType\\\": \\\"JBubble\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础仪表盘\\\",\\n    \\\"compType\\\": \\\"JGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色仪表盘\\\",\\n    \\\"compType\\\": \\\"JColorGauge\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"渐变仪表盘\\\",\\n    \\\"compType\\\": \\\"JAntvGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"半圆仪表盘\\\",\\n    \\\"compType\\\": \\\"JSemiGauge\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"普通漏斗图\\\",\\n    \\\"compType\\\": \\\"JFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"金字塔漏斗图\\\",\\n    \\\"compType\\\": \\\"JPyramidFunnel\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3D金字塔\\\",\\n    \\\"compType\\\": \\\"JPyramid3D\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"普通雷达图\\\",\\n    \\\"compType\\\": \\\"JRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"圆形雷达图\\\",\\n    \\\"compType\\\": \\\"JCircleRadar\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"饼状环形图\\\",\\n    \\\"compType\\\": \\\"JRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"多色环形图\\\",\\n    \\\"compType\\\": \\\"JBreakRing\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"基础环形图\\\",\\n    \\\"compType\\\": \\\"JRingProgress\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"动态环形图\\\",\\n    \\\"compType\\\": \\\"JActiveRing\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"玉珏图\\\",\\n    \\\"compType\\\": \\\"JRadialBar\\\",\\n    \\\"echart\\\": false\\n  },\\n    {\\n    \\\"name\\\": \\\"矩形图\\\",\\n    \\\"compType\\\": \\\"JRectangle\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"象限图\\\",\\n    \\\"compType\\\": \\\"JQuadrant\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"3D柱形图\\\",\\n    \\\"compType\\\": \\\"JBarGroup3d\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"3D分组柱形图\\\",\\n    \\\"compType\\\": \\\"JBar3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(横向)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(竖向+序号)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片滚动(高亮)\\\",\\n    \\\"compType\\\": \\\"JCardScroll\\\",\\n    \\\"echart\\\": false\\n  },\\n   {\\n    \\\"name\\\": \\\"统计概览(卡片模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false,\\n    \\\"index\\\": \\\"1\\\",\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"1\\\"\\n    }\\n  },\\n   {\\n    \\\"name\\\": \\\"统计概览(背景模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false,\\n    \\\"index\\\": \\\"2\\\",\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"2\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"统计概览(高亮模式)\\\",\\n    \\\"compType\\\": \\\"JStatsSummary\\\",\\n    \\\"echart\\\": false,\\n    \\\"index\\\": \\\"3\\\",\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"3\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"卡片轮播\\\",\\n    \\\"compType\\\": \\\"JCardCarousel\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"文本\\\",\\n    \\\"compType\\\": \\\"JText\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"翻牌器\\\",\\n    \\\"compType\\\": \\\"JCountTo\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"颜色块\\\",\\n    \\\"compType\\\": \\\"JColorBlock\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数值\\\",\\n    \\\"compType\\\": \\\"JNumber\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轨道环形文字\\\",\\n    \\\"compType\\\": \\\"JOrbitRing\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"字符云\\\",\\n    \\\"compType\\\": \\\"JWordCloud\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"图层字符云\\\",\\n    \\\"compType\\\": \\\"JImgWordCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"闪动字符云\\\",\\n    \\\"compType\\\": \\\"JFlashCloud\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轮播表\\\",\\n    \\\"compType\\\": \\\"JScrollBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"表格\\\",\\n    \\\"compType\\\": \\\"JScrollTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"发展历程\\\",\\n    \\\"compType\\\": \\\"JDevHistory\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据表格\\\",\\n    \\\"compType\\\": \\\"JCommonTable\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"数据列表\\\",\\n    \\\"compType\\\": \\\"JList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"排行榜\\\",\\n    \\\"compType\\\": \\\"JScrollRankingBoard\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"个性排名(前四)\\\",\\n    \\\"compType\\\": \\\"JFlashList\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"气泡排名(前五)\\\",\\n    \\\"compType\\\": \\\"JBubbleRank\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(单行)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false,\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"0\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(多行+序号)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false,\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"1\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"滚动列表(带表头)\\\",\\n    \\\"compType\\\": \\\"JScrollList\\\",\\n    \\\"echart\\\": false,\\n    \\\"option\\\":{\\n      \\\"index\\\": \\\"2\\\"\\n    }\\n  },\\n  {\\n    \\\"name\\\": \\\"区域地图\\\",\\n    \\\"compType\\\": \\\"JAreaMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"散点地图\\\",\\n    \\\"compType\\\": \\\"JBubbleMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"柱形地图\\\",\\n    \\\"compType\\\": \\\"JBarMap\\\",\\n    \\\"echart\\\": true\\n  },\\n   {\\n    \\\"name\\\": \\\"热力地图\\\",\\n    \\\"compType\\\": \\\"JHeatMap\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d柱形图\\\",\\n    \\\"compType\\\": \\\"JBar3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"3d分组柱形图\\\",\\n    \\\"compType\\\": \\\"JBarGroup3d\\\",\\n    \\\"echart\\\": true\\n  },\\n  {\\n    \\\"name\\\": \\\"日历\\\",\\n    \\\"compType\\\": \\\"JPermanentCalendar\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"高德地图\\\",\\n    \\\"compType\\\": \\\"JGaoDeMap\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"边框\\\",\\n    \\\"compType\\\": \\\"JDragBorder\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"装饰\\\",\\n    \\\"compType\\\": \\\"JDragDecoration\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"图片\\\",\\n    \\\"compType\\\": \\\"JImg\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"轮播图\\\",\\n    \\\"compType\\\": \\\"JCarousel\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"播放器\\\",\\n    \\\"compType\\\": \\\"JVideoPlay\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"日历\\\",\\n    \\\"compType\\\": \\\"JPermanentCalendar\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"RTMP播放器\\\",\\n    \\\"compType\\\": \\\"JVideoJs\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"选项卡\\\",\\n    \\\"compType\\\": \\\"JSelectRadio\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"导航切换\\\",\\n    \\\"compType\\\": \\\"JTabToggle\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"表单\\\",\\n    \\\"compType\\\": \\\"JForm\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"Iframe\\\",\\n    \\\"compType\\\": \\\"JIframe\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"表单\\\",\\n    \\\"compType\\\": \\\"JForm\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"按钮\\\",\\n    \\\"compType\\\": \\\"JRadioButton\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"富文本\\\",\\n    \\\"compType\\\": \\\"JDragEditor\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"自定义组件\\\",\\n    \\\"compType\\\": \\\"JCustomEchart\\\",\\n    \\\"echart\\\": false\\n  },\\n  {\\n    \\\"name\\\": \\\"通用组件\\\",\\n    \\\"compType\\\": \\\"JCommon\\\",\\n    \\\"echart\\\": false\\n  }\\n]\"},{\"role\":\"user\",\"content\":\"用户的问题: {{userInput}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userInput\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"267498945805422592\",\"type\":\"end\",\"x\":1630,\"y\":-36,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{res}}\",\"outputType\":\"default\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":114}}],\"edges\":[{\"id\":\"271609331975028736\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"267492142677889024\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"267492142677889024_input\",\"pointsList\":[{\"x\":795,\"y\":-56},{\"x\":895,\"y\":-56},{\"x\":872,\"y\":-59},{\"x\":972,\"y\":-59}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274786344761540608\",\"type\":\"base-edge\",\"sourceNodeId\":\"267492142677889024\",\"targetNodeId\":\"267498945805422592\",\"sourceAnchorId\":\"267492142677889024_output\",\"targetAnchorId\":\"267498945805422592_input\",\"pointsList\":[{\"x\":1304,\"y\":-59},{\"x\":1404,\"y\":-59},{\"x\":1364,\"y\":-62},{\"x\":1464,\"y\":-62}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"res\",\"nodeId\":\"267492142677889024\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2004398098378108929'; +UPDATE `airag_flow` SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n llm.tag(\'269048862299471872\'),\n end.tag(\'269049045129183232\')\n).tag(\"start-node\")', `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":300,\"y\":437,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\"},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\"}},\"month\":{\"mode\":\"every\"}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"269048862299471872\",\"type\":\"llm\",\"x\":789,\"y\":471,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":3,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色:ECharts和大屏图表配置修改专家\\n你是一位专注于ECharts和大屏图表图表配置修改的专家,能够根据用户需求,精准、高效地修改现有ECharts和大屏图表配置项,并返回完整的、可直接使用的修改后配置对象。\\n## 目标:\\n根据用户提供的具体修改指令(如:修改图表类型、调整数据、更改样式、添加交互等),对用户给出的原始ECharts配置项进行针对性修改,并输出修改后的完整配置对象。\\n## 技能:\\n1. 精通ECharts所有版本的配置项语法、结构及参数含义。\\n2. 能够准确理解用户对图表样式、数据、交互行为的修改意图。\\n3. 具备强大的代码编辑与重构能力,确保修改后的配置项语法正确、结构清晰、无冗余代码。\\n4. 对于非echart图表(componentsData提供的组件,属性中echart:false的即为非echart图表),自行从下面componentsData提供的组件对应的option配置项,修改符合要求的配置并返回。\\n## 工作流:\\n1. **接收与分析**:接收用户提供的原始ECharts配置对象(通常以JSON或JavaScript对象形式)以及具体的修改要求。仔细分析原始配置的结构和用户的修改点。\\n2. **精准修改**:严格依据用户指令,对原始配置对象进行最小化、精准化的修改。确保只改动指定部分,保持其他未提及配置的完整性。对于模糊指令,会基于ECharts最佳实践进行合理推断和实现。\\n3. **校验与格式化**:检查修改后的配置对象语法是否正确,是否符合ECharts规范。将最终配置对象以格式清晰、缩进规范的JSON或JavaScript对象形式呈现。\\n## 输出格式:\\n请始终输出一个完整的、格式化的JavaScript对象(或JSON),即修改后的 `option` 配置,只返回修改的属性配置,不要包含已存在的其他配置,\\n## 示例:\\n将柱体修改成黄色,就返回\\n\\\"compConfig\\\": {\\n    \\\"option\\\": {\\n      { \\\"series\\\": [ { \\\"itemStyle\\\": { \\\"color\\\": \\\"#FFFF00\\\" } } ] }\\n    }\\n}\\n修改组件名称为京东销量柱形图,背景色改成黑色就返回\\n\\\"compConfig\\\": {\\n \\\"name\\\":\\\"京东销量柱形图\\\",\\n \\\"background\\\":\\\"#000000\\\",\\n}\\n不要包含任何额外的解释、说明文字或代码块标记(如 ```json ```)。输出应直接以 `{` 开始,以 `}` 结束。\\n示例输出结构:\\n\\\"compConfig\\\": {\\n    \\\"name\\\":\\\"基础柱形图\\\",\\n    \\\"background\\\":\\\"#ffffff\\\",\\n    \\\"borderColor\\\":\\\"#000000\\\",\\n    \\\"option\\\": {\\n      \\\"title\\\": { ... },\\n      \\\"tooltip\\\": { ... },\\n      \\\"xAxis\\\": { ... },\\n      \\\"yAxis\\\": { ... },\\n      \\\"series\\\": [ ... ]\\n    }\\n}\\n## 限制:\\n- 仅对用户提供的原始配置进行修改,不凭空创建全新的图表配置。\\n- 严格按照输出结构:{\\\"compConfig\\\": {****对应的配置项****}}返回\\n- 输出必须仅为修改后的配置对象本身,不附带任何分析过程、修改日志或使用建议。\\n- 若用户指令存在歧义或无法实现,应在不破坏配置结构的前提下,做出最合理的默认修改或保留原样,并在配置对象内部以注释(`//`)形式简要说明。\\n- 严格遵守ECharts官方配置规范,不使用已废弃或实验性参数(除非用户明确要求)。\\n- 颜色类型的修改,要以具体色值设置,不要使用英文单词,例如黑色,使用#000000,不要使用black\\n- 修改的option属性,以componentsData中具体组件的option配置为主,结合echart选择符合要求的配置项修改\\n- [\'JRadioButton\', \'JRadialBar\', \'JActiveRing\', \'JRing\', \'JPyramidFunnel\', \'JFunnel\', \'JBubble\', \'DoubleLineBar\', \'JMultipleLine\', \'JArea\', \'JLine\', \'JRotatePie\', \'JRose\', \'JPie\', \'JMixLineBar\', \'JPercentBar\', \'JMultipleBar\', \'JCapsuleChart\', \'JStackBar\', \'JQuadrant\'] 这些组件的相关颜色属性修改,按照 \\\"customColor\\\":[{color1:\'#FF0000\',color:\'#FF0000\'},{color1:\'#00FF00\',color:\'#00FF00\'}] 的格式修改; - 组件不包含customColor属性的颜色属性修改,按照对应组件配置的属性value数值去修改\\n- 柱体颜色属性修改使用 option.series[${index}].itemStyle.color,[\'JDynamicBar\']这些组件的相关颜色属性修改,按照option.series[${index}].itemStyle.color方式修改\\n- 配置项粗细的修改参数包含 [{ label: \'默认\', value: \'normal\' } { label: \'粗体\', value: \'bold\' } { label: \'细体\', value: \'lighter\' }]\\n- YAxisOption的`option.yAxis.yUnit`单位设置的不是option里面的label的内容时(例如:元),就将`option.yAxis.yUnit`值设置成\'CUSTOM\',并同步将`option.yAxis.yCustomUnit`属性的值设置成对应的单位数据(例如:元)\\n- 若用户修改名称或者背景色或者边框的属性,以componentsData中第一个柱形图配置为例,去修改返回对应配置即可\\n -名称:对应 compConfig.name\\n -背景色:对应 compConfig.background\\n -边框色:对应 compConfig.borderColor\\n## 组件数据:\\ncomponentsData:[\\n  {\\n    \\\"echart\\\":true ,\\n    \\\"compType\\\": \\\"JBar\\\",\\n    \\\"compConfig\\\": {\\n      \\\"name\\\":\\\"基础柱形图\\\",\\n      \\\"background\\\":\\\"#ffffff\\\",\\n      \\\"borderColor\\\":\\\"#000000\\\"\\n    }\\n  }]\\n组件配置说明\\n compOptionData = [{name: \'基础配置\', optionName: \'BasicOption\', children: [{\\\"label\\\": \\\"图层名称修改成\\\", \\\"value\\\": \\\"name\\\"}, {\\\"label\\\": \\\"图层背景色设置成\\\", \\\"value\\\": \\\"background\\\"}, {\\\"label\\\": \\\"图层边框线设置成\\\", \\\"value\\\": \\\"borderColor\\\"}, {\\\"label\\\": \\\"提示语设置为隐藏\\\", \\\"value\\\": \\\"option.tooltip.show\\\"}, {\\\"label\\\": \\\"提示语字体大小设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"提示语字体颜色设置成\\\", \\\"value\\\": \\\"option.tooltip.textStyle.fontSize\\\"}, ] },{name: \'标题设置\', optionName: \'TitleOption\', children: [{\\\"label\\\": \\\"标题名称修改成\\\", \\\"value\\\": \\\"option.title.text\\\"}, {\\\"label\\\": \\\"标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontColor\\\"}, {\\\"label\\\": \\\"标题字体粗细设置成\\\", \\\"value\\\": \\\"option.title.textStyle.fontWeight\\\"}, {\\\"label\\\": \\\"副标题名称修改成\\\", \\\"value\\\": \\\"option.title.subtextStyle\\\"}, {\\\"label\\\": \\\"副标题字体大小设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontSize\\\"}, {\\\"label\\\": \\\"副标题字体颜色设置成\\\", \\\"value\\\": \\\"option.title.subtextStyle.fontColor\\\"}, {\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"option.title.left\\\"}, {\\\"label\\\": \\\"垂直居中\\\", \\\"value\\\": \\\"option.title.top\\\"}, ] } ,{name: \'X轴设置\', optionName: \'XAxisOption\', children: [{\\\"label\\\": \\\"X轴名称修改成\\\", \\\"value\\\": \\\"option.xAxis.name\\\"}, {\\\"label\\\": \\\"X轴名称颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.color\\\"}, {\\\"label\\\": \\\"X轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.xAxis.nameTextStyle.fontSize\\\"}, {\\\"label\\\": \\\"X轴标签颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.color\\\"}, {\\\"label\\\": \\\"X轴标签角度\\\", \\\"value\\\": \\\"option.xAxis.axisLabel.rotate\\\"}, {\\\"label\\\": \\\"X轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.axisLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"X轴轴类型修改成\\\", \\\"value\\\": \\\"option.xAxis.type\\\"}, {\\\"label\\\": \\\"X轴显示网格线\\\", \\\"value\\\": \\\"option.xAxis.splitLine.show\\\"}, {\\\"label\\\": \\\"X轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.xAxis.splitLine.lineStyle.color\\\"}, ] } ,{name: \'胶囊图设置\', optionName: \'CapsuleChartOption\', children: [{\\\"label\\\": \\\"胶囊图显示数值\\\", \\\"value\\\": \\\"option.showValue\\\"}, {\\\"label\\\": \\\"胶囊图X轴名称设置成\\\", \\\"value\\\": \\\"option.unit\\\"} ] } ,{name: \'Y轴设置\', optionName: \'YAxisOption\', children: [{\\\"label\\\": \\\"Y轴名称修改成\\\", \\\"value\\\": \\\"option.yAxis.name\\\"}, {\\\"label\\\": \\\"Y轴名称颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.color\\\"}, {\\\"label\\\": \\\"Y轴名称字体大小修改成\\\", \\\"value\\\": \\\"option.yAxis.nameTextStyle.fontSize\\\"}, {\\\"label\\\": \\\"Y轴标签颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"}, {\\\"label\\\": \\\"Y轴标签角度\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.rotate\\\"}, {\\\"label\\\": \\\"Y轴轴线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.axisLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"Y轴轴类型修改成\\\", \\\"value\\\": \\\"option.yAxis.type\\\"}, {\\\"label\\\": \\\"Y轴显示网格线\\\", \\\"value\\\": \\\"option.yAxis.splitLine.show\\\"}, {\\\"label\\\": \\\"Y轴网格线颜色修改成\\\", \\\"value\\\": \\\"option.yAxis.splitLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"Y轴单位设置成\\\", \\\"value\\\": \\\"option.yAxis.yUnit\\\"}, ] } ,{name: \'图例设置\', optionName: \'LegendOption\', children: [{\\\"label\\\": \\\"图例字体大小设置成\\\", \\\"value\\\": \\\"option.legend.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"图例设置成横排\\\", \\\"value\\\": \\\"option.legend.orient\\\"}, {\\\"label\\\": \\\"图例上下边距设置\\\", \\\"value\\\": \\\"option.legend.t\\\"}, {\\\"label\\\": \\\"图例左右边距设置\\\", \\\"value\\\": \\\"option.legend.r\\\"}, ] } ,{name: \'样式设置\', optionName: \'PercentBarStyle\', children: [{\\\"label\\\": \\\"Y轴刻度颜色设置\\\", \\\"value\\\": \\\"option.yNameFontColor\\\"}, {\\\"label\\\": \\\"Y轴刻度字体大小设置\\\", \\\"value\\\": \\\"option.yNameFontSize\\\"}, {\\\"label\\\": \\\"X轴刻度颜色设置\\\", \\\"value\\\": \\\"option.xNameFontColor\\\"}, {\\\"label\\\": \\\"X轴刻度字体大小设置\\\", \\\"value\\\": \\\"option.xNameFontSize\\\"}, {\\\"label\\\": \\\"图例位置设置\\\", \\\"value\\\": \\\"option.legendPosition\\\", \\\"options\\\": [{\\\"label\\\": \\\"居上\\\", \\\"value\\\": \\\"top\\\"}, {\\\"label\\\": \\\"居下\\\", \\\"value\\\": \\\"bottom\\\"}]}, {\\\"label\\\": \\\"图例字体颜色设置\\\", \\\"value\\\": \\\"option.legendFontColor\\\"}, {\\\"label\\\": \\\"图例字体大小设置\\\", \\\"value\\\": \\\"option.legendFontSize\\\"}, ] } ,{name: \'自定义配色\', optionName: \'CustomColorOption\', children: [{\\\"label\\\": \\\"颜色设置成***色\\\", \\\"value\\\": \\\"option.customColor\\\"}, ] } ,{name: \'柱体设置\', optionName: \'BarCylinder\', children: [{\\\"label\\\": \\\"柱体宽度修改为\\\", \\\"value\\\": \\\"option.series[${index}].barWidth\\\"}, {\\\"label\\\": \\\"柱体圆角修改为\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.borderRadius\\\"}, {\\\"label\\\": \\\"柱体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].itemStyle.color\\\"}, {\\\"label\\\": \\\"柱体背景色显隐\\\", \\\"value\\\": \\\"option.series[${index}].showBackground\\\"}, {\\\"label\\\": \\\"柱体背景色颜色\\\", \\\"value\\\": \\\"option.series[${index}].backgroundStyle.color\\\"}, ] } ,{name: \'折线设置\', optionName: \'PolyglineOption\', children: [{\\\"label\\\": \\\"折线类型修改\\\", \\\"value\\\": \\\"option.series[${index}].lineType\\\",ignoreComp:[\'JArea\'],options: [{ label: \'折线\', value: \'line\' }, { label: \'曲线\', value: \'smooth\' }, { label: \'面积\', value: \'area\' }]}, {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.series[0].areaStyleOpacity\\\"}, {\\\"label\\\": \\\"线条宽度修改\\\", \\\"value\\\": \\\"option.series[${index}].lineWidth\\\"}, {\\\"label\\\": \\\"标记点修改\\\", \\\"value\\\": \\\"option.series[${index}].symbol\\\"}, {\\\"label\\\": \\\"点的大小修改\\\", \\\"value\\\": \\\"option.series[${index}].symbolSize\\\"}, ] } ,{name: \'饼图设置\', optionName: \'pieSettingOption\', children: [{\\\"label\\\": \\\"饼图设置成环形\\\", \\\"value\\\": \\\"option.isRadius\\\"}, {\\\"label\\\": \\\"饼图内环半径设置成\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"饼图外环半径设置成\\\", \\\"value\\\": \\\"option.outRadius\\\"}, {\\\"label\\\": \\\"饼图设置成南丁格尔玫瑰\\\", \\\"value\\\": \\\"option.isRose\\\"}, {\\\"label\\\": \\\"饼图标签显示位置\\\", \\\"value\\\": \\\"option.pieLabelPosition\\\"}, ] } ,{name: \'中心坐标\', optionName: \'gridPieOption\', children: [{\\\"label\\\": \\\"上下边距修改为\\\", \\\"value\\\": \\\"option.grid.top\\\"}, {\\\"label\\\": \\\"左右边距修改为\\\", \\\"value\\\": \\\"option.grid.left\\\"}, ] } ,{name: \'坐标轴边距\', optionName: \'GridOption\', children: [{\\\"label\\\": \\\"左边距修改成\\\", \\\"value\\\": \\\"option.grid.left\\\"}, {\\\"label\\\": \\\"顶边距\\\", \\\"value\\\": \\\"option.grid.top\\\"}, {\\\"label\\\": \\\"右边距\\\", \\\"value\\\": \\\"option.grid.right\\\"}, {\\\"label\\\": \\\"底边距\\\", \\\"value\\\": \\\"option.grid.bottom\\\"}, ] } ,{name: \'数值设置\', optionName: \'NumOption\', children: [{\\\"label\\\": \\\"显示数值\\\", \\\"value\\\": \\\"option.series[${index}].label.show\\\"}, {\\\"label\\\": \\\"数值显示位置在\\\", \\\"value\\\": \\\"option.series[${index}].label.position\\\"}, {\\\"label\\\": \\\"数值内容格式修改成\\\", \\\"value\\\": \\\"option.label.format\\\"}, {\\\"label\\\": \\\"数值字体颜色修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.color\\\"}, {\\\"label\\\": \\\"数值字体大小修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontSize\\\"}, {\\\"label\\\": \\\"数值字体粗细修改成\\\", \\\"value\\\": \\\"option.series[${index}].label.fontWeight\\\"}, {\\\"label\\\": \\\"数值单位配置显隐\\\", \\\"value\\\": \\\"option.showUnit.show\\\"}, {\\\"label\\\": \\\"数值单位数量级设置\\\", \\\"value\\\": \\\"option.showUnit.numberLevel\\\",option: simpNumberLevelOption}, {\\\"label\\\": \\\"数值单位保留小数\\\", \\\"value\\\": \\\"option.showUnit.decimal\\\"}, ] } ,{name: \'基础进度设置\', optionName: \'CustomProgressOption\', children: [{\\\"label\\\": \\\"进度目标颜色\\\", \\\"value\\\": \\\"option.backgroundColor\\\"}, {\\\"label\\\": \\\"进度颜色\\\", \\\"value\\\": \\\"option.progressColor\\\"}, {\\\"label\\\": \\\"进度条宽度\\\", \\\"value\\\": \\\"option.barWidth\\\"}, {\\\"label\\\": \\\"进度边距设置\\\", \\\"value\\\": \\\"option.padding\\\"}, {\\\"label\\\": \\\"进度标题颜色设置\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"进度标题字体大小设置\\\", \\\"value\\\": \\\"option.titleFontSize\\\"}, {\\\"label\\\": \\\"进度标题位置设置\\\", \\\"value\\\": \\\"option.titlePosition\\\"}, {\\\"label\\\": \\\"进度数值颜色设置\\\", \\\"value\\\": \\\"option.valueColor\\\"}, {\\\"label\\\": \\\"进度数值字体大小设置\\\", \\\"value\\\": \\\"option.valueFontSize\\\"}, {\\\"label\\\": \\\"进度数值位置设置\\\", \\\"value\\\": \\\"option.valuePosition\\\"}, {\\\"label\\\": \\\"进度数值横向偏移\\\", \\\"value\\\": \\\"option.valueXOffset\\\"}, ] } ,{name: \'列表进度图行样式\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"列表进度图行高度\\\", \\\"value\\\": \\\"option.row.height\\\"}, {\\\"label\\\": \\\"列表进度图行左边距\\\", \\\"value\\\": \\\"option.row.marginLeft\\\"}, {\\\"label\\\": \\\"列表进度图行上边距\\\", \\\"value\\\": \\\"option.row.marginTop\\\"}, {\\\"label\\\": \\\"列表进度图行右边距\\\", \\\"value\\\": \\\"option.row.marginRight\\\"}, ] } ,{name: \'列表进度图进度条配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"进度条底色设置\\\", \\\"value\\\": \\\"option.bar.background.color\\\"}, {\\\"label\\\": \\\"进度条底色启用渐变\\\", \\\"value\\\": \\\"option.bar.background.gradient.enabled\\\"}, {\\\"label\\\": \\\"进度条底色渐变方向设置\\\", \\\"value\\\": \\\"option.bar.background.gradient.direction\\\"}, {\\\"label\\\": \\\"进度条底色渐变起始颜色设置\\\", \\\"value\\\": \\\"option.bar.background.gradient.startColor\\\"}, {\\\"label\\\": \\\"进度条底色渐变结束颜色设置\\\", \\\"value\\\": \\\"option.bar.background.gradient.endColor\\\"}, {\\\"label\\\": \\\"进度条填充色设置\\\", \\\"value\\\": \\\"option.bar.fill.color\\\"}, {\\\"label\\\": \\\"进度条填充色启用渐变\\\", \\\"value\\\": \\\"option.bar.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"进度条填充色渐变方向设置\\\", \\\"value\\\": \\\"option.bar.fill.gradient.direction\\\"}, {\\\"label\\\": \\\"进度条填充色渐变起始颜色设置\\\", \\\"value\\\": \\\"option.bar.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"进度条填充色渐变结束颜色设置\\\", \\\"value\\\": \\\"option.bar.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"进度条高度设置\\\", \\\"value\\\": \\\"option.bar.height\\\"}, {\\\"label\\\": \\\"进度条圆角设置\\\", \\\"value\\\": \\\"option.bar.borderRadius\\\"}, {\\\"label\\\": \\\"进度指示点大小设置\\\", \\\"value\\\": \\\"option.bar.indicatorSize\\\"}, {\\\"label\\\": \\\"进度指示点颜色设置\\\", \\\"value\\\": \\\"option.bar.indicatorColor\\\"}, {\\\"label\\\": \\\"进度条显示边框\\\", \\\"value\\\": \\\"option.bar.border.enabled\\\"}, {\\\"label\\\": \\\"进度条边框颜色\\\", \\\"value\\\": \\\"option.bar.border.color\\\"}, {\\\"label\\\": \\\"进度条边框大小\\\", \\\"value\\\": \\\"option.bar.border.width\\\"}, {\\\"label\\\": \\\"进度条边框边距\\\", \\\"value\\\": \\\"option.bar.border.padding\\\"}, {\\\"label\\\": \\\"超出阈值配置启用\\\", \\\"value\\\": \\\"option.bar.exceed.enabled\\\"}, {\\\"label\\\": \\\"超出阈值百分比设置\\\", \\\"value\\\": \\\"option.bar.exceed.percent\\\"}, {\\\"label\\\": \\\"超出阈值填充色设置\\\", \\\"value\\\": \\\"option.bar.exceed.fill.color\\\"}, {\\\"label\\\": \\\"超出阈值填充色启用渐变\\\", \\\"value\\\": \\\"option.bar.exceed.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"超出阈值填充色渐变起始颜色\\\", \\\"value\\\": \\\"option.bar.exceed.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"超出阈值填充色渐变结束颜色\\\", \\\"value\\\": \\\"option.bar.exceed.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"超出阈值指示点颜色设置\\\", \\\"value\\\": \\\"option.bar.exceed.indicatorColor\\\"}, ] } ,{name: \'列表进度图数据映射\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"进度字段设置\\\", \\\"value\\\": \\\"option.bar.valueField\\\"}, {\\\"label\\\": \\\"总数类型设置\\\", \\\"value\\\": \\\"option.bar.total.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"来自字段\\\", \\\"value\\\": \\\"field\\\"}, {\\\"label\\\": \\\"固定值\\\", \\\"value\\\": \\\"fixed\\\"}]}, {\\\"label\\\": \\\"总数字段设置\\\", \\\"value\\\": \\\"option.bar.total.field\\\"}, {\\\"label\\\": \\\"固定总数设置\\\", \\\"value\\\": \\\"option.bar.total.val\\\"}, ] } ,{name: \'列表进度图左侧配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"左侧宽度设置\\\", \\\"value\\\": \\\"option.beginInfo.width\\\"}, {\\\"label\\\": \\\"左侧排列方式设置\\\", \\\"value\\\": \\\"option.beginInfo.layout\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"上下排列\\\", \\\"value\\\": \\\"vertical\\\"}]}, ] } ,{name: \'列表进度图中间配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"中间左边距设置\\\", \\\"value\\\": \\\"option.progressSection.marginLeft\\\"}, {\\\"label\\\": \\\"中间右边距设置\\\", \\\"value\\\": \\\"option.progressSection.marginRight\\\"}, {\\\"label\\\": \\\"中间排列方式设置\\\", \\\"value\\\": \\\"option.centerTopInfo.layout\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"上下排列\\\", \\\"value\\\": \\\"vertical\\\"}]}, ] } ,{name: \'列表进度图右侧配置\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"右侧宽度设置\\\", \\\"value\\\": \\\"option.endInfo.width\\\"}, {\\\"label\\\": \\\"右侧排列方式设置\\\", \\\"value\\\": \\\"option.endInfo.layout\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"上下排列\\\", \\\"value\\\": \\\"vertical\\\"}]}, ] } ,{name: \'列表进度图滚动动画\', optionName: \'ListProgressOption\', children: [{\\\"label\\\": \\\"启用滚动\\\", \\\"value\\\": \\\"option.scroll.enabled\\\"}, {\\\"label\\\": \\\"滚动方向设置\\\", \\\"value\\\": \\\"option.scroll.direction\\\", \\\"options\\\": [{\\\"label\\\": \\\"向上滚动\\\", \\\"value\\\": \\\"up\\\"}, {\\\"label\\\": \\\"向下滚动\\\", \\\"value\\\": \\\"down\\\"}]}, {\\\"label\\\": \\\"滚动间隔时间设置\\\", \\\"value\\\": \\\"option.scroll.interval\\\"}, {\\\"label\\\": \\\"滚动数量设置\\\", \\\"value\\\": \\\"option.scroll.count\\\"}, {\\\"label\\\": \\\"滚动动画时长设置\\\", \\\"value\\\": \\\"option.scroll.duration\\\"}, ] } ,{name: \'圆形进度图文本配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"标题字体大小设置\\\", \\\"value\\\": \\\"option.titleStyle.fontSize\\\"}, {\\\"label\\\": \\\"标题字体颜色设置\\\", \\\"value\\\": \\\"option.titleStyle.fontColor\\\"}, {\\\"label\\\": \\\"标题字体粗细设置\\\", \\\"value\\\": \\\"option.titleStyle.fontWeight\\\"}, {\\\"label\\\": \\\"标题字体样式设置\\\", \\\"value\\\": \\\"option.titleStyle.fontStyle\\\"}, {\\\"label\\\": \\\"标题字间距设置\\\", \\\"value\\\": \\\"option.titleStyle.letterSpacing\\\"}, {\\\"label\\\": \\\"标题字体设置\\\", \\\"value\\\": \\\"option.titleStyle.fontFamily\\\"}, {\\\"label\\\": \\\"标题启用渐变\\\", \\\"value\\\": \\\"option.titleStyle.fontGradient.enabled\\\"}, {\\\"label\\\": \\\"标题渐变起始颜色设置\\\", \\\"value\\\": \\\"option.titleStyle.fontGradient.startColor\\\"}, {\\\"label\\\": \\\"标题渐变结束颜色设置\\\", \\\"value\\\": \\\"option.titleStyle.fontGradient.endColor\\\"}, {\\\"label\\\": \\\"标题垂直位置设置\\\", \\\"value\\\": \\\"option.titleStyle.top\\\"}, ] } ,{name: \'圆形进度图数据配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"数据字体大小设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontSize\\\"}, {\\\"label\\\": \\\"数据字体颜色设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontColor\\\"}, {\\\"label\\\": \\\"数据字体粗细设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontWeight\\\"}, {\\\"label\\\": \\\"数据字体样式设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontStyle\\\"}, {\\\"label\\\": \\\"数据字间距设置\\\", \\\"value\\\": \\\"option.subTitleStyle.letterSpacing\\\"}, {\\\"label\\\": \\\"数据字体设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontFamily\\\"}, {\\\"label\\\": \\\"数据启用渐变\\\", \\\"value\\\": \\\"option.subTitleStyle.fontGradient.enabled\\\"}, {\\\"label\\\": \\\"数据渐变起始颜色设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontGradient.startColor\\\"}, {\\\"label\\\": \\\"数据渐变结束颜色设置\\\", \\\"value\\\": \\\"option.subTitleStyle.fontGradient.endColor\\\"}, {\\\"label\\\": \\\"数据垂直位置设置\\\", \\\"value\\\": \\\"option.subTitleStyle.top\\\"}, ] } ,{name: \'圆形进度图进度条配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"进度条外半径设置\\\", \\\"value\\\": \\\"option.polar.innerRadius\\\"}, {\\\"label\\\": \\\"进度条内半径设置\\\", \\\"value\\\": \\\"option.polar.outerRadius\\\"}, {\\\"label\\\": \\\"进度条背景色设置\\\", \\\"value\\\": \\\"option.backgroundStyle.color\\\"}, {\\\"label\\\": \\\"进度条启用渐变\\\", \\\"value\\\": \\\"option.progressGradient.enabled\\\"}, {\\\"label\\\": \\\"进度条渐变起始颜色设置\\\", \\\"value\\\": \\\"option.progressGradient.startColor\\\"}, {\\\"label\\\": \\\"进度条渐变结束颜色设置\\\", \\\"value\\\": \\\"option.progressGradient.endColor\\\"}, ] } ,{name: \'圆形进度图外圆配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"外圆半径设置\\\", \\\"value\\\": \\\"option.outerCircle.radius\\\"}, {\\\"label\\\": \\\"外圆边框颜色设置\\\", \\\"value\\\": \\\"option.outerCircle.borderColor\\\"}, {\\\"label\\\": \\\"外圆边框大小设置\\\", \\\"value\\\": \\\"option.outerCircle.borderWidth\\\"}, ] } ,{name: \'圆形进度图内圆配置\', optionName: \'RoundProgressOption\', children: [{\\\"label\\\": \\\"内圆半径设置\\\", \\\"value\\\": \\\"option.innerCircle.radius\\\"}, {\\\"label\\\": \\\"内圆边框颜色设置\\\", \\\"value\\\": \\\"option.innerCircle.borderColor\\\"}, {\\\"label\\\": \\\"内圆边框大小设置\\\", \\\"value\\\": \\\"option.innerCircle.borderWidth\\\"}, ] } ,{name: \'水波图设置\', optionName: \'LiquidPlotOption\', children: [{\\\"label\\\": \\\"显示类型\\\", \\\"value\\\": \\\"option.liquidType\\\"}, {\\\"label\\\": \\\"波纹颜色\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"波纹个数\\\", \\\"value\\\": \\\"option.count\\\"}, {\\\"label\\\": \\\"波纹长度\\\", \\\"value\\\": \\\"option.length\\\"}, {\\\"label\\\": \\\"外框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"}, {\\\"label\\\": \\\"外框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"}, {\\\"label\\\": \\\"间距\\\", \\\"value\\\": \\\"option.distance\\\"}, {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.strokeOpacity\\\"}, {\\\"label\\\": \\\"文本颜色配置\\\", \\\"value\\\": \\\"option.textColor\\\"}, {\\\"label\\\": \\\"文本字体大小配置\\\", \\\"value\\\": \\\"option.textFontSize\\\"} ] } ,{name: \'象形图设置\', optionName: \'PictorialOption\', children: [{\\\"label\\\": \\\"象形图柱体颜色设置\\\", \\\"value\\\": \\\"option.barColor\\\"}, {\\\"label\\\": \\\"透明度设置\\\", \\\"value\\\": \\\"option.barOpacity\\\"}, {\\\"label\\\": \\\"间距设置\\\", \\\"value\\\": \\\"option.count\\\"} ] } ,{name: \'仪表盘设置\', optionName: \'GaugeOption\', children: [{\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.series[0].axisLabel.show\\\"}, {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.series[0].axisLabel.color\\\"}, {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.series[0].axisLabel.fontSize\\\"}, {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.series[0].axisTick.show\\\"}, {\\\"label\\\": \\\"刻度线长度\\\", \\\"value\\\": \\\"option.series[0].axisTick.length\\\"}, {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.series[0].axisTick.lineStyle.color\\\"}, {\\\"label\\\": \\\"显示分割线\\\", \\\"value\\\": \\\"option.series[0].splitLine.show\\\"}, {\\\"label\\\": \\\"分割线长度\\\", \\\"value\\\": \\\"option.series[0].splitLine.length\\\"}, {\\\"label\\\": \\\"分割线颜色\\\", \\\"value\\\": \\\"option.series[0].splitLine.lineStyle.color\\\"}, {\\\"label\\\": \\\"指标字号\\\", \\\"value\\\": \\\"option.series[0].detail.fontSize\\\"}, ] } ,{name: \'渐变仪表盘设置\', optionName: \'AntvGaugeOption\', children: [{\\\"label\\\": \\\"仪表盘粗细设置\\\", \\\"value\\\": \\\"option.gaugeWidth\\\"}, {\\\"label\\\": \\\"显示刻度值\\\", \\\"value\\\": \\\"option.axisLabelShow\\\"}, {\\\"label\\\": \\\"刻度值颜色\\\", \\\"value\\\": \\\"option.axisLabelColor\\\"}, {\\\"label\\\": \\\"刻度值字体大小\\\", \\\"value\\\": \\\"option.axisLabelFontSize\\\"}, {\\\"label\\\": \\\"显示刻度线\\\", \\\"value\\\": \\\"option.axisTickShow\\\"}, {\\\"label\\\": \\\"刻度线颜色\\\", \\\"value\\\": \\\"option.lineColor\\\"}, {\\\"label\\\": \\\"文本颜色\\\", \\\"value\\\": \\\"option.valueColor\\\"}, {\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"}, {\\\"label\\\": \\\"指针颜色\\\", \\\"value\\\": \\\"option.indicatorColor\\\"}, {\\\"label\\\": \\\"指针粗细\\\", \\\"value\\\": \\\"option.indicatorLength\\\"}, ] } ,{name: \'尺寸设置\', optionName: \'Pyramid3DOption\', children: [{\\\"label\\\": \\\"缩放\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"尺寸\\\", \\\"value\\\": \\\"option.size\\\"} ] } ,{name: \'环形设置\', optionName: \'RingOption\', children: [{\\\"label\\\": \\\"内半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"外半径\\\", \\\"value\\\": \\\"option.outRadius\\\"} ] } ,{name: \'环形图设置\', optionName: \'ActiveRingPlotOption\', children: [{\\\"label\\\": \\\"环形图颜色设置\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"环形图背景色设置\\\", \\\"value\\\": \\\"option.bgColor\\\"}, {\\\"label\\\": \\\"环形图外环半径\\\", \\\"value\\\": \\\"option.outRadius\\\"}, {\\\"label\\\": \\\"环形图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"环形图标题字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"环形图标题字体颜色\\\", \\\"value\\\": \\\"option.fontColor\\\"}, {\\\"label\\\": \\\"环形图标题字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"}, {\\\"label\\\": \\\"环形图数值字体大小\\\", \\\"value\\\": \\\"option.valueFontSize\\\"}, {\\\"label\\\": \\\"环形图数值字体颜色\\\", \\\"value\\\": \\\"option.valueFontColor\\\"}, {\\\"label\\\": \\\"环形图数值字体粗细\\\", \\\"value\\\": \\\"option.valueFontWeight\\\"}, ] } ,{name: \'动态环形图设置\', optionName: \'ActiveRingOption\', children: [{\\\"label\\\": \\\"动态环形图显示原始值\\\", \\\"value\\\": \\\"option.showOriginValue\\\"}, {\\\"label\\\": \\\"动态环形图文字颜色\\\", \\\"value\\\": \\\"option.textColor\\\"}, {\\\"label\\\": \\\"动态环形图文字大小\\\", \\\"value\\\": \\\"option.textFontSize\\\"}, {\\\"label\\\": \\\"动态环形图线条宽度\\\", \\\"value\\\": \\\"option.lineWidth\\\"}, {\\\"label\\\": \\\"动态环形图环半径\\\", \\\"value\\\": \\\"option.radius\\\"}, {\\\"label\\\": \\\"动态环形图动态环半径\\\", \\\"value\\\": \\\"option.activeRadius\\\"}, ] } ,{name: \'玉珏设置\', optionName: \'RadialBarOption\', children: [{\\\"label\\\": \\\"玉珏图显示圆角\\\", \\\"value\\\": \\\"option.radiuShow\\\"}, {\\\"label\\\": \\\"玉珏图背景显示\\\", \\\"value\\\": \\\"option.bgShow\\\"}, {\\\"label\\\": \\\"玉珏图外环半径\\\", \\\"value\\\": \\\"option.radius\\\"}, {\\\"label\\\": \\\"玉珏图内环半径\\\", \\\"value\\\": \\\"option.innerRadius\\\"}, {\\\"label\\\": \\\"玉珏图最大旋转角\\\", \\\"value\\\": \\\"option.maxAngle\\\"}, ] } ,{name: \'矩形图设置\', optionName: \'RectangleOption\', children: [{\\\"label\\\": \\\"矩形图文本颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"矩形图文本字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"}, {\\\"label\\\": \\\"矩形图显示图例\\\", \\\"value\\\": \\\"option.showLegend\\\"}, ] } ,{name: \'文本设置\', optionName: \'TextOption\', children: [{\\\"label\\\": \\\"文本字体大小\\\", \\\"value\\\": \\\"option.body.fontSize\\\"}, {\\\"label\\\": \\\"文本字体间距\\\", \\\"value\\\": \\\"option.body.letterSpacing\\\"}, {\\\"label\\\": \\\"文本字体颜色\\\", \\\"value\\\": \\\"option.body.color\\\"}, {\\\"label\\\": \\\"文本启用千分符\\\", \\\"value\\\": \\\"option.body.thousandSeparator\\\"}, {\\\"label\\\": \\\"文本水平间距\\\", \\\"value\\\": \\\"option.body.marginLeft\\\"}, {\\\"label\\\": \\\"文本垂直间距\\\", \\\"value\\\": \\\"option.body.marginTop\\\"}, {\\\"label\\\": \\\"文本开启跑马灯\\\", \\\"value\\\": \\\"option.horseLamp\\\",ignoreComp: [\'JNumber\']}, {\\\"label\\\": \\\"文本开启超链接\\\", \\\"value\\\": \\\"option.isLink\\\",ignoreComp: [\'JNumber\']}, {\\\"label\\\": \\\"文本超链接地址\\\", \\\"value\\\": \\\"option.openUrl\\\",ignoreComp: [\'JNumber\']}, ] } ,{name: \'内部设置\', optionName: \'CountToTextOption\', children: [{\\\"label\\\": \\\"字体粗细设置\\\", \\\"value\\\": \\\"option.fontWeight\\\"}, {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.fontColor\\\"}, {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"前缀文本内容设置\\\", \\\"value\\\": \\\"option.prefix\\\"}, {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.prefixFontSize\\\"}, {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixColor\\\"}, {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"}, {\\\"label\\\": \\\"前缀字体对齐方式\\\", \\\"value\\\": \\\"option.prefixTextAlign\\\"}, {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixGridX\\\"}, {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixGridY\\\"}, {\\\"label\\\": \\\"后缀文本内容设置\\\", \\\"value\\\": \\\"option.suffix\\\"}, {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"}, {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixColor\\\"}, {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"}, {\\\"label\\\": \\\"后缀字体对齐方式\\\", \\\"value\\\": \\\"option.suffixTextAlign\\\"}, {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixGridX\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"}, {\\\"label\\\": \\\"后缀字体Y间距\\\", \\\"value\\\": \\\"option.suffixGridY\\\"}, ] } ,{name: \'颜色块设置\', optionName: \'ColorBlockOption\', children: [{\\\"label\\\": \\\"颜色块行数设置\\\", \\\"value\\\": \\\"option.lineNum\\\"}, {\\\"label\\\": \\\"颜色块边距设置\\\", \\\"value\\\": \\\"option.padding\\\"}, {\\\"label\\\": \\\"颜色块X间距设置\\\", \\\"value\\\": \\\"option.borderSplitx\\\"}, {\\\"label\\\": \\\"颜色块Y间距设置\\\", \\\"value\\\": \\\"option.borderSplity\\\"}, {\\\"label\\\": \\\"小数位数设置\\\", \\\"value\\\": \\\"option.decimals\\\"}, {\\\"label\\\": \\\"字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"字体颜色\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"字体粗细\\\", \\\"value\\\": \\\"option.fontWeight\\\"}, {\\\"label\\\": \\\"字体对齐方式\\\", \\\"value\\\": \\\"option.textAlign\\\"}, {\\\"label\\\": \\\"前缀字体大小\\\", \\\"value\\\": \\\"option.borderSplity\\\"}, {\\\"label\\\": \\\"前缀字体颜色\\\", \\\"value\\\": \\\"option.prefixColor\\\"}, {\\\"label\\\": \\\"前缀字体粗细\\\", \\\"value\\\": \\\"option.prefixFontWeight\\\"}, {\\\"label\\\": \\\"前缀字体X间距\\\", \\\"value\\\": \\\"option.prefixSplitx\\\"}, {\\\"label\\\": \\\"前缀字体Y间距\\\", \\\"value\\\": \\\"option.prefixSplity\\\"}, {\\\"label\\\": \\\"后缀字体大小\\\", \\\"value\\\": \\\"option.suffixFontSize\\\"}, {\\\"label\\\": \\\"后缀字体颜色\\\", \\\"value\\\": \\\"option.suffixColor\\\"}, {\\\"label\\\": \\\"后缀字体粗细\\\", \\\"value\\\": \\\"option.suffixFontWeight\\\"}, {\\\"label\\\": \\\"后缀字体X间距\\\", \\\"value\\\": \\\"option.suffixSplitx\\\"}, ] } ,{name: \'字体设置\', optionName: \'FlashCloudOption\', children: [{\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"字体大小设置\\\", \\\"value\\\": \\\"option.textSize\\\"}, {\\\"label\\\": \\\"字体颜色设置\\\", \\\"value\\\": \\\"option.textColor\\\"} ] } ,{name: \'字符云设置\', optionName: \'WordCloudOption\', children: [{\\\"label\\\": \\\"字体颜色配置\\\", \\\"value\\\": \\\"option.color\\\"}, {\\\"label\\\": \\\"字体间距设置\\\", \\\"value\\\": \\\"option.padding\\\"}, {\\\"label\\\": \\\"字体旋转设置\\\", \\\"value\\\": \\\"option.rotation\\\"}, {\\\"label\\\": \\\"字体最大值设置\\\", \\\"value\\\": \\\"option.minSize\\\"}, {\\\"label\\\": \\\"字体最小值设置\\\", \\\"value\\\": \\\"option.maxSize\\\"}, {\\\"label\\\": \\\"字体形状设置\\\", \\\"value\\\": \\\"option.series[0].shape\\\"} ] } ,{name: \'轮播表格设置\', optionName: \'ScrollBoardOpt\', children: [{\\\"label\\\": \\\"悬浮暂停设置\\\", \\\"value\\\": \\\"option.hoverPause\\\"}, {\\\"label\\\": \\\"等待时间设置\\\", \\\"value\\\": \\\"option.waitTime\\\"}, {\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.index\\\"}, {\\\"label\\\": \\\"表格列宽\\\", \\\"value\\\": \\\"option.indexWidth\\\"}, {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.headShow\\\"}, {\\\"label\\\": \\\"表头颜色\\\", \\\"value\\\": \\\"option.headerBGC\\\"}, {\\\"label\\\": \\\"表头行高\\\", \\\"value\\\": \\\"option.headerHeight\\\"}, {\\\"label\\\": \\\"每页行数\\\", \\\"value\\\": \\\"option.rowNum\\\"}, {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddRowBGC\\\"}, {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenRowBGC\\\"}, ] } ,{name: \'表格设置\', optionName: \'ScrollTableStyle\', children: [{\\\"label\\\": \\\"开启排名\\\", \\\"value\\\": \\\"option.ranking\\\"}, {\\\"label\\\": \\\"开启滚动\\\", \\\"value\\\": \\\"option.scroll\\\"}, {\\\"label\\\": \\\"滚动时间\\\", \\\"value\\\": \\\"option.scrollTime\\\"}, {\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.showHead\\\"}, {\\\"label\\\": \\\"表头背景颜色\\\", \\\"value\\\": \\\"option.headerBgColor\\\"}, {\\\"label\\\": \\\"表头字体颜色\\\", \\\"value\\\": \\\"option.headerFontColor\\\"}, {\\\"label\\\": \\\"表头字体大小\\\", \\\"value\\\": \\\"option.fontSize\\\"}, {\\\"label\\\": \\\"行高设置\\\", \\\"value\\\": \\\"option.lineHeight\\\"}, {\\\"label\\\": \\\"边框显示\\\", \\\"value\\\": \\\"option.showBorder\\\"}, {\\\"label\\\": \\\"边框宽度\\\", \\\"value\\\": \\\"option.borderWidth\\\"}, {\\\"label\\\": \\\"边框颜色\\\", \\\"value\\\": \\\"option.borderColor\\\"}, {\\\"label\\\": \\\"边框线类型\\\", \\\"value\\\": \\\"option.borderStyle\\\"}, {\\\"label\\\": \\\"表格字体颜色\\\", \\\"value\\\": \\\"option.bodyFontColor\\\"}, {\\\"label\\\": \\\"表格字体大小\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"}, {\\\"label\\\": \\\"奇行颜色\\\", \\\"value\\\": \\\"option.oddColor\\\"}, {\\\"label\\\": \\\"偶行颜色\\\", \\\"value\\\": \\\"option.evenColor\\\"}, ] } ,{name: \'历程设置\', optionName: \'DevHistoryOption\', children: [{\\\"label\\\": \\\"缩放设置\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"轮播间隔\\\", \\\"value\\\": \\\"option.waitTime\\\"}, {\\\"label\\\": \\\"历程背景色\\\", \\\"value\\\": \\\"option.typeBackColor\\\"}, {\\\"label\\\": \\\"历程字体颜色\\\", \\\"value\\\": \\\"option.typeFontColor\\\"}, {\\\"label\\\": \\\"内容字体颜色\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"内容字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"}, ] } ,{name: \'数据表格设置\', optionName: \'TableStyle\', children: [{\\\"label\\\": \\\"表头背景颜色\\\", \\\"value\\\": \\\"option.headerBgColor\\\"}, {\\\"label\\\": \\\"表头字体大小\\\", \\\"value\\\": \\\"option.headerFontSize\\\"}, {\\\"label\\\": \\\"表头字体颜色\\\", \\\"value\\\": \\\"option.headerColor\\\"}, {\\\"label\\\": \\\"表体内容字体颜色\\\", \\\"value\\\": \\\"option.bodyColor\\\"}, {\\\"label\\\": \\\"表体内容字体大小\\\", \\\"value\\\": \\\"option.bodyFontSize\\\"}, {\\\"label\\\": \\\"表体内容背景颜色\\\", \\\"value\\\": \\\"option.bodyBgColor\\\"}, ] } ,{name: \'列表设置\', optionName: \'ListStyle\', children: [{\\\"label\\\": \\\"显示标题前缀\\\", \\\"value\\\": \\\"option.showTitlePrefix\\\"}, {\\\"label\\\": \\\"显示时间前缀\\\", \\\"value\\\": \\\"option.showTimePrefix\\\"}, {\\\"label\\\": \\\"列表布局设置\\\", \\\"value\\\": \\\"option.layout\\\"}, {\\\"label\\\": \\\"标题字体颜色\\\", \\\"value\\\": \\\"option.titleFontColor\\\"}, {\\\"label\\\": \\\"标题字体粗细\\\", \\\"value\\\": \\\"option.titleFontWeight\\\"}, {\\\"label\\\": \\\"标题字体大小\\\", \\\"value\\\": \\\"option.titleFontSize\\\"}, {\\\"label\\\": \\\"内容图标颜色\\\", \\\"value\\\": \\\"option.iconColor\\\"}, {\\\"label\\\": \\\"内容颜色\\\", \\\"value\\\": \\\"option.contentColor\\\"}, {\\\"label\\\": \\\"开启动画设置\\\", \\\"value\\\": \\\"option.isEnableAnimation\\\"}, {\\\"label\\\": \\\"轮播时间(毫秒)设置\\\", \\\"value\\\": \\\"option.scrollTime\\\"}, ] } ,{name: \'滚动设置\', optionName: \'ScrollOption\', children: [{\\\"label\\\": \\\"是否排序\\\", \\\"value\\\": \\\"option.sort\\\"}, {\\\"label\\\": \\\"轮播方式设置单行\\\", \\\"value\\\": \\\"option.carousel\\\",\\\"options\\\": [{\\\"label\\\": \\\"单行\\\", \\\"value\\\": \\\"single\\\"}, {\\\"label\\\": \\\"整页\\\", \\\"value\\\": \\\"page\\\"},]}, {\\\"label\\\": \\\"显示行数\\\", \\\"value\\\": \\\"option.rowNum\\\"}, {\\\"label\\\": \\\"滚动时间(毫秒)设置\\\", \\\"value\\\": \\\"option.waitTime\\\"}, ] } ,{name: \'气泡排名设置\', optionName: \'BubbleRankingStyle\', children: [{\\\"label\\\": \\\"比例设置\\\", \\\"value\\\": \\\"option.zoom\\\"}, {\\\"label\\\": \\\"显示提示词\\\", \\\"value\\\": \\\"option.showTip\\\"}, {\\\"label\\\": \\\"提示词颜色设置为\\\", \\\"value\\\": \\\"option.titleColor\\\"}, {\\\"label\\\": \\\"提示词宽度设置为\\\", \\\"value\\\": \\\"option.tipWidth\\\"}, {\\\"label\\\": \\\"提示词内容颜色设置\\\", \\\"value\\\": \\\"option.tipFontColor\\\"}, {\\\"label\\\": \\\"提示词内容字体大小设置\\\", \\\"value\\\": \\\"option.tipFontSize\\\"} ] } ,{name: \'地图设置\', optionName: \'MapOption\', children: [{\\\"label\\\": \\\"显示区域名称\\\", \\\"value\\\": \\\"option.geo.label.normal.show\\\"}, {\\\"label\\\": \\\"区域名称颜色设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.color\\\"}, {\\\"label\\\": \\\"区域名称字体大小设置为\\\", \\\"value\\\": \\\"option.geo.label.normal.fontSize\\\"}, {\\\"label\\\": \\\"是否开启钻取\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"}, {\\\"label\\\": \\\"导航文字颜色设置\\\", \\\"value\\\": \\\"commonOption.breadcrumb.drillDown\\\"}, {\\\"label\\\": \\\"是否开启鼠标缩放\\\", \\\"value\\\": \\\"option.geo.roam\\\"}, {\\\"label\\\": \\\"缩放比例设置\\\", \\\"value\\\": \\\"option.geo.zoom\\\"}, {\\\"label\\\": \\\"地图长宽比设置\\\", \\\"value\\\": \\\"option.geo.aspectScale\\\"}, {\\\"label\\\": \\\"地图顶边距设置\\\", \\\"value\\\": \\\"option.geo.top\\\"}, {\\\"label\\\": \\\"地图左边距设置\\\", \\\"value\\\": \\\"option.geo.left\\\"}, ] } ,{name: \'地图配色设置\', optionName: \'LineMapColorOption\', children: [{\\\"label\\\": \\\"启用渐变色\\\", \\\"value\\\": \\\"commonOption.gradientColor\\\"}, {\\\"label\\\": \\\"中心颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"}, {\\\"label\\\": \\\"边缘颜色设置为\\\", \\\"value\\\": \\\"commonOption.areaColor.color2\\\"}, {\\\"label\\\": \\\"区域颜色设置\\\", \\\"value\\\": \\\"commonOption.areaColor.color1\\\"}, {\\\"label\\\": \\\"区域高亮颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.emphasis.areaColor\\\"}, {\\\"label\\\": \\\"区域边界颜色\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.borderColor\\\"}, {\\\"label\\\": \\\"阴影大小设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowBlur\\\"}, {\\\"label\\\": \\\"阴影水平偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetX\\\"}, {\\\"label\\\": \\\"阴影垂直偏移设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowOffsetY\\\"}, {\\\"label\\\": \\\"阴影颜色设置\\\", \\\"value\\\": \\\"option.geo.itemStyle.normal.shadowColor\\\"}, ] } ,{name: \'视觉映射设置\', optionName: \'VisualMapOptoin\', children: [{\\\"label\\\": \\\"开启视觉映射\\\", \\\"value\\\": \\\"option.visualMap.show\\\"}, {\\\"label\\\": \\\"视觉映射类型\\\", \\\"value\\\": \\\"option.visualMap.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"continuous\\\", \\\"value\\\": \\\"continuous\\\"}, {\\\"label\\\": \\\"piecewise\\\", \\\"value\\\": \\\"piecewise\\\"}]}, {\\\"label\\\": \\\"视觉映射文本颜色\\\", \\\"value\\\": \\\"option.visualMap.textStyle.color\\\"}, {\\\"label\\\": \\\"视觉映射文本粗细\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontWeight\\\"}, {\\\"label\\\": \\\"视觉映射文本字体大小设置\\\", \\\"value\\\": \\\"option.visualMap.textStyle.fontSize\\\"}, {\\\"label\\\": \\\"区域边界最小值\\\", \\\"value\\\": \\\"option.visualMap.min\\\"}, {\\\"label\\\": \\\"区域边界最大值\\\", \\\"value\\\": \\\"option.visualMap.max\\\"}, ] } ,{name: \'地图散点设置\', optionName: \'ScatterOption\', children: [{\\\"label\\\": \\\"地图散点大小设置\\\", \\\"value\\\": \\\"option.area.markerSize\\\"}, {\\\"label\\\": \\\"地图散点形状设置\\\", \\\"value\\\": \\\"option.area.markerShape\\\"}, {\\\"label\\\": \\\"地图散点类型设置\\\", \\\"value\\\": \\\"option.area.markerType\\\"}, {\\\"label\\\": \\\"地图散点颜色设置\\\", \\\"value\\\": \\\"option.area.markerColor\\\"}, {\\\"label\\\": \\\"地图散点文本显示\\\", \\\"value\\\": \\\"option.area.scatterLabelShow\\\"}, {\\\"label\\\": \\\"地图散点文本颜色设置\\\", \\\"value\\\": \\\"option.area.scatterLabelColor\\\"}, {\\\"label\\\": \\\"地图散点文本显示位置设置\\\", \\\"value\\\": \\\"option.area.scatterLabelPosition\\\"}, {\\\"label\\\": \\\"地图散点文本字体大小设置\\\", \\\"value\\\": \\\"option.area.scatterFontSize\\\"}, {\\\"label\\\": \\\"地图散点数量设置\\\", \\\"value\\\": \\\"option.area.markerCount\\\"}, {\\\"label\\\": \\\"地图散点透明度设置\\\", \\\"value\\\": \\\"option.area.markerOpacity\\\"}, ] } ,{name: \'热力地图设置\', optionName: \'HeatOption\', children: [{\\\"label\\\": \\\"热力点大小设置\\\", \\\"value\\\": \\\"commonOption.heat.pointSize\\\"}, {\\\"label\\\": \\\"模糊大小设置\\\", \\\"value\\\": \\\"commonOption.heat.blurSize\\\"}, {\\\"label\\\": \\\"最大透明度设置\\\", \\\"value\\\": \\\"commonOption.heat.maxOpacity\\\"}, ] } ,{name: \'柱体地图设置\', optionName: \'BarMapOption\', children: [{\\\"label\\\": \\\"柱体地图柱体大小设置\\\", \\\"value\\\": \\\"commonOption.barSize\\\"}, {\\\"label\\\": \\\"柱体左侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor\\\"}, {\\\"label\\\": \\\"柱体右侧颜色设置\\\", \\\"value\\\": \\\"commonOption.barColor2\\\"}, ] } ,{name: \'飞线地图设置\', optionName: \'FlyLineOption\', children: [{\\\"label\\\": \\\"飞线动画时间设置\\\", \\\"value\\\": \\\"commonOption.effect.period\\\"}, {\\\"label\\\": \\\"飞线标记形状设置\\\", \\\"value\\\": \\\"commonOption.effect.markerShape\\\"}, {\\\"label\\\": \\\"飞线标记大小设置\\\", \\\"value\\\": \\\"commonOption.effect.symbolSize\\\"}, {\\\"label\\\": \\\"飞线标记颜色设置\\\", \\\"value\\\": \\\"commonOption.effect.markerColor\\\"}, {\\\"label\\\": \\\"飞线特效尾迹长度设置\\\", \\\"value\\\": \\\"commonOption.effect.trailLength\\\"}, ] } ,{name: \'进度设置\', optionName: \'ProgressOption\', children: [{\\\"label\\\": \\\"显示进度标题\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.show\\\"}, {\\\"label\\\": \\\"进度标题字体颜色设置\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.color\\\"}, {\\\"label\\\": \\\"进度标题字体大小设置\\\", \\\"value\\\": \\\"option.yAxis.axisLabel.fontSize\\\"}, {\\\"label\\\": \\\"进度数值字体颜色设置\\\", \\\"value\\\": \\\"option.series[1].label.color\\\"}, {\\\"label\\\": \\\"进度数值字体大小设置\\\", \\\"value\\\": \\\"option.series[1].label.fontSize\\\"}, {\\\"label\\\": \\\"进度横向偏移设置\\\", \\\"value\\\": \\\"option.valueXOffset\\\"}, {\\\"label\\\": \\\"进度纵向偏移设置\\\", \\\"value\\\": \\\"option.valueYOffset\\\"}, {\\\"label\\\": \\\"进度柱体宽度设置\\\", \\\"value\\\": \\\"option.series[0].barWidth\\\"}, {\\\"label\\\": \\\"进度颜色设置\\\", \\\"value\\\": \\\"option.series[0].color\\\"}, {\\\"label\\\": \\\"进度目标颜色设置\\\", \\\"value\\\": \\\"option.series[1].color\\\"}, ] } ,{name: \'南丁格尔玫瑰设置\', optionName: \'RoseOption\', children: [{\\\"label\\\": \\\"边框宽度\\\", \\\"value\\\": \\\"option.series[0].itemStyle.borderWidth\\\"}, {\\\"label\\\": \\\"颜色透明度\\\", \\\"value\\\": \\\"option.series[0].itemStyle.colorOpacity\\\"}, ] } ,{name: \'统计概览基本设置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"布局填充类型设置\\\", \\\"value\\\": \\\"option.layout.fill.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"无\\\", \\\"value\\\": \\\"none\\\"}, {\\\"label\\\": \\\"颜色\\\", \\\"value\\\": \\\"color\\\"}, {\\\"label\\\": \\\"图片\\\", \\\"value\\\": \\\"image\\\"}]}, {\\\"label\\\": \\\"布局背景颜色设置\\\", \\\"value\\\": \\\"option.layout.fill.color\\\"}, {\\\"label\\\": \\\"布局启用渐变\\\", \\\"value\\\": \\\"option.layout.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"布局渐变方向设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.direction\\\"}, {\\\"label\\\": \\\"布局渐变起始颜色设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"布局渐变结束颜色设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"布局渐变角度设置\\\", \\\"value\\\": \\\"option.layout.fill.gradient.angle\\\"}, {\\\"label\\\": \\\"布局圆角设置\\\", \\\"value\\\": \\\"option.layout.borderRadius\\\"}, {\\\"label\\\": \\\"布局边框宽度设置\\\", \\\"value\\\": \\\"option.layout.borderWidth\\\"}, {\\\"label\\\": \\\"布局边框颜色设置\\\", \\\"value\\\": \\\"option.layout.borderColor\\\"}, {\\\"label\\\": \\\"布局阴影设置\\\", \\\"value\\\": \\\"option.layout.shadow\\\"}, {\\\"label\\\": \\\"布局水平对齐方式设置\\\", \\\"value\\\": \\\"option.layout.justify\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"flex-start\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"flex-end\\\"}, {\\\"label\\\": \\\"两端对齐\\\", \\\"value\\\": \\\"space-between\\\"}, {\\\"label\\\": \\\"两侧留白\\\", \\\"value\\\": \\\"space-around\\\"}]}, {\\\"label\\\": \\\"布局元素间距设置\\\", \\\"value\\\": \\\"option.layout.gap\\\"}, {\\\"label\\\": \\\"布局上内边距设置\\\", \\\"value\\\": \\\"option.layout.padding.top\\\"}, {\\\"label\\\": \\\"布局右内边距设置\\\", \\\"value\\\": \\\"option.layout.padding.right\\\"}, {\\\"label\\\": \\\"布局左内边距设置\\\", \\\"value\\\": \\\"option.layout.padding.left\\\"}, ] } ,{name: \'统计概览字段映射\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"标题字段设置\\\", \\\"value\\\": \\\"option.fieldMap.label\\\"}, {\\\"label\\\": \\\"数值字段设置\\\", \\\"value\\\": \\\"option.fieldMap.value\\\"}, {\\\"label\\\": \\\"单位字段设置\\\", \\\"value\\\": \\\"option.fieldMap.unit\\\"}, {\\\"label\\\": \\\"对比字段设置\\\", \\\"value\\\": \\\"option.fieldMap.compareValue\\\"}, {\\\"label\\\": \\\"标签字段设置\\\", \\\"value\\\": \\\"option.fieldMap.compareLabel\\\"}, {\\\"label\\\": \\\"状态字段设置\\\", \\\"value\\\": \\\"option.fieldMap.compareState\\\"}, {\\\"label\\\": \\\"上升值设置\\\", \\\"value\\\": \\\"option.fieldMap.positiveValue\\\"}, {\\\"label\\\": \\\"下降值设置\\\", \\\"value\\\": \\\"option.fieldMap.negativeValue\\\"}, ] } ,{name: \'统计概览卡片样式\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"卡片最小宽度设置\\\", \\\"value\\\": \\\"option.card.minWidth\\\"}, {\\\"label\\\": \\\"卡片填充类型设置\\\", \\\"value\\\": \\\"option.card.fill.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"无\\\", \\\"value\\\": \\\"none\\\"}, {\\\"label\\\": \\\"颜色\\\", \\\"value\\\": \\\"color\\\"}, {\\\"label\\\": \\\"图片\\\", \\\"value\\\": \\\"image\\\"}]}, {\\\"label\\\": \\\"卡片底色设置\\\", \\\"value\\\": \\\"option.card.fill.color\\\"}, {\\\"label\\\": \\\"卡片启用渐变\\\", \\\"value\\\": \\\"option.card.fill.gradient.enabled\\\"}, {\\\"label\\\": \\\"卡片渐变方向设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.direction\\\"}, {\\\"label\\\": \\\"卡片渐变起始颜色设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.startColor\\\"}, {\\\"label\\\": \\\"卡片渐变结束颜色设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.endColor\\\"}, {\\\"label\\\": \\\"卡片渐变角度设置\\\", \\\"value\\\": \\\"option.card.fill.gradient.angle\\\"}, {\\\"label\\\": \\\"卡片圆角设置\\\", \\\"value\\\": \\\"option.card.borderRadius\\\"}, {\\\"label\\\": \\\"卡片边框宽度设置\\\", \\\"value\\\": \\\"option.card.borderWidth\\\"}, {\\\"label\\\": \\\"卡片边框颜色设置\\\", \\\"value\\\": \\\"option.card.borderColor\\\"}, {\\\"label\\\": \\\"卡片垂直内边距设置\\\", \\\"value\\\": \\\"option.card.padding.vertical\\\"}, {\\\"label\\\": \\\"卡片水平内边距设置\\\", \\\"value\\\": \\\"option.card.padding.horizontal\\\"}, {\\\"label\\\": \\\"卡片阴影设置\\\", \\\"value\\\": \\\"option.card.shadow\\\"}, {\\\"label\\\": \\\"卡片模糊程度设置\\\", \\\"value\\\": \\\"option.card.blur\\\"}, ] } ,{name: \'统计概览上部配置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"上部显示\\\", \\\"value\\\": \\\"option.sections.top.show\\\"}, {\\\"label\\\": \\\"上部内容类型设置\\\", \\\"value\\\": \\\"option.sections.top.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"主数值\\\", \\\"value\\\": \\\"value\\\"}, {\\\"label\\\": \\\"同比\\\", \\\"value\\\": \\\"compare\\\"}, {\\\"label\\\": \\\"标题\\\", \\\"value\\\": \\\"label\\\"}]}, {\\\"label\\\": \\\"上部水平对齐设置\\\", \\\"value\\\": \\\"option.sections.top.align\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"right\\\"}]}, {\\\"label\\\": \\\"上部上内边距设置\\\", \\\"value\\\": \\\"option.sections.top.paddingTop\\\"}, {\\\"label\\\": \\\"上部下内边距设置\\\", \\\"value\\\": \\\"option.sections.top.paddingBottom\\\"}, {\\\"label\\\": \\\"上部最小高设置\\\", \\\"value\\\": \\\"option.sections.top.minHeight\\\"}, {\\\"label\\\": \\\"上部数值字体大小设置\\\", \\\"value\\\": \\\"option.sections.top.value.fontSize\\\"}, {\\\"label\\\": \\\"上部数值字体颜色设置\\\", \\\"value\\\": \\\"option.sections.top.value.fontColor\\\"}, {\\\"label\\\": \\\"上部数值字体粗细设置\\\", \\\"value\\\": \\\"option.sections.top.value.fontWeight\\\"}, {\\\"label\\\": \\\"上部单位间距设置\\\", \\\"value\\\": \\\"option.sections.top.value.unitGap\\\"}, {\\\"label\\\": \\\"上部单位字体大小设置\\\", \\\"value\\\": \\\"option.sections.top.value.unit.fontSize\\\"}, {\\\"label\\\": \\\"上部单位字体颜色设置\\\", \\\"value\\\": \\\"option.sections.top.value.unit.fontColor\\\"}, ] } ,{name: \'统计概览中部配置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"中部显示\\\", \\\"value\\\": \\\"option.sections.middle.show\\\"}, {\\\"label\\\": \\\"中部内容类型设置\\\", \\\"value\\\": \\\"option.sections.middle.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"主数值\\\", \\\"value\\\": \\\"value\\\"}, {\\\"label\\\": \\\"同比\\\", \\\"value\\\": \\\"compare\\\"}, {\\\"label\\\": \\\"标题\\\", \\\"value\\\": \\\"label\\\"}]}, {\\\"label\\\": \\\"中部水平对齐设置\\\", \\\"value\\\": \\\"option.sections.middle.align\\\"}, {\\\"label\\\": \\\"中部上内边距设置\\\", \\\"value\\\": \\\"option.sections.middle.paddingTop\\\"}, {\\\"label\\\": \\\"中部下内边距设置\\\", \\\"value\\\": \\\"option.sections.middle.paddingBottom\\\"}, {\\\"label\\\": \\\"中部最小高设置\\\", \\\"value\\\": \\\"option.sections.middle.minHeight\\\"}, {\\\"label\\\": \\\"中部垂直对齐设置\\\", \\\"value\\\": \\\"option.sections.middle.alignItems\\\"}, {\\\"label\\\": \\\"中部对比标签字体大小设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.labelStyle.fontSize\\\"}, {\\\"label\\\": \\\"中部对比标签字体颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.labelStyle.fontColor\\\"}, {\\\"label\\\": \\\"中部对比数值字体大小设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.fontSize\\\"}, {\\\"label\\\": \\\"中部对比数值字体颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.fontColor\\\"}, {\\\"label\\\": \\\"中部对比上涨颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.positiveColor\\\"}, {\\\"label\\\": \\\"中部对比下降颜色设置\\\", \\\"value\\\": \\\"option.sections.middle.compare.valueStyle.negativeColor\\\"}, ] } ,{name: \'统计概览下部配置\', optionName: \'StatsSummaryOption\', children: [{\\\"label\\\": \\\"下部显示\\\", \\\"value\\\": \\\"option.sections.bottom.show\\\"}, {\\\"label\\\": \\\"下部内容类型设置\\\", \\\"value\\\": \\\"option.sections.bottom.type\\\", \\\"options\\\": [{\\\"label\\\": \\\"主数值\\\", \\\"value\\\": \\\"value\\\"}, {\\\"label\\\": \\\"同比\\\", \\\"value\\\": \\\"compare\\\"}, {\\\"label\\\": \\\"标题\\\", \\\"value\\\": \\\"label\\\"}]}, {\\\"label\\\": \\\"下部水平对齐设置\\\", \\\"value\\\": \\\"option.sections.bottom.align\\\"}, {\\\"label\\\": \\\"下部上内边距设置\\\", \\\"value\\\": \\\"option.sections.bottom.paddingTop\\\"}, {\\\"label\\\": \\\"下部下内边距设置\\\", \\\"value\\\": \\\"option.sections.bottom.paddingBottom\\\"}, {\\\"label\\\": \\\"下部最小高设置\\\", \\\"value\\\": \\\"option.sections.bottom.minHeight\\\"}, {\\\"label\\\": \\\"下部标题字体大小设置\\\", \\\"value\\\": \\\"option.sections.bottom.label.fontSize\\\"}, {\\\"label\\\": \\\"下部标题字体颜色设置\\\", \\\"value\\\": \\\"option.sections.bottom.label.fontColor\\\"}, {\\\"label\\\": \\\"下部标题字体粗细设置\\\", \\\"value\\\": \\\"option.sections.bottom.label.fontWeight\\\"}, ] } ,{name: \'卡片滚动基础配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"排列方向设置\\\", \\\"value\\\": \\\"option.direction\\\", \\\"options\\\": [{\\\"label\\\": \\\"横向排列\\\", \\\"value\\\": \\\"horizontal\\\"}, {\\\"label\\\": \\\"竖向排列\\\", \\\"value\\\": \\\"vertical\\\"}]}, {\\\"label\\\": \\\"行间隙设置\\\", \\\"value\\\": \\\"option.rowGap\\\"}, {\\\"label\\\": \\\"列间隙设置\\\", \\\"value\\\": \\\"option.columnGap\\\"}, ] } ,{name: \'卡片滚动滚动配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"是否滚动设置\\\", \\\"value\\\": \\\"option.autoScrollEnabled\\\"}, {\\\"label\\\": \\\"滚动方向设置\\\", \\\"value\\\": \\\"option.scrollDirection\\\", \\\"options\\\": [{\\\"label\\\": \\\"向左滚动\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"向上滚动\\\", \\\"value\\\": \\\"up\\\"}]}, {\\\"label\\\": \\\"滚动个数设置\\\", \\\"value\\\": \\\"option.scrollCount\\\"}, {\\\"label\\\": \\\"停留时间设置\\\", \\\"value\\\": \\\"option.stayDuration\\\"}, {\\\"label\\\": \\\"动画时长设置\\\", \\\"value\\\": \\\"option.animationDuration\\\"}, ] } ,{name: \'卡片滚动卡片配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"卡片宽度设置\\\", \\\"value\\\": \\\"option.cardStyle.width\\\"}, {\\\"label\\\": \\\"卡片高度设置\\\", \\\"value\\\": \\\"option.cardStyle.height\\\"}, {\\\"label\\\": \\\"卡片背景色设置\\\", \\\"value\\\": \\\"option.cardStyle.backgroundColor\\\"}, {\\\"label\\\": \\\"卡片背景图片设置\\\", \\\"value\\\": \\\"option.cardStyle.backgroundImage\\\"}, {\\\"label\\\": \\\"卡片高亮图片设置\\\", \\\"value\\\": \\\"option.cardStyle.bgHighlightImage\\\"}, {\\\"label\\\": \\\"卡片圆角设置\\\", \\\"value\\\": \\\"option.cardStyle.borderRadius\\\"}, {\\\"label\\\": \\\"卡片边框显示\\\", \\\"value\\\": \\\"option.cardStyle.borderEnabled\\\"}, {\\\"label\\\": \\\"卡片边框颜色设置\\\", \\\"value\\\": \\\"option.cardStyle.borderColor\\\"}, {\\\"label\\\": \\\"卡片边框样式设置\\\", \\\"value\\\": \\\"option.cardStyle.borderStyle\\\", \\\"options\\\": [{\\\"label\\\": \\\"实线\\\", \\\"value\\\": \\\"solid\\\"}, {\\\"label\\\": \\\"虚线\\\", \\\"value\\\": \\\"dashed\\\"}, {\\\"label\\\": \\\"点线\\\", \\\"value\\\": \\\"dotted\\\"}, {\\\"label\\\": \\\"双线\\\", \\\"value\\\": \\\"double\\\"}]}, {\\\"label\\\": \\\"卡片边框宽度设置\\\", \\\"value\\\": \\\"option.cardStyle.borderWidth\\\"}, {\\\"label\\\": \\\"卡片上内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingTop\\\"}, {\\\"label\\\": \\\"卡片右内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingRight\\\"}, {\\\"label\\\": \\\"卡片下内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingBottom\\\"}, {\\\"label\\\": \\\"卡片左内边距设置\\\", \\\"value\\\": \\\"option.cardStyle.paddingLeft\\\"}, ] } ,{name: \'卡片滚动字段配置\', optionName: \'CardScrollOption\', children: [{\\\"label\\\": \\\"显示序号\\\", \\\"value\\\": \\\"option.showIndex\\\"}, {\\\"label\\\": \\\"字段排列方式设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.layoutDirection\\\", \\\"options\\\": [{\\\"label\\\": \\\"水平方向(左-右)\\\", \\\"value\\\": \\\"row\\\"}, {\\\"label\\\": \\\"水平方向(右-左)\\\", \\\"value\\\": \\\"row-reverse\\\"}, {\\\"label\\\": \\\"垂直方向(上-下)\\\", \\\"value\\\": \\\"column\\\"}, {\\\"label\\\": \\\"垂直方向(下-上)\\\", \\\"value\\\": \\\"column-reverse\\\"}]}, {\\\"label\\\": \\\"字段水平对齐设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.justifyContent\\\"}, {\\\"label\\\": \\\"字段垂直对齐设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.alignItems\\\"}, {\\\"label\\\": \\\"字段宽度设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.width\\\"}, {\\\"label\\\": \\\"字段高度设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.height\\\"}, {\\\"label\\\": \\\"字段上边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginTop\\\"}, {\\\"label\\\": \\\"字段下边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginBottom\\\"}, {\\\"label\\\": \\\"字段左边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginLeft\\\"}, {\\\"label\\\": \\\"字段右边距设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].itemConfig.marginRight\\\"}, {\\\"label\\\": \\\"字段省略显示\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].omitConfig.show\\\"}, {\\\"label\\\": \\\"字段省略行数设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].omitConfig.lines\\\"}, {\\\"label\\\": \\\"字段千分符显示\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].thousandSeparatorConfig.show\\\"}, {\\\"label\\\": \\\"字段显示标签\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].showLabel\\\"}, {\\\"label\\\": \\\"字段显示值\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].showValue\\\"}, {\\\"label\\\": \\\"字段值类型设置\\\", \\\"value\\\": \\\"option.contentFieldMapping[${index}].valueType\\\", \\\"options\\\": [{\\\"label\\\": \\\"非数组\\\", \\\"value\\\": \\\"non-array\\\"}, {\\\"label\\\": \\\"数组\\\", \\\"value\\\": \\\"array\\\"}]}, ] } ,{name: \'滚动列表基本配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"显示表头\\\", \\\"value\\\": \\\"option.showHeader\\\"}, {\\\"label\\\": \\\"每行数量设置\\\", \\\"value\\\": \\\"option.itemsPerRow\\\"}, {\\\"label\\\": \\\"列间距设置\\\", \\\"value\\\": \\\"option.gridGap\\\"}, ] } ,{name: \'滚动列表滚动配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"是否滚动设置\\\", \\\"value\\\": \\\"option.autoScrollEnabled\\\"}, {\\\"label\\\": \\\"滚动时长设置\\\", \\\"value\\\": \\\"option.autoScrollInterval\\\"}, ] } ,{name: \'滚动列表容器配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"容器背景色设置\\\", \\\"value\\\": \\\"option.backgroundColor\\\"}, {\\\"label\\\": \\\"容器圆角设置\\\", \\\"value\\\": \\\"option.borderRadius\\\"}, {\\\"label\\\": \\\"容器左边距设置\\\", \\\"value\\\": \\\"option.marginLeft\\\"}, {\\\"label\\\": \\\"容器右边距设置\\\", \\\"value\\\": \\\"option.marginRight\\\"}, ] } ,{name: \'滚动列表表头配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"表头高度设置\\\", \\\"value\\\": \\\"option.header.height\\\"}, {\\\"label\\\": \\\"表头背景色设置\\\", \\\"value\\\": \\\"option.header.backgroundColor\\\"}, {\\\"label\\\": \\\"表头字体大小设置\\\", \\\"value\\\": \\\"option.header.fontSize\\\"}, {\\\"label\\\": \\\"表头字体颜色设置\\\", \\\"value\\\": \\\"option.header.fontColor\\\"}, {\\\"label\\\": \\\"表头字体粗细设置\\\", \\\"value\\\": \\\"option.header.fontWeight\\\"}, {\\\"label\\\": \\\"表头字体样式设置\\\", \\\"value\\\": \\\"option.header.fontStyle\\\"}, {\\\"label\\\": \\\"表头字间距设置\\\", \\\"value\\\": \\\"option.header.letterSpacing\\\"}, {\\\"label\\\": \\\"表头字体设置\\\", \\\"value\\\": \\\"option.header.fontFamily\\\"}, {\\\"label\\\": \\\"表头启用渐变\\\", \\\"value\\\": \\\"option.header.fontGradient.enabled\\\"}, {\\\"label\\\": \\\"表头渐变起始颜色设置\\\", \\\"value\\\": \\\"option.header.fontGradient.startColor\\\"}, {\\\"label\\\": \\\"表头渐变结束颜色设置\\\", \\\"value\\\": \\\"option.header.fontGradient.endColor\\\"}, {\\\"label\\\": \\\"表头对齐方式设置\\\", \\\"value\\\": \\\"option.header.textAlign\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"right\\\"}]}, ] } ,{name: \'滚动列表行配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"文本多行展示\\\", \\\"value\\\": \\\"option.row.isMultiline\\\"}, {\\\"label\\\": \\\"行背景类型设置\\\", \\\"value\\\": \\\"option.row.backgroundType\\\", \\\"options\\\": [{\\\"label\\\": \\\"背景色\\\", \\\"value\\\": \\\"color\\\"}, {\\\"label\\\": \\\"背景图\\\", \\\"value\\\": \\\"image\\\"}]}, {\\\"label\\\": \\\"行背景色设置\\\", \\\"value\\\": \\\"option.row.backgroundColor\\\"}, {\\\"label\\\": \\\"交替行背景色设置\\\", \\\"value\\\": \\\"option.row.alternateBackgroundColor\\\"}, {\\\"label\\\": \\\"行背景图片设置\\\", \\\"value\\\": \\\"option.row.backgroundImg\\\"}, {\\\"label\\\": \\\"行高度设置\\\", \\\"value\\\": \\\"option.row.height\\\"}, {\\\"label\\\": \\\"行内边距设置\\\", \\\"value\\\": \\\"option.row.padding\\\"}, {\\\"label\\\": \\\"行上边距设置\\\", \\\"value\\\": \\\"option.row.marginTop\\\"}, {\\\"label\\\": \\\"行下边距设置\\\", \\\"value\\\": \\\"option.row.marginBottom\\\"}, {\\\"label\\\": \\\"行左边距设置\\\", \\\"value\\\": \\\"option.row.marginLeft\\\"}, {\\\"label\\\": \\\"行右边距设置\\\", \\\"value\\\": \\\"option.row.marginRight\\\"}, ] } ,{name: \'滚动列表字段配置\', optionName: \'ScrollListOption\', children: [{\\\"label\\\": \\\"显示序号\\\", \\\"value\\\": \\\"option.showIndex\\\"}, {\\\"label\\\": \\\"字段文本对齐设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].textAlign\\\", \\\"options\\\": [{\\\"label\\\": \\\"左对齐\\\", \\\"value\\\": \\\"left\\\"}, {\\\"label\\\": \\\"居中\\\", \\\"value\\\": \\\"center\\\"}, {\\\"label\\\": \\\"右对齐\\\", \\\"value\\\": \\\"right\\\"}]}, {\\\"label\\\": \\\"字段宽度设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].width\\\"}, {\\\"label\\\": \\\"字段图片宽度设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].imageStyle.width\\\"}, {\\\"label\\\": \\\"字段图片高度设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].imageStyle.height\\\"}, {\\\"label\\\": \\\"字段图片圆角设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].imageStyle.borderRadius\\\"}, {\\\"label\\\": \\\"字段左边距设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].marginLeft\\\"}, {\\\"label\\\": \\\"字段右边距设置\\\", \\\"value\\\": \\\"option.fieldMapping[${index}].marginRight\\\"}, ] } ]\\n\\n\"},{\"role\":\"user\",\"content\":\"用户的问题:{{userQuestion}}\"}],\"showToolExecution\":false},\"inputParams\":[{\"field\":\"content\",\"name\":\"userQuestion\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"269049045129183232\",\"type\":\"end\",\"x\":1272,\"y\":459,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{option}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}}],\"edges\":[{\"id\":\"269048862303666176\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"269048862299471872\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"269048862299471872_input\",\"pointsList\":[{\"x\":466,\"y\":422},{\"x\":566,\"y\":422},{\"x\":523,\"y\":412},{\"x\":623,\"y\":412}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"269049045129183233\",\"type\":\"base-edge\",\"sourceNodeId\":\"269048862299471872\",\"targetNodeId\":\"269049045129183232\",\"sourceAnchorId\":\"269048862299471872_output\",\"targetAnchorId\":\"269049045129183232_input\",\"pointsList\":[{\"x\":955,\"y\":412},{\"x\":1055,\"y\":412},{\"x\":1006,\"y\":422},{\"x\":1106,\"y\":422}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"text\",\"name\":\"option\",\"nodeId\":\"269048862299471872\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', `trigger_cron` = '' WHERE `id` = '2005948202528501762'; + + +-- 1. 新增 Chat2BI 公共工具 MCP(getChartExampleJson,独立于数据源,不与数据库/Online表单耦合) +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) +VALUES ('2006287314794676300', NULL, 'Chat2BI', '用于获取图表示例数据', 'plugin', 'api', '', '{"X-Sign":"true"}', + '[{"name":"getChartExampleJson","description":"获取图表示例数据,返回指定类型的图表JSON格式示例","path":"/airag/mcp/database/getChartExampleJson","method":"GET","enabled":true,"parameters":[{"name":"type","description":"图表类型,多个用英文逗号分割","type":"String","location":"Query","required":true,"defaultValue":""}],"responses":[]}]', + 'enable', 1, + '{"tokenParamName":"X-Access-Token","tool_count":1,"authType":"token","tokenParamValue":""}', + 'admin', '2026-03-26 19:22:47', 'admin', NULL, 'A01', NULL); + +-- 2. 新增 Online表单插件 MCP(4个工具:查询表列表、查询表结构、执行SQL、分页执行SQL) +INSERT INTO `airag_mcp` (`id`, `icon`, `name`, `descr`, `category`, `type`, `endpoint`, `headers`, `tools`, `status`, `synced`, `metadata`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`) +VALUES ('2006287314794676301', NULL, 'Online表单插件', '用于查询Online表单的元数据和数据', 'plugin', 'api', '', '{"X-Sign":"true"}', + '[{"name":"queryOnlineFormList","description":"查询所有已同步的Online表单列表(含表类型标注和主子表关联关系)","path":"/online/cgform/airag/queryOnlineFormList","method":"GET","enabled":true,"parameters":[],"responses":[]},{"name":"queryOnlineFormMetadata","description":"查询指定Online表单的字段结构(字段名、备注、类型、是否主键)及关联关系(主子表JOIN条件)","path":"/online/cgform/airag/queryOnlineFormMetadata","method":"GET","enabled":true,"parameters":[{"name":"tableName","description":"表名","type":"String","location":"Query","required":true,"defaultValue":""}],"responses":[{"name":"success","description":"是否成功","type":"Boolean"},{"name":"message","description":"若失败则返回失败原因","type":"String"},{"name":"result.tableName","description":"表名","type":"Object"},{"name":"result.tableComment","description":"表说明(业务含义)","type":"Object"},{"name":"result.columns[].columnName","description":"字段名","type":"Array"},{"name":"result.columns[].columnComment","description":"字段备注(核心,帮助大模型理解业务)","type":"Array"},{"name":"result.columns[].dataType","description":"数据类型(如varchar、int、datetime)","type":"Array"},{"name":"result.columns[].isPrimaryKey","description":"是否主键","type":"Array"},{"name":"result.relations[].relationType","description":"关联类型(一对多/一对一)","type":"Array"},{"name":"result.relations[].targetTable","description":"关联的目标表名","type":"Array"},{"name":"result.relations[].targetTableComment","description":"目标表说明","type":"Array"},{"name":"result.relations[].joinCondition","description":"JOIN条件(如 sub_table.fk_field = main_table.id)","type":"Array"}]},{"name":"sqlExecute","description":"执行SQL查询(仅支持SELECT,且只能查询Online表单范围内的表),不要输入注释等无关信息。","path":"/online/cgform/airag/sqlExecute","method":"POST","enabled":true,"parameters":[{"name":"sql","description":"要执行的SQL","type":"String","location":"Body","required":true,"defaultValue":""}],"responses":[{"name":"success","description":"是否成功","type":"Boolean"},{"name":"message","description":"若失败则返回失败原因","type":"String"},{"name":"result","description":"返回查询的结果,是个对象数组,数组的每一项都是一条数据,每条数据的key都是传入的查询的列。","type":"Array"}]},{"name":"sqlPageExecute","description":"分页执行SQL查询(仅支持SELECT,且只能查询Online表单范围内的表)","path":"/online/cgform/airag/sqlPageExecute","method":"POST","enabled":true,"parameters":[{"name":"sql","description":"原始sql,无需传入分页sql","type":"String","location":"Body","required":true,"defaultValue":""},{"name":"pageNo","description":"当前页码","type":"Number","location":"Body","required":false,"defaultValue":"1"},{"name":"pageSize","description":"每页页数","type":"Number","location":"Body","required":false,"defaultValue":"10"}],"responses":[{"name":"records","description":"数据行","type":"Array"},{"name":"total","description":"总数","type":"Number"}]}]', + 'enable', 1, + '{"tokenParamName":"X-Access-Token","tool_count":4,"authType":"token","tokenParamValue":""}', + 'admin', '2026-03-26 19:23:21', 'admin', NULL, 'A01', NULL); + +-- 3. 从数据库插件中移除 getChartExampleJson 工具(更新 tools JSON 和 tool_count) +UPDATE `airag_mcp` +SET `tools` = '[{"name":"queryTableMetadata","description":"用于查询表的表结构(元数据)","path":"/airag/mcp/database/queryTableMetadata","method":"GET","enabled":true,"parameters":[{"name":"tableName","description":"表名","type":"String","location":"Query","required":true,"defaultValue":""},{"name":"dbSourceKey","description":"数据源key","type":"String","location":"Query","required":false,"defaultValue":""}],"responses":[{"name":"success","description":"是否成功","type":"Boolean"},{"name":"message","description":"若失败则返回失败原因","type":"String"},{"name":"result.tableName","description":"表名(数据库实际表名)","type":"Object"},{"name":"result.tableComment","description":"表注释(业务含义)","type":"Object"},{"name":"result.columns[].columnName","description":"字段名","type":"Array"},{"name":"result.columns[].columnComment","description":"字段注释(核心,帮助大模型理解业务)","type":"Array"},{"name":"result.columns[].dataType","description":"数据类型(如varchar、int、datetime)","type":"Array"},{"name":"result.columns[].isPrimaryKey","description":"是否主键","type":"Array"}]},{"name":"sqlExecute","description":"用于执行 SQL 语句,仅能支持执行SELECT语句,不要输入注释等无关信息。","path":"/airag/mcp/database/sqlExecute","method":"POST","enabled":true,"parameters":[{"name":"sql","description":"要执行的SQL","type":"String","location":"Body","required":true,"defaultValue":""},{"name":"dbSourceKey","description":"数据源key","type":"String","location":"Body","required":false,"defaultValue":""}],"responses":[{"name":"success","description":"是否成功","type":"Boolean"},{"name":"message","description":"若失败则返回失败原因","type":"String"},{"name":"result","description":"返回查询的结果,是个对象数组,数组的每一项都是一条数据,每条数据的key都是传入的查询的列。","type":"Array"}]},{"name":"queryTablesInfoText","description":"用于查询指定数据源的所有表名和描述","path":"/airag/mcp/database/queryTablesInfoText","method":"GET","enabled":true,"parameters":[{"name":"dbSourceKey","description":"数据源code,不填则系统默认","type":"String","location":"Query","required":false,"defaultValue":""}],"responses":[]},{"name":"queryDataSourceInfoText","description":"用于查询所有数据源的信息,不需要传递参数。","path":"/airag/mcp/database/queryDataSourceInfoText","method":"GET","enabled":true,"parameters":[],"responses":[]},{"name":"queryDataSourceType","description":"获取默认数据源或指定数据的数据库类型","path":"/airag/mcp/database/queryDataSourceType","method":"GET","enabled":true,"parameters":[{"name":"dbSourceKey","description":"数据源key,若为空则系统默认","type":"String","location":"Query","required":false,"defaultValue":""}],"responses":[]},{"name":"sqlPageExecute","description":"分页执行 SQL 查询(仅支持 SELECT)","path":"/airag/mcp/database/sqlPageExecute","method":"POST","enabled":true,"parameters":[{"name":"sql","description":"原始sql,无需传入分页sql","type":"String","location":"Body","required":true,"defaultValue":""},{"name":"dbSourceKey","description":"数据源,可为空","type":"String","location":"Body","required":false,"defaultValue":""},{"name":"pageNo","description":"当前页码","type":"Number","location":"Body","required":false,"defaultValue":"1"},{"name":"pageSize","description":"每页页数","type":"Number","location":"Body","required":false,"defaultValue":"10"}],"responses":[{"name":"records","description":"数据行","type":"Array"},{"name":"total","description":"总数","type":"Number"}]}]', + `metadata` = '{"authType": "token", "tool_count": 6, "tokenParamName": "X-Access-Token", "tokenParamValue": ""}', + `update_time` = '2026-03-26 19:23:33' +WHERE `id` = '2006287314794676226'; + +-- 4. 更新 Chat2BI 数据库版流程 +UPDATE `airag_flow` +SET `chain` = 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', + `design` = '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":99,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位严谨的数据可视化助手。你的唯一职责是:根据用户需求,从数据库查询数据并生成 `` 图表标签。你不处理任何与图表生成无关的请求。\\n## 核心原则\\n1. **数据真实性**:所有图表数据必须来源于 SQL 查询结果或用户直接提供的数据,严禁虚构、编造、推测任何数据。\\n2. **格式严格性**:输出必须严格遵循指定的 `` 标签格式,不得有任何偏差。\\n3. **最小权限**:仅对下方列出的已授权表执行 SELECT 查询,拒绝一切超出范围的操作。\\n4. **隐私合规**:禁止输出可识别个人身份的敏感信息(完整身份证号、详细住址、明文密码等),涉及此类字段必须脱敏或拒绝。\\n## 图表类型定义\\n### 简单图表(直接使用 x/y 格式)\\n| type | 名称 | data 格式 |\\n|------|------|-----------|\\n| `bar` | 柱状图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n| `line` | 折线图/曲线图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n| `pie` | 饼图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n### 复杂图表(必须先查询示例格式)\\n| type | 名称 |\\n|------|------|\\n| `radar` | 雷达图 |\\n| `gauge` | 仪表盘 |\\n| `barline` | 折柱混合图 |\\n| `multibar` | 多列柱状图 |\\n| `multiline` | 多行折线图 |\\n| `area` | 面积图 |\\n## 工作流程(严格按顺序执行)\\n### 第一步:需求解析\\n分析用户请求,提取以下信息:\\n- **图表类型**:用户想要哪种图表?若未明确指定,根据数据特征推断最合适的类型。记录用户是否**明确指定**了图表类型(影响后续是否生成 `altTypes`)。\\n- **数据维度**:x 轴(分类/时间)和 y 轴(度量/指标)分别是什么?\\n- **数据来源**:用户是否指定了数据源?是否直接提供了数据?\\n- **筛选条件**:是否有时间范围、分组条件、排序要求、数量限制等?\\n**若需求模糊不可执行**(无法确定表、字段或图表类型),必须向用户提问澄清,不得猜测执行。\\n### 第二步:判断数据来源\\n```\\n用户已直接提供数据?\\n├─ 是 → 跳到第四步(数据转换)\\n└─ 否 → 继续第三步(数据库查询)\\n```\\n### 第三步:数据库查询\\n**3.1 验证表范围**\\n检查需求涉及的表是否在下方「支持的数据源」列表中。\\n- 若不在列表中 → 立即告知用户\\\"该表不在可查询范围内\\\",终止流程。\\n- 若在列表中 → 继续。\\n**3.2 查询表结构**\\n调用工具查询相关表的字段结构,了解可用列名、数据类型和字段备注。\\n字段备注将用于数据表格的列标题显示,请在构建 `columns` 时使用。\\n**3.3 构建 SQL**\\n你需要构建两条 SQL:\\n1. **图表聚合 SQL**(用于图表渲染):包含 `GROUP BY`、聚合函数等,产出图表所需的汇总数据。\\n2. **原始数据 SQL**(用于数据表格展示):查询图表统计所依赖的原始明细数据,不包含 `GROUP BY` 和聚合函数,不添加 `LIMIT` 分页限制(系统会自动处理分页)。\\n两条 SQL 严格遵守以下规则:\\n- 仅允许 `SELECT` 语句,禁止 `INSERT`/`UPDATE`/`DELETE`/`DROP`/`ALTER`/`TRUNCATE` 等任何非查询操作。\\n- 禁止 SQL 注释(`--`、`/* */`)。\\n- 根据当前数据源的数据库类型(见「默认数据源类型」)使用对应的 SQL 方言。\\n- SQL 必须高效:使用适当的 `WHERE` 条件避免全表扫描。\\n- 图表聚合 SQL 在数据量可能较大时,默认添加合理的 `LIMIT`(建议不超过 100 条)。\\n- 原始数据 SQL 禁止添加 `LIMIT`,分页由系统自动处理。\\n**3.4 执行查询**\\n调用工具执行**图表聚合 SQL**,获取结果集。若查询返回空数据,告知用户未查询到数据,终止流程。\\n**3.5 验证原始数据 SQL**\\n构建完原始数据 SQL 后,必须调用 `sqlPageExecute` 工具进行验证,参数设置为 `pageNo=1, pageSize=1`,仅查询 1 条数据用于验证 SQL 语法的正确性。\\n- 若验证通过 → 将该 SQL 放入输出的 `sql` 字段。\\n- 若验证失败 → 根据错误信息修正 SQL 后再次验证,最多重试 3 次。若仍失败,则不输出 `sql` 字段(图表仍正常渲染,但不提供数据表格功能)。\\n### 第四步:图表格式确定\\n```\\n图表类型是 bar/line/pie(简单图表)?\\n├─ 是 → 直接使用 [{\\\"x\\\":\\\"...\\\", \\\"y\\\":...}] 格式,禁止调用示例查询工具\\n└─ 否 → 该复杂图表的示例格式是否已在本次对话中查询过?\\n    ├─ 是 → 复用已有格式,禁止重复调用\\n    └─ 否 → 调用工具查询示例格式(支持逗号分割,一次性查询所有需要的复杂图表类型,禁止逐个查询)\\n```\\n### 第四步半:确定可替代图表类型\\n```\\n用户在第一步中明确指定了图表类型?\\n├─ 是 → altTypes 直接传空数组 [],跳过本步骤\\n└─ 否 → 按下方规则填充 altTypes\\n```\\n当用户未明确指定图表类型(由你自动推断)时,根据当前数据结构,判断哪些其他图表类型可以使用**同一份 data** 直接渲染(无需修改数据格式),将它们填入 `altTypes` 数组。互转规则如下:\\n| 当前类型 | 可替代类型(altTypes 候选) |\\n|----------|--------------------------|\\n| `bar` | `line`、`pie` |\\n| `line` | `bar`、`pie` |\\n| `pie` | `bar`、`line` |\\n| `multibar` | `multiline`、`area` |\\n| `multiline` | `multibar`、`area` |\\n| `area` | `multibar`、`multiline` |\\n| `radar` | 无(数据结构独特) |\\n| `gauge` | 无(单值数据) |\\n| `barline` | 无(含 seriesType 区分) |\\n注意:\\n- `altTypes` 不包含当前主类型(`type` 字段已指定)。\\n- 仅列出数据结构完全兼容的类型,不得列出需要修改 data 格式才能渲染的类型。\\n- 若无可替代类型,`altTypes` 传空数组 `[]`。\\n### 第五步:数据转换\\n将查询结果转换为目标图表格式:\\n- 简单图表:每行数据映射为 `{\\\"x\\\": 字符串, \\\"y\\\": 数字}`。\\n- 复杂图表:严格按照查询到的示例格式组装数据。\\n- `x` 值必须为字符串类型,`y` 值必须为数字类型。\\n- 若需聚合(求和、计数、平均等),在 SQL 中完成,不在转换阶段手动计算。\\n- 数据转换在你的回复中直接完成,禁止调用工具进行转换。\\n### 第六步:输出\\n生成最终结果前,执行双重校验:\\n1. **标签校验**:`` 和 `` 首尾完整闭合。\\n2. **JSON 校验**:`` 内的 JSON 是标准格式——无多余逗号、无未闭合括号、无尾随逗号、所有键名使用双引号。\\n3. **数据校验**:`data` 数组不为空,每个对象包含必需的键。\\n4. **SQL 字段校验**:若数据来自数据库查询,`sql` 字段必须包含原始数据查询 SQL,`dbSource` 字段必须与查询时使用的数据源一致,`columns` 字段必须包含原始数据 SQL 中所有 SELECT 字段的中文标题映射。\\n## 输出格式\\n最终输出必须且仅包含以下格式,`` 标签前后各保留两个空行。禁止在标签外添加额外说明、解释或修饰文字。如需对数据做简短说明,放在标签之前。\\n### 数据来自数据库查询时:\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"altTypes\\\":[\\\"可替代类型1\\\",\\\"可替代类型2\\\"],\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}],\\\"sql\\\":\\\"原始数据查询SQL\\\",\\\"dbSource\\\":\\\"数据源标识或空字符串\\\",\\\"columns\\\":{\\\"field1\\\":\\\"列标题1\\\",\\\"field2\\\":\\\"列标题2\\\"}}\\n\\n\\n### 数据由用户直接提供时:\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"altTypes\\\":[\\\"可替代类型1\\\",\\\"可替代类型2\\\"],\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n### 字段说明\\n| 字段 | 类型 | 必填 | 说明 |\\n|------|------|------|------|\\n| `type` | string | 是 | 图表类型 |\\n| `altTypes` | string[] | 是 | 可替代的图表类型数组。系统会据此提供图表切换功能。无可替代类型时传 `[]` |\\n| `data` | array/object | 是 | 图表展示数据(聚合后的数据) |\\n| `sql` | string | 条件必填 | 原始数据查询 SQL(不含 LIMIT/分页,系统自动处理)。仅当数据来自数据库查询时必填 |\\n| `dbSource` | string | 条件必填 | 数据源标识。默认数据源传空字符串 `\\\"\\\"`,指定数据源传对应的 key。仅当数据来自数据库查询时必填 |\\n| `columns` | object | 条件必填 | 原始数据 SQL 中 SELECT 字段名到中文列标题的映射。仅当数据来自数据库查询时必填。key 为 SQL 中的字段名(或别名),value 为该字段的中文显示标题。标题来源于表结构的字段备注,若备注过长(超过 4 个字),需根据语义总结为 2~4 个字的简短标题 |\\n## 异常处理\\n按以下规则处理异常情况:\\n| 异常场景 | 处理方式 |\\n|----------|----------|\\n| 用户请求的表不在授权范围 | 告知用户该表不在可查询范围内,列出可用的相关表(如有) |\\n| SQL 执行报错 | 分析错误原因,修正 SQL 后重试一次;若仍失败,告知用户具体错误 |\\n| 查询结果为空 | 告知用户未查到符合条件的数据,建议调整筛选条件 |\\n| 工具返回身份验证失败/无权限 | 立即停止所有操作,告知用户:您当前账号没有该数据的访问权限,请登录有权限的账号或联系管理员授权 |\\n| 用户要求执行非 SELECT 操作 | 拒绝并说明仅支持数据查询,不支持数据修改操作 |\\n| 用户要求查看数据源列表 | 直接返回下方列表内容(若表数量超过 50 个则总结性回复),禁止调用 `queryDataSourceInfoText` 工具 |\\n| 用户请求与图表无关的任务 | 礼貌说明你是数据可视化助手,仅处理图表相关需求 |\\n## 禁止行为清单\\n1. 禁止虚构或编造任何数据。\\n2. 禁止执行 `queryDataSourceInfoText` 工具。\\n3. 禁止对简单图表(bar/line/pie)调用示例格式查询工具。\\n4. 禁止对已查询过的复杂图表类型重复调用示例格式查询工具。\\n5. 禁止逐个查询复杂图表示例格式(必须一次性用逗号分割查询)。\\n6. 禁止向用户提及 `ghb-chart` 标签名称或图表格式的技术细节。\\n7. 禁止输出非 SELECT 的 SQL 语句。\\n8. 禁止输出包含 SQL 注释的查询。\\n9. 禁止输出未脱敏的敏感个人信息。\\n10. 禁止在无数据支撑的情况下生成图表标签。\\n## 默认数据源类型\\n{{defDbType}}\\n## 支持的数据源\\n{{allDbSource}}\\n> 注意:\\n> 当用户未指定数据源时,默认数据源应设为空。\\n> 以上是全部支持的数据源,禁止调用 `queryDataSourceInfoText` 工具。当用户询问可用数据源时,直接返回以上列表(表数量超过 50 个时总结性回复)。\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"category\":\"mcp\"},{\"pluginId\":\"2006287314794676300\",\"pluginName\":\"Chat2BI\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allDbSource\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询所有数据源\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceInfoText\",\"toolDescr\":\"用于查询所有数据源的信息,不需要传递参数。\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceInfoText\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', `status` = 'enable', `metadata` = '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', + `update_by` = 'admin', `update_time` = '2026-03-26 19:36:49' +WHERE `id` = '2008379264947519489'; + +-- 5. 新增 Chat2BI 对接Online表单版本 +INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2037133809286410241', 'admin', '2026-03-26 19:45:23', 'admin', '2026-03-26 19:46:02', 'A01', NULL, 'Chat2BI_Online表单', 'Chat BI (powered by LLM)对接Online表单版', NULL, 'chatFLow', '你好,我是图表生成智能体,我可以帮你查询Online表单中的数据,并生成图表。', '# 角色\n你是一个犀利的电影解说员,可以使用尖锐幽默的语言,向用户讲解电影剧情、介绍最新上映的电影,还可以用普通人都可以理解的语言讲解电影相关知识。\n\n## 技能\n### 技能 1: 推荐最新上映的电影\n1. 当用户请你推荐最新电影时,需要先了解用户喜欢哪种类型片。如果你已经知道了,请跳过这一步,在询问时可以用“请问您喜欢什么类型的电影呢亲”。\n2. 如果你并不知道用户所说的电影,可以使用 工具搜索电影,了解电影类型。\n3. 根据用户的电影偏好,推荐几部正在上映和即将上映的电影,在推荐开头可以说“好的亲,以下是为您推荐的电影”。\n===回复示例===\n - 🎬 电影名: <电影名>\n - 🕐 上映时间: <电影在中国大陆的上映的日期>\n - 💡 电影简介: <100字总结这部电影的剧情摘要>\n===示例结束===\n\n### 技能 2: 介绍电影\n1. 当用户说介绍某一部电影,请使用工具 搜索电影介绍的链接,在收到需求时可以回应“好嘞亲,马上为您查找相关电影介绍”。\n2. 如果此时获取的信息不够全面,可以继续使用 工具 打开搜索结果中的相关链接,以了解电影详情。\n3. 根据搜索和浏览结果,生成电影介绍\n### 技能 3: 介绍电影概念\n- 你可以使用数据集中的知识,调用 知识库 搜索相关知识,并向用户介绍基础概念,介绍前可以说“亲,下面为您介绍一下这个电影概念”。\n- 使用用户熟悉的电影,举一个实际的场景解释概念\n\n## 限制:\n- 只讨论与电影有关的内容,拒绝回答与电影无关的话题,拒绝时可以说“不好意思亲,这边只讨论电影相关话题哦”。\n- 所输出的内容必须按照给定的格式进行组织,不能偏离框架要求,在表述中合理运用常用语。\n- 总结部分不能超过 100 字。\n- 只会输出知识库中已有内容, 不在知识库中的书籍, 通过 工具去了解。\n- 请使用 Markdown 的 ^^ 形式说明引用来源。”', NULL, '', '2037131053712568322', 'enable', 99, NULL, '[{\"key\":1,\"descr\":\"用户性别比例\",\"update\":false}]', NULL, NULL, NULL, NULL, NULL, NULL); + +-- 【#9468】给数据源操作加上权限 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038567162158080001', '1439511654494937090', '导出数据源', NULL, NULL, 0, NULL, NULL, 2, 'system:datasource:export', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-30 18:41:01', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038567065030582273', '1439511654494937090', '导入数据源', NULL, NULL, 0, NULL, NULL, 2, 'system:datasource:import', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-30 18:40:38', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038566470550904833', '1439511654494937090', '删除数据源', NULL, NULL, 0, NULL, NULL, 2, 'system:datasource:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-30 18:38:16', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038566382055284738', '1439511654494937090', '编辑数据源', NULL, NULL, 0, NULL, NULL, 2, 'system:datasource:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-30 18:37:55', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038566315785281538', '1439511654494937090', '添加数据源', NULL, NULL, 0, NULL, NULL, 2, 'system:datasource:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-30 18:37:39', NULL, NULL, 0, 0, '1', 0); + +INSERT INTO `airag_flow` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `application_name`, `name`, `descr`, `icon`, `chain`, `design`, `status`, `metadata`, `trigger_cron`) VALUES ('2037131053712568322', 'admin', '2026-03-26 19:34:26', 'admin', '2026-03-26 19:44:04', 'A01', '0', 'ghb', 'Chat2BI生成图表_Online', '', '', 'THEN(\n start.tag(\'start-node\'),\n SWITCH(switch.tag(\'271554566412288000\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n SWITCH(classifier.tag(\'271554622242668544\')).to(\n SWITCH(classifier.tag(\'271481764802605056\')).to(\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\"),\n end.tag(\'271480115023458304\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271481764802605056\'),\n THEN(\n varMerge.tag(\'271556843709317120\'),\n tools.tag(\'276308429448634368\'),\n tools.tag(\'274495573258244096\'),\n llm.tag(\'271548210211192832\'),\n reply.tag(\'271548872986722304\'),\n end.tag(\'271483924713975808\')\n ).tag(\"271556843709317120\")\n ).tag(\'271554622242668544\')\n ).tag(\'271554566412288000\')\n).tag(\"start-node\")', '{\"nodes\":[{\"id\":\"start-node\",\"type\":\"start\",\"x\":-197,\"y\":509,\"properties\":{\"text\":\"开始\",\"remarks\":\"\",\"options\":{\"cronTrigger\":{\"enabled\":false,\"cronExp\":\"0 0 0 * * ?\",\"beginTime\":null,\"endTime\":null,\"inputParams\":{},\"custom\":{\"time\":{\"second\":0,\"minute\":0},\"hour\":{\"mode\":\"every\",\"range\":[0,23],\"values\":[],\"interval\":{\"start\":0,\"step\":1}},\"day\":{\"type\":\"day\",\"day\":{\"mode\":\"every\",\"range\":[1,31],\"values\":[],\"interval\":{\"start\":1,\"step\":1}},\"week\":{\"values\":[1]}},\"month\":{\"mode\":\"every\",\"values\":[]}}}},\"inputParams\":[{\"field\":\"content\",\"name\":\"用户问题\",\"type\":\"string\",\"required\":false},{\"field\":\"history\",\"name\":\"历史记录\",\"type\":\"string[]\",\"required\":false},{\"field\":\"images\",\"name\":\"图片\",\"type\":\"picture\",\"required\":false}],\"outputParams\":[],\"width\":332,\"height\":92}},{\"id\":\"271480115023458304\",\"type\":\"end\",\"x\":1372,\"y\":819,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"很抱歉,我无法回复您的这个问题,您可以向我询问图表相关的信息,比如:查询用户表的男女比例。\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"customValue\":\"\",\"type\":\"number\"}],\"width\":332,\"height\":136}},{\"id\":\"271481764802605056\",\"type\":\"classifier\",\"x\":854,\"y\":462,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询图表、报表或相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271480115023458304\"}},\"inputParams\":[{\"field\":\"content\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271483924713975808\",\"type\":\"end\",\"x\":3200,\"y\":430,\"properties\":{\"text\":\"结束\",\"options\":{\"outputText\":false,\"outputContent\":\"{{回复}}\",\"outputType\":\"text\",\"cardConfig\":null},\"inputParams\":[],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271548210211192832\",\"type\":\"llm\",\"x\":2454,\"y\":433,\"properties\":{\"text\":\"LLM\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek\",\"temperature\":0.7,\"timeout\":60}},\"history\":99,\"messages\":[{\"role\":\"system\",\"content\":\"# 角色\\n你是一位严谨的数据可视化助手。你的唯一职责是:根据用户需求,从 Online 表单查询数据并生成 `` 图表标签。你不处理任何与图表生成无关的请求。\\n## 核心原则\\n1. **数据真实性**:所有图表数据必须来源于 SQL 查询结果或用户直接提供的数据,严禁虚构、编造、推测任何数据。\\n2. **格式严格性**:输出必须严格遵循指定的 `` 标签格式,不得有任何偏差。\\n3. **最小权限**:仅对下方列出的已授权 Online 表单执行 SELECT 查询,拒绝一切超出范围的操作。\\n4. **隐私合规**:禁止输出可识别个人身份的敏感信息(完整身份证号、详细住址、明文密码等),涉及此类字段必须脱敏或拒绝。\\n## 图表类型定义\\n### 简单图表(直接使用 x/y 格式)\\n| type | 名称 | data 格式 |\\n|------|------|-----------|\\n| `bar` | 柱状图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n| `line` | 折线图/曲线图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n| `pie` | 饼图 | `[{\\\"x\\\":\\\"类别\\\",\\\"y\\\":数值}, ...]` |\\n### 复杂图表(必须先查询示例格式)\\n| type | 名称 |\\n|------|------|\\n| `radar` | 雷达图 |\\n| `gauge` | 仪表盘 |\\n| `barline` | 折柱混合图 |\\n| `multibar` | 多列柱状图 |\\n| `multiline` | 多行折线图 |\\n| `area` | 面积图 |\\n## 工作流程(严格按顺序执行)\\n### 第一步:需求解析\\n分析用户请求,提取以下信息:\\n- **图表类型**:用户想要哪种图表?若未明确指定,根据数据特征推断最合适的类型。记录用户是否**明确指定**了图表类型(影响后续是否生成 `altTypes`)。\\n- **数据维度**:x 轴(分类/时间)和 y 轴(度量/指标)分别是什么?\\n- **数据来源**:用户是否指定了 Online 表单?是否直接提供了数据?\\n- **筛选条件**:是否有时间范围、分组条件、排序要求、数量限制等?\\n**若需求模糊不可执行**(无法确定表、字段或图表类型),必须向用户提问澄清,不得猜测执行。\\n### 第二步:判断数据来源\\n```\\n用户已直接提供数据?\\n├─ 是 → 跳到第四步(数据转换)\\n└─ 否 → 继续第三步(Online 表单查询)\\n```\\n### 第三步:Online 表单查询\\n**3.1 验证表范围**\\n检查需求涉及的表是否在下方「支持的 Online 表单」列表中。\\n- 若不在列表中 → 立即告知用户\\\"该表不在可查询的 Online 表单范围内\\\",终止流程。\\n- 若在列表中 → 继续。\\n**3.2 查询表结构**\\n调用 `queryOnlineFormMetadata` 工具查询相关表的字段结构,了解可用列名、数据类型和字段备注。\\n字段备注将用于数据表格的列标题显示,请在构建 `columns` 时使用。\\n工具返回的 `relations` 字段包含该表的关联关系(主子表关系、外键关联),请重点关注:\\n- `relationType`:关联类型(一对多 / 一对一)\\n- `targetTable`:关联的目标表名\\n- `joinCondition`:JOIN 条件(如 `order_item.order_id = demo_order.id`)\\n当用户需求涉及多个表的数据时(如\\\"统计每个订单的明细数量\\\"),必须查询相关表的元数据以获取 JOIN 条件,不得猜测关联关系。\\n**3.3 构建 SQL**\\n你需要构建两条 SQL:\\n1. **图表聚合 SQL**(用于图表渲染):包含 `GROUP BY`、聚合函数等,产出图表所需的汇总数据。\\n2. **原始数据 SQL**(用于数据表格展示):查询图表统计所依赖的原始明细数据,不包含 `GROUP BY` 和聚合函数,不添加 `LIMIT` 分页限制(系统会自动处理分页)。\\n**跨表查询规则**:\\n- 当需求涉及主子表关联数据时,使用 `JOIN` 连接表,JOIN 条件必须使用 `queryOnlineFormMetadata` 返回的 `relations.joinCondition`。\\n- 下方表单列表中已标注主子表关系和 JOIN 条件,可作为快速参考;但构建 SQL 前仍需调用 `queryOnlineFormMetadata` 查询完整字段信息。\\n- 禁止凭猜测构造 JOIN 条件。\\n两条 SQL 严格遵守以下规则:\\n- 仅允许 `SELECT` 语句,禁止 `INSERT`/`UPDATE`/`DELETE`/`DROP`/`ALTER`/`TRUNCATE` 等任何非查询操作。\\n- 禁止 SQL 注释(`--`、`/* */`)。\\n- 根据当前数据库类型(见「数据库类型」)使用对应的 SQL 方言。\\n- SQL 必须高效:使用适当的 `WHERE` 条件避免全表扫描。\\n- 图表聚合 SQL 在数据量可能较大时,默认添加合理的 `LIMIT`(建议不超过 100 条)。\\n- 原始数据 SQL 禁止添加 `LIMIT`,分页由系统自动处理。\\n- **SQL 中只能涉及已授权的 Online 表单对应的表**,不得查询其他任何表。\\n**3.4 执行查询**\\n调用 `sqlExecute` 工具执行**图表聚合 SQL**,获取结果集。若查询返回空数据,告知用户未查询到数据,终止流程。\\n**3.5 验证原始数据 SQL**\\n构建完原始数据 SQL 后,必须调用 `sqlPageExecute` 工具进行验证,参数设置为 `pageNo=1, pageSize=1`,仅查询 1 条数据用于验证 SQL 语法的正确性。\\n- 若验证通过 → 将该 SQL 放入输出的 `sql` 字段。\\n- 若验证失败 → 根据错误信息修正 SQL 后再次验证,最多重试 3 次。若仍失败,则不输出 `sql` 字段(图表仍正常渲染,但不提供数据表格功能)。\\n### 第四步:图表格式确定\\n```\\n图表类型是 bar/line/pie(简单图表)?\\n├─ 是 → 直接使用 [{\\\"x\\\":\\\"...\\\", \\\"y\\\":...}] 格式,禁止调用示例查询工具\\n└─ 否 → 该复杂图表的示例格式是否已在本次对话中查询过?\\n    ├─ 是 → 复用已有格式,禁止重复调用\\n    └─ 否 → 调用工具查询示例格式(支持逗号分割,一次性查询所有需要的复杂图表类型,禁止逐个查询)\\n```\\n### 第四步半:确定可替代图表类型\\n```\\n用户在第一步中明确指定了图表类型?\\n├─ 是 → altTypes 直接传空数组 [],跳过本步骤\\n└─ 否 → 按下方规则填充 altTypes\\n```\\n当用户未明确指定图表类型(由你自动推断)时,根据当前数据结构,判断哪些其他图表类型可以使用**同一份 data** 直接渲染(无需修改数据格式),将它们填入 `altTypes` 数组。互转规则如下:\\n| 当前类型 | 可替代类型(altTypes 候选) |\\n|----------|--------------------------|\\n| `bar` | `line`、`pie` |\\n| `line` | `bar`、`pie` |\\n| `pie` | `bar`、`line` |\\n| `multibar` | `multiline`、`area` |\\n| `multiline` | `multibar`、`area` |\\n| `area` | `multibar`、`multiline` |\\n| `radar` | 无(数据结构独特) |\\n| `gauge` | 无(单值数据) |\\n| `barline` | 无(含 seriesType 区分) |\\n注意:\\n- `altTypes` 不包含当前主类型(`type` 字段已指定)。\\n- 仅列出数据结构完全兼容的类型,不得列出需要修改 data 格式才能渲染的类型。\\n- 若无可替代类型,`altTypes` 传空数组 `[]`。\\n### 第五步:数据转换\\n将查询结果转换为目标图表格式:\\n- 简单图表:每行数据映射为 `{\\\"x\\\": 字符串, \\\"y\\\": 数字}`。\\n- 复杂图表:严格按照查询到的示例格式组装数据。\\n- `x` 值必须为字符串类型,`y` 值必须为数字类型。\\n- 若需聚合(求和、计数、平均等),在 SQL 中完成,不在转换阶段手动计算。\\n- 数据转换在你的回复中直接完成,禁止调用工具进行转换。\\n### 第六步:输出\\n生成最终结果前,执行双重校验:\\n1. **标签校验**:`` 和 `` 首尾完整闭合。\\n2. **JSON 校验**:`` 内的 JSON 是标准格式——无多余逗号、无未闭合括号、无尾随逗号、所有键名使用双引号。\\n3. **数据校验**:`data` 数组不为空,每个对象包含必需的键。\\n4. **SQL 字段校验**:若数据来自 Online 表单查询,`sql` 字段必须包含原始数据查询 SQL,`columns` 字段必须包含原始数据 SQL 中所有 SELECT 字段的中文标题映射。\\n## 输出格式\\n最终输出必须且仅包含以下格式,`` 标签前后各保留两个空行。禁止在标签外添加额外说明、解释或修饰文字。如需对数据做简短说明,放在标签之前。\\n### 数据来自 Online 表单查询时:\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"altTypes\\\":[\\\"可替代类型1\\\",\\\"可替代类型2\\\"],\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}],\\\"sql\\\":\\\"原始数据查询SQL\\\",\\\"dbSource\\\":\\\"\\\",\\\"columns\\\":{\\\"field1\\\":\\\"列标题1\\\",\\\"field2\\\":\\\"列标题2\\\"}}\\n\\n\\n### 数据由用户直接提供时:\\n\\n{\\\"type\\\":\\\"图表类型\\\",\\\"altTypes\\\":[\\\"可替代类型1\\\",\\\"可替代类型2\\\"],\\\"data\\\":[{\\\"x\\\":\\\"数据项1\\\",\\\"y\\\":数值1},{\\\"x\\\":\\\"数据项2\\\",\\\"y\\\":数值2}]}\\n\\n\\n### 字段说明\\n| 字段 | 类型 | 必填 | 说明 |\\n|------|------|------|------|\\n| `type` | string | 是 | 图表类型 |\\n| `altTypes` | string[] | 是 | 可替代的图表类型数组。系统会据此提供图表切换功能。无可替代类型时传 `[]` |\\n| `data` | array/object | 是 | 图表展示数据(聚合后的数据) |\\n| `sql` | string | 条件必填 | 原始数据查询 SQL(不含 LIMIT/分页,系统自动处理)。仅当数据来自 Online 表单查询时必填 |\\n| `dbSource` | string | 条件必填 | 数据源标识。Online 表单固定传空字符串 `\\\"\\\"`。仅当数据来自 Online 表单查询时必填 |\\n| `columns` | object | 条件必填 | 原始数据 SQL 中 SELECT 字段名到中文列标题的映射。仅当数据来自 Online 表单查询时必填。key 为 SQL 中的字段名(或别名),value 为该字段的中文显示标题。标题来源于表结构的字段备注,若备注过长(超过 4 个字),需根据语义总结为 2~4 个字的简短标题 |\\n## 异常处理\\n按以下规则处理异常情况:\\n| 异常场景 | 处理方式 |\\n|----------|----------|\\n| 用户请求的表不在 Online 表单范围 | 告知用户该表不在可查询范围内,列出可用的相关表(如有) |\\n| SQL 执行报错 | 分析错误原因,修正 SQL 后重试一次;若仍失败,告知用户具体错误 |\\n| 查询结果为空 | 告知用户未查到符合条件的数据,建议调整筛选条件 |\\n| 工具返回身份验证失败/无权限 | 立即停止所有操作,告知用户:您当前账号没有该数据的访问权限,请登录有权限的账号或联系管理员授权 |\\n| 用户要求执行非 SELECT 操作 | 拒绝并说明仅支持数据查询,不支持数据修改操作 |\\n| 用户要求查看可用表列表 | 直接返回下方列表内容(若表数量超过 50 个则总结性回复),禁止调用 `queryOnlineFormList` 工具 |\\n| 用户请求与图表无关的任务 | 礼貌说明你是数据可视化助手,仅处理图表相关需求 |\\n## 禁止行为清单\\n1. 禁止虚构或编造任何数据。\\n2. 禁止调用 `queryOnlineFormList` 工具(可用表列表已在下方提供)。\\n3. 禁止对简单图表(bar/line/pie)调用示例格式查询工具。\\n4. 禁止对已查询过的复杂图表类型重复调用示例格式查询工具。\\n5. 禁止逐个查询复杂图表示例格式(必须一次性用逗号分割查询)。\\n6. 禁止向用户提及 `ghb-chart` 标签名称或图表格式的技术细节。\\n7. 禁止输出非 SELECT 的 SQL 语句。\\n8. 禁止输出包含 SQL 注释的查询。\\n9. 禁止输出未脱敏的敏感个人信息。\\n10. 禁止在无数据支撑的情况下生成图表标签。\\n11. 禁止查询不在 Online 表单范围内的任何数据库表。\\n## 数据库类型\\n{{defDbType}}\\n## 支持的 Online 表单\\n{{allOnlineFormList}}\\n> 注意:\\n> - 以上是全部可查询的 Online 表单,禁止调用 `queryOnlineFormList` 工具。当用户询问可用表时,直接返回以上列表(表数量超过 50 个时总结性回复)。\\n> - 列表中已标注表类型(`[单表]`/`[主表]`/`[附表]`)和主子表关联关系(包括关联类型和 JOIN 条件),跨表查询时可作为快速参考。\\n\\n\"},{\"role\":\"user\",\"content\":\"{{问题}}\\n\\n\"}],\"plugins\":[{\"pluginId\":\"2006287314794676301\",\"pluginName\":\"Online表单插件\",\"category\":\"mcp\"},{\"pluginId\":\"2006287314794676300\",\"pluginName\":\"Chat2BI\",\"category\":\"mcp\"}],\"showToolExecution\":true},\"inputParams\":[{\"field\":\"content\",\"name\":\"问题\",\"nodeId\":\"start-node\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"allOnlineFormList\",\"nodeId\":\"274495573258244096\",\"customValue\":\"\",\"type\":\"string\"},{\"field\":\"result\",\"name\":\"defDbType\",\"nodeId\":\"276308429448634368\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[{\"field\":\"text\",\"name\":\"回复内容\",\"type\":\"string\"}],\"width\":332,\"height\":180}},{\"id\":\"271548872986722304\",\"type\":\"reply\",\"x\":2829,\"y\":631,\"properties\":{\"text\":\"直接回复\",\"options\":{\"content\":\"{{回复}}\",\"stream\":true},\"inputParams\":[{\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"customValue\":\"\",\"type\":\"string\"}],\"outputParams\":[],\"width\":332,\"height\":114}},{\"id\":\"271554566412288000\",\"type\":\"switch\",\"x\":188,\"y\":419,\"properties\":{\"text\":\"历史记录是否为空\",\"options\":{\"if\":[{\"logic\":\"AND\",\"conditions\":[{\"nodeId\":\"start-node\",\"field\":\"history\",\"operator\":\"EMPTY\",\"value\":\"\",\"type\":\"string[]\"}],\"next\":\"271481764802605056\"}],\"else\":{\"next\":\"271554622242668544\"}},\"inputParams\":[],\"outputParams\":[{\"field\":\"index\",\"name\":\"分支索引\",\"type\":\"number\"}],\"width\":332,\"height\":118}},{\"id\":\"271554622242668544\",\"type\":\"classifier\",\"x\":511,\"y\":605,\"properties\":{\"text\":\"分类器\",\"options\":{\"model\":{\"modeId\":\"1897481367743143938\",\"params\":{\"model\":\"deepseek-chat\",\"temperature\":0.7}},\"categories\":[{\"category\":\"用户希望查询或正在和Assistant聊图表相关数据、信息\",\"next\":\"271556843709317120\"}],\"else\":{\"next\":\"271481764802605056\"}},\"inputParams\":[{\"field\":\"history\",\"nodeId\":\"start-node\"}],\"outputParams\":[{\"field\":\"index\",\"name\":\"分类索引\",\"type\":\"number\"},{\"field\":\"content\",\"name\":\"分类描述\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"271556843709317120\",\"type\":\"varMerge\",\"x\":1368,\"y\":620,\"properties\":{\"text\":\"聚合\",\"options\":{\"varGroups\":[{\"name\":\"用户问题\",\"type\":\"string\",\"vars\":[{\"nodeId\":\"start-node\",\"field\":\"content\",\"isCustom\":false,\"type\":\"string\"}]}]},\"inputParams\":[],\"outputParams\":[{\"field\":\"用户问题\",\"name\":\"用户问题\",\"type\":\"string\"}],\"width\":332,\"height\":92}},{\"id\":\"274495573258244096\",\"type\":\"tools\",\"x\":2105,\"y\":659,\"properties\":{\"text\":\"查询已同步Online表\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676301\",\"pluginName\":\"Online表单插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryOnlineFormList\",\"toolDescr\":\"查询所有已同步的Online表单列表(含表类型标注和主子表关联关系)\",\"toolParameters\":[],\"endpoint\":\"\",\"path\":\"/online/cgform/airag/queryOnlineFormList\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":136}},{\"id\":\"276308429448634368\",\"type\":\"tools\",\"x\":1736,\"y\":494,\"properties\":{\"text\":\"查询默认数据源类型\",\"options\":{\"tools\":{\"pluginId\":\"2006287314794676226\",\"pluginName\":\"数据库插件\",\"pluginCategory\":\"plugin\",\"toolName\":\"queryDataSourceType\",\"toolDescr\":\"获取默认数据源或指定数据的数据库类型\",\"toolParameters\":[{\"name\":\"dbSourceKey\",\"description\":\"数据源key,若为空则系统默认\",\"required\":false,\"type\":\"String\",\"location\":\"Query\",\"value\":\"\"}],\"endpoint\":\"\",\"path\":\"/airag/mcp/database/queryDataSourceType\",\"method\":\"GET\",\"headers\":{\"X-Sign\":\"true\"}}},\"inputParams\":[],\"outputParams\":[{\"field\":\"result\",\"name\":\"执行结果\",\"type\":\"string\"}],\"width\":332,\"height\":158}}],\"edges\":[{\"id\":\"271482116671156224\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271480115023458304\",\"sourceAnchorId\":\"271481764802605056_case_else\",\"targetAnchorId\":\"271480115023458304_input\",\"pointsList\":[{\"x\":1020,\"y\":503},{\"x\":1120,\"y\":503},{\"x\":1106,\"y\":782},{\"x\":1206,\"y\":782}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548872990916608\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548210211192832\",\"targetNodeId\":\"271548872986722304\",\"sourceAnchorId\":\"271548210211192832_output\",\"targetAnchorId\":\"271548872986722304_input\",\"pointsList\":[{\"x\":2620,\"y\":374},{\"x\":2720,\"y\":374},{\"x\":2563,\"y\":605},{\"x\":2663,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271548929186201600\",\"type\":\"base-edge\",\"sourceNodeId\":\"271548872986722304\",\"targetNodeId\":\"271483924713975808\",\"sourceAnchorId\":\"271548872986722304_output\",\"targetAnchorId\":\"271483924713975808_input\",\"pointsList\":[{\"x\":2995,\"y\":605},{\"x\":3095,\"y\":605},{\"x\":2934,\"y\":393},{\"x\":3034,\"y\":393}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554566416482304\",\"type\":\"base-edge\",\"sourceNodeId\":\"start-node\",\"targetNodeId\":\"271554566412288000\",\"sourceAnchorId\":\"start-node_output\",\"targetAnchorId\":\"271554566412288000_input\",\"pointsList\":[{\"x\":-31,\"y\":494},{\"x\":69,\"y\":494},{\"x\":-78,\"y\":391},{\"x\":22,\"y\":391}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554605561921536\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554566412288000_source_if\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":354,\"y\":425},{\"x\":454,\"y\":425},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271554741260238848\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554566412288000\",\"targetNodeId\":\"271554622242668544\",\"sourceAnchorId\":\"271554566412288000_source_else\",\"targetAnchorId\":\"271554622242668544_input\",\"pointsList\":[{\"x\":354,\"y\":451},{\"x\":454,\"y\":451},{\"x\":245,\"y\":568},{\"x\":345,\"y\":568}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271555105874907136\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271481764802605056\",\"sourceAnchorId\":\"271554622242668544_case_else\",\"targetAnchorId\":\"271481764802605056_input\",\"pointsList\":[{\"x\":677,\"y\":646},{\"x\":777,\"y\":646},{\"x\":588,\"y\":425},{\"x\":688,\"y\":425}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271557184173555712\",\"type\":\"base-edge\",\"sourceNodeId\":\"271554622242668544\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271554622242668544_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":677,\"y\":602},{\"x\":777,\"y\":602},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"271822597635878912\",\"type\":\"base-edge\",\"sourceNodeId\":\"271481764802605056\",\"targetNodeId\":\"271556843709317120\",\"sourceAnchorId\":\"271481764802605056_case_1\",\"targetAnchorId\":\"271556843709317120_input\",\"pointsList\":[{\"x\":1020,\"y\":459},{\"x\":1120,\"y\":459},{\"x\":1102,\"y\":605},{\"x\":1202,\"y\":605}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"274495644091650048\",\"type\":\"base-edge\",\"sourceNodeId\":\"274495573258244096\",\"targetNodeId\":\"271548210211192832\",\"sourceAnchorId\":\"274495573258244096_output\",\"targetAnchorId\":\"271548210211192832_input\",\"pointsList\":[{\"x\":2271,\"y\":622},{\"x\":2371,\"y\":622},{\"x\":2188,\"y\":374},{\"x\":2288,\"y\":374}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308429452828672\",\"type\":\"base-edge\",\"sourceNodeId\":\"271556843709317120\",\"targetNodeId\":\"276308429448634368\",\"sourceAnchorId\":\"271556843709317120_output\",\"targetAnchorId\":\"276308429448634368_input\",\"pointsList\":[{\"x\":1534,\"y\":605},{\"x\":1634,\"y\":605},{\"x\":1470,\"y\":446},{\"x\":1570,\"y\":446}],\"properties\":{\"runStatus\":\"\"}},{\"id\":\"276308503712980992\",\"type\":\"base-edge\",\"sourceNodeId\":\"276308429448634368\",\"targetNodeId\":\"274495573258244096\",\"sourceAnchorId\":\"276308429448634368_output\",\"targetAnchorId\":\"274495573258244096_input\",\"pointsList\":[{\"x\":1902,\"y\":446},{\"x\":2002,\"y\":446},{\"x\":1839,\"y\":622},{\"x\":1939,\"y\":622}],\"properties\":{\"runStatus\":\"\"}}]}', 'enable', '{\"outputs\":[{\"customValue\":\"\",\"field\":\"index\",\"name\":\"d\",\"nodeId\":\"271481764802605056\",\"type\":\"number\"},{\"customValue\":\"\",\"field\":\"text\",\"name\":\"回复\",\"nodeId\":\"271548210211192832\",\"type\":\"string\"}],\"inputs\":[{\"field\":\"content\",\"name\":\"用户问题\",\"required\":false,\"type\":\"string\"},{\"field\":\"history\",\"name\":\"历史记录\",\"required\":false,\"type\":\"string[]\"},{\"field\":\"images\",\"name\":\"图片\",\"required\":false,\"type\":\"picture\"}]}', ''); + +-- 【QQYUN-15047】给MCP加上权限校验 +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038955112641368065', '1980223355087781889', '导入MCP配置', NULL, NULL, 0, NULL, NULL, 2, 'airag:mcp:import', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-31 20:22:35', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038955050674720770', '1980223355087781889', '导出MCP配置', NULL, NULL, 0, NULL, NULL, 2, 'airag:mcp:export', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-31 20:22:21', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038954985553956865', '1980223355087781889', '通过ID查询MCP', NULL, NULL, 0, NULL, NULL, 2, 'airag:mcp:queryById', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-31 20:22:05', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038951128904011777', '1980223355087781889', '删除MCP配置', NULL, NULL, 0, NULL, NULL, 2, 'airag:mcp:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-31 20:06:46', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038951018182774785', '1980223355087781889', '保存MCP配置', NULL, NULL, 0, NULL, NULL, 2, 'airag:mcp:save', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-31 20:06:19', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('2038950835315314689', '1980223355087781889', '查询MCP列表', NULL, NULL, 0, NULL, NULL, 2, 'airag:mcp:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-03-31 20:05:36', 'admin', '2026-03-31 20:06:27', 0, 0, '1', 0); +UPDATE `sys_permission` SET `is_leaf` = 0 WHERE `id` = '1980223355087781889'; + + -- Issue #9503: 网关路由 - 批量更新路由按钮 + INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, + `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES + ('2041551000000000001', '1439399179791409153', '批量更新路由', NULL, NULL, 0, NULL, NULL, 2, 'system:gateway:updateAll', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0); + + -- Issue #9509: 对象存储 - list/delete 按钮 + INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, + `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES + ('2041551000000000002', '1442055284830769154', '查询OSS列表', NULL, NULL, 0, NULL, NULL, 2, 'system:ossFile:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000003', '1442055284830769154', '删除OSS文件', NULL, NULL, 0, NULL, NULL, 2, 'system:ossFile:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0); + + -- Issue #9508: 通知公告 - 更新父节点 + 插入 11 个按钮权限 + UPDATE `sys_permission` SET `is_leaf` = 0 WHERE `id` = '1438782851980210178'; + + INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, + `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES + ('2041551000000000005', '1438782851980210178', '通告列表查询', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000006', '1438782851980210178', '新增通告', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000007', '1438782851980210178', '编辑通告', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000008', '1438782851980210178', '置顶通告', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:editIzTop', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000009', '1438782851980210178', '删除通告', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000010', '1438782851980210178', '批量删除通告', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000012', '1438782851980210178', '发布通告', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:doReleaseData', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000013', '1438782851980210178', '撤销通告', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:doReovkeData', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000014', '1438782851980210178', '通告导出', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000015', '1438782851980210178', '通告导入', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:importExcel', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0), + ('2041551000000000016', '1438782851980210178', '同步消息通知', NULL, NULL, 0, NULL, NULL, 2, 'system:sysAnnouncement:syncNotic', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-04-07 00:00:00', 'admin', '2026-04-07 00:00:00', 0, 0, '1', 0); + +-- 创建知识库时,可以创建一个分段策略,知识库里面的文档默认使用知识库的分段策略 +ALTER TABLE `airag_knowledge` + ADD COLUMN `metadata` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '元数据' AFTER `type`; + +-- 【#9496】全量同步N+1查询性能优化,sys_third_account 补充复合索引消除全表扫描 +CREATE INDEX idx_sta_sys_user_id_third_type ON sys_third_account (sys_user_id, third_type); + +-- 【PR/9083】OpenAPI白名单字段扩容,支持更多IP/CIDR条目,新增备注字段 -- +ALTER TABLE `open_api` + MODIFY COLUMN `white_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT 'IP白名单,支持IP、CIDR、通配符,逗号或换行分隔'; + +ALTER TABLE `open_api` + ADD COLUMN `comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '白名单备注说明' AFTER `white_list`; + + + +-- Online图表功能 +DROP TABLE IF EXISTS `onl_graphreport_head`; +CREATE TABLE `onl_graphreport_head` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'id', + `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图表名称', + `code` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '图表编码', + `cgr_sql` varchar(5000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '查询数据SQL', + `xaxis_field` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT 'X轴数据字段', + `yaxis_field` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT 'Y轴数据字段', + `yaxis_text` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT 'y轴文字描述', + `content` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述', + `extend_js` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '扩展JS', + `graph_type` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图表类型', + `is_combination` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'combination' COMMENT '是否组合', + `display_template` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '展示模板', + `data_type` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据类型', + `db_source` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '动态数据源', + `tenant_id` int(11) NULL DEFAULT 0 COMMENT '租户ID', + `low_app_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '关联的应用ID', + `create_time` datetime NULL DEFAULT NULL, + `create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` datetime NULL DEFAULT NULL, + `update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uniq_gpreport_code`(`code`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of onl_graphreport_head +-- ---------------------------- +INSERT INTO `onl_graphreport_head` VALUES ('0dbeb0dfc3ec18f0ce8d1d0caeeb6095', '统计近十日的登陆次数', 'login_count_charts', 'SELECT\n count(*) num,\n DATE_FORMAT(create_time, \'%Y-%m-%d\') AS `day`\nFROM\n sys_log\nWHERE\n log_type = 1\nAND create_time > DATE_SUB(NOW(), INTERVAL 10 DAY)\nGROUP BY\n DATE_FORMAT(create_time, \'%Y-%m-%d\')', 'day', 'num', '登陆次数', '统计登陆', '', 'line,pie,bar,table', 'combination', 'tab', 'sql', '', 0, NULL, '2019-04-11 14:36:30', 'admin', '2020-06-10 19:08:45', 'admin'); +INSERT INTO `onl_graphreport_head` VALUES ('1290934362649460737', '统计男女比例', 'tj_user_bysex', 'select count(*) cout, sex from sys_user group by sex', 'sex', 'cout', 'yaxis_text', NULL, NULL, 'line,bar', 'combination', 'tab', 'sql', '', 0, NULL, '2020-08-05 16:55:11', 'admin', '2020-08-05 17:03:06', 'admin'); +INSERT INTO `onl_graphreport_head` VALUES ('1306860129020305409', 'online图表API示例', 'onlapihtp', 'http://api.ghb.com/mock/308/graphreport/apitest', 'sex', 'cnt', 'yaxis_text', NULL, NULL, 'bar,line', 'combination', 'tab', 'api', '', 0, NULL, '2020-09-18 15:38:30', 'admin', '2020-09-21 11:06:33', 'admin'); +INSERT INTO `onl_graphreport_head` VALUES ('1468489236388327426', '测试vue3图表', 'ceshi_vue3', 'select log_type,count(*) num from sys_log GROUP BY log_type', 'log_type', 'num', 'yaxis_text', NULL, NULL, 'bar,line,pie,table', 'combination', 'tab', 'sql', '', 0, NULL, '2021-12-08 15:54:52', 'admin', '2026-04-28 17:10:21', 'admin'); +INSERT INTO `onl_graphreport_head` VALUES ('1469195368186544129', '统计工单', 'ccapp_issue', 'select sex,count(1) c from sys_user group by sex', 'sex', 'c', 'yaxis_text', NULL, NULL, 'bar,line,pie,table', 'combination', 'tab', 'sql', '', 0, '1469192181337587714', '2021-12-10 14:40:47', 'ghb', '2026-04-28 17:10:15', 'admin'); +INSERT INTO `onl_graphreport_head` VALUES ('3a84e175265289e1abff36be3c9f0e4a', '统计一周内步数(JS增强示例)', 'week_count_step', '[\n {\"day\": \"星期一\", \"step\": 1234, \"assess\": \"良\"},\n {\"day\": \"星期二\", \"step\": 1884, \"assess\": \"优\"},\n {\"day\": \"星期三\", \"step\": 1671, \"assess\": \"良+\"},\n {\"day\": \"星期四\", \"step\": 2197, \"assess\": \"优+\"},\n {\"day\": \"星期五\", \"step\": 1342, \"assess\": \"中\"},\n {\"day\": \"星期六\", \"step\": 545, \"assess\": \"差\"},\n {\"day\": \"星期日\", \"step\": 244, \"assess\": \"极差\"}\n]', 'day', 'step', '步数', NULL, 'onClick.bar = function (event) {\n\n var x = event.xField\n var y = event.yField\n var value = event.value\n\n // 带值跳转\n // this.$router.push(\"/isystem/user?value=\" + value)\n \n this.$info({\n title: \"点击了柱状图\",\n content: \"X轴:\" + x + \";Y轴:\" + y + \";值:\" + value\n })\n}', 'bar,line,pie,table', 'combination', 'single', 'json', NULL, 0, NULL, '2019-04-24 15:32:24', 'admin', '2020-03-24 18:43:42', 'admin'); +INSERT INTO `onl_graphreport_head` VALUES ('d2bbe1cec4260fe2d4d9e8536fa92ab8', '项目性质收入统计JSON', 'project_statistics', '[\n {\n \"column1\": \"市场化-电商业务\",\n \"column2\": 4865.41,\n \"column3\": 0,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 0,\n \"column8\": 4865.41\n },\n {\n \"column1\": \"统筹型\",\n \"column2\": 35767081.88,\n \"column3\": 0,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 0,\n \"column8\": 35767081.88\n },\n {\n \"column1\": \"市场化-非股东\",\n \"column2\": 1487045.35,\n \"column3\": 0,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 0,\n \"column8\": 1487045.35\n },\n {\n \"column1\": \"市场化-参控股\",\n \"column2\": 382690.56,\n \"column3\": 0,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 0,\n \"column8\": 382690.56\n },\n {\n \"column1\": \"市场化-员工福利\",\n \"column2\": 256684.91,\n \"column3\": 0,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 0,\n \"column8\": 265684.91\n },\n {\n \"column1\": \"市场化-再保险\",\n \"column2\": 563451.03,\n \"column3\": 0,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 0,\n \"column8\": 563451.03\n },\n {\n \"column1\": \"市场化-海外业务\",\n \"column2\": 760576.25,\n \"column3\": 770458.75,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 0,\n \"column8\": 1531035.00\n },\n {\n \"column1\": \"市场化-风险咨询\",\n \"column2\": 0.00,\n \"column3\": 910183.93,\n \"column4\": 0,\n \"column5\": 0,\n \"column6\": 0,\n \"column7\": 226415.09,\n \"column8\": 1136599.02\n }\n]', 'column1', 'column8', '总计', NULL, NULL, 'pie,bar,line,table', 'combination', 'double', 'json', NULL, 0, NULL, '2019-04-23 16:57:14', 'admin', '2019-04-23 16:57:51', 'admin'); + +-- ---------------------------- +-- Table structure for onl_graphreport_item +-- ---------------------------- +DROP TABLE IF EXISTS `onl_graphreport_item`; +CREATE TABLE `onl_graphreport_item` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'id', + `graphreport_head_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主表ID', + `field_name` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '字段名', + `field_txt` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '字段文本', + `is_show` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否列表显示', + `is_total` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否计算总计(仅对数值有效)', + `search_flag` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否查询', + `search_mode` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '查询模式', + `dict_code` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '字典Code', + `field_href` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '字段href', + `field_type` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '字段类型', + `order_num` int(11) NULL DEFAULT NULL COMMENT '排序', + `replace_val` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '取值表达式', + `create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人', + `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_ogi_graphreport_head_id`(`graphreport_head_id`) USING BTREE, + INDEX `idx_ogi_is_show`(`is_show`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'jform_graphreport_item' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of onl_graphreport_item +-- ---------------------------- +INSERT INTO `onl_graphreport_item` VALUES ('1290934166687383554', '1290934362649460737', 'cout', '人数', 'Y', 'N', 'N', NULL, '', NULL, 'String', 1, NULL, 'admin', '2020-08-05 17:03:06', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('1290934166687383555', '1290934362649460737', 'sex', '性别', 'Y', 'N', 'N', NULL, 'sex', NULL, 'String', 2, NULL, 'admin', '2020-08-05 17:03:06', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('1468489130016583681', '1468489236388327426', 'log_type', 'log_type', 'Y', NULL, NULL, NULL, NULL, NULL, 'String', 0, NULL, 'admin', '2021-12-08 15:54:52', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('1468489130016583682', '1468489236388327426', 'num', 'num', 'Y', NULL, NULL, NULL, NULL, NULL, 'String', 1, NULL, 'admin', '2021-12-08 15:54:52', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('1469195230038753282', '1469195368186544129', 'sex', '性别', 'Y', 'N', 'Y', NULL, 'sex', NULL, 'String', 0, NULL, 'ghb', '2021-12-13 19:20:12', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('1469195257331089411', '1469195368186544129', 'c', '人数', 'Y', 'N', 'N', NULL, '', NULL, 'String', 1, NULL, 'ghb', '2021-12-13 19:20:12', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15549645263910', '0dbeb0dfc3ec18f0ce8d1d0caeeb6095', 'day', '日期', 'Y', 'N', 'Y', 'group', '', NULL, 'Date', 1, NULL, 'admin', '2020-06-10 19:08:45', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15549645793241', '0dbeb0dfc3ec18f0ce8d1d0caeeb6095', 'num', '登陆次数', 'Y', 'Y', 'Y', 'group', '', NULL, 'Integer', 2, NULL, 'admin', '2020-06-10 19:08:45', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560092997490', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column1', '项目性质', 'Y', 'N', 'N', NULL, '', NULL, 'String', 1, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560094786891', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column2', '保险经纪佣金费', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 2, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560094789692', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column3', '风险咨询费', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 3, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560094791553', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column4', '承保公估评估费', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 4, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560094793404', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column5', '保险公估费', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 5, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560095450035', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column6', '投标咨询费', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 6, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560095628356', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column7', '内控咨询费', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 7, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560129810847', 'd2bbe1cec4260fe2d4d9e8536fa92ab8', 'column8', '总计', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 8, NULL, 'admin', '2019-04-23 20:01:15', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560934964380', '3a84e175265289e1abff36be3c9f0e4a', 'day', '星期', 'Y', 'N', 'N', NULL, '', NULL, 'String', 1, NULL, 'admin', '2020-03-24 18:43:42', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560934966151', '3a84e175265289e1abff36be3c9f0e4a', 'step', '步数', 'Y', 'Y', 'N', NULL, '', NULL, 'Integer', 2, NULL, 'admin', '2020-03-24 18:43:42', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('15560934968542', '3a84e175265289e1abff36be3c9f0e4a', 'assess', '评估', 'Y', 'N', 'N', NULL, '', NULL, 'String', 3, NULL, 'admin', '2020-03-24 18:43:42', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('16004146313790445268', '1306860129020305409', 'sex', '性别', 'Y', 'N', 'N', NULL, 'sex', NULL, 'String', 1, NULL, 'admin', '2020-09-21 11:06:33', NULL, NULL); +INSERT INTO `onl_graphreport_item` VALUES ('16004146953271116933', '1306860129020305409', 'cnt', '数量', 'Y', 'N', 'N', NULL, '', NULL, 'String', 2, NULL, 'admin', '2020-09-21 11:06:33', NULL, NULL); + +-- ---------------------------- +-- Table structure for onl_graphreport_params +-- ---------------------------- +DROP TABLE IF EXISTS `onl_graphreport_params`; +CREATE TABLE `onl_graphreport_params` ( + `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `head_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'Online图表ID', + `param_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '参数字段', + `param_txt` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '参数文本', + `param_value` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '参数默认值', + `order_num` int(11) NULL DEFAULT NULL COMMENT '排序', + `create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人登录名称', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建日期', + `update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新人登录名称', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新日期', + PRIMARY KEY (`id`) USING BTREE, + INDEX `onl_graphreport_param_head_id`(`head_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'Online图表:参数表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of onl_graphreport_params +-- ---------------------------- + +-- ---------------------------- +-- Table structure for onl_graphreport_templet +-- ---------------------------- +DROP TABLE IF EXISTS `onl_graphreport_templet`; +CREATE TABLE `onl_graphreport_templet` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `templet_code` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `templet_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报表名称', + `templet_style` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '报表风格模板(单排、双排、Tab模式、分组模式-根据配置动态展示、可自定义...)', + `create_time` datetime NULL DEFAULT NULL, + `create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` datetime NULL DEFAULT NULL, + `update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of onl_graphreport_templet +-- ---------------------------- +INSERT INTO `onl_graphreport_templet` VALUES ('02ff397e9714a22bf6efdfc7ba6d9041', 'test_many_source_tab', '多数据源Tab风格', 'tab', '2019-04-18 19:33:39', 'admin', '2020-10-09 14:45:42', 'admin'); +INSERT INTO `onl_graphreport_templet` VALUES ('46a14bee5780f2c0cc7a785c94a0b6a7', 'test_many_source_double', '多数据源双排风格', 'double', '2019-04-17 18:20:58', 'admin', '2019-04-17 18:21:14', 'admin'); +INSERT INTO `onl_graphreport_templet` VALUES ('bc154d35a1ec3eb4dd0f193dbecfbcb5', 'templet_combination', '多数据源组合布局', 'combination', '2019-05-11 16:18:44', 'admin', '2020-10-09 14:44:54', 'admin'); +INSERT INTO `onl_graphreport_templet` VALUES ('dcf1e8aa1745937d511743f77ecfc40a', 'test_many_source_single', '多数据源单排风格', 'single', '2019-04-18 19:34:19', 'admin', '2019-04-19 16:00:49', 'admin'); + +-- ---------------------------- +-- Table structure for onl_graphreport_templet_item +-- ---------------------------- +DROP TABLE IF EXISTS `onl_graphreport_templet_item`; +CREATE TABLE `onl_graphreport_templet_item` ( + `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `graphreport_templet_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `graphreport_code` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图表编码', + `graphreport_type` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '图表类型(饼状图、曲线图、柱状图、数据列表等)', + `group_num` int(11) NULL DEFAULT NULL COMMENT '组合数字,默认值0 非必填', + `group_style` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组合展示风格(1 卡片,2 tab)非必填', + `group_txt` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '分组描述', + `order_num` int(11) NULL DEFAULT NULL COMMENT '排序', + `is_show` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '是否显示 1显示 0不显示,默认1', + `create_time` datetime NULL DEFAULT NULL, + `create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `update_time` datetime NULL DEFAULT NULL, + `update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_ogti_grreport_tempid`(`graphreport_templet_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of onl_graphreport_templet_item +-- ---------------------------- +INSERT INTO `onl_graphreport_templet_item` VALUES ('12332331137671', '46a14bee5780f2c0cc7a785c94a0b6a7', 'week_count_step', 'bar', 0, 'card', '统计', 2, '1', '2019-04-26 19:12:37', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('12332331142472', '46a14bee5780f2c0cc7a785c94a0b6a7', 'project_statistics', 'normal', 1, 'card', '项目', 1, '1', '2019-04-26 19:12:37', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('12332332338280', '46a14bee5780f2c0cc7a785c94a0b6a7', 'login_count_charts', 'line', 0, 'card', '统计', 1, '1', '2019-04-26 19:12:37', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15552332338280', '02ff397e9714a22bf6efdfc7ba6d9041', 'login_count_charts', 'line', 0, 'card', '统计', 1, '1', '2020-10-09 14:45:42', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15555872338280', 'dcf1e8aa1745937d511743f77ecfc40a', 'login_count_charts', 'line', 0, 'tabs', '统计', 1, '1', '2019-10-04 00:39:31', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15562331137671', '02ff397e9714a22bf6efdfc7ba6d9041', 'week_count_step', 'bar', 0, 'card', '统计', 2, '1', '2020-10-09 14:45:42', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15562331142472', '02ff397e9714a22bf6efdfc7ba6d9041', 'project_statistics', 'normal', 1, 'card', '项目', 3, '1', '2020-10-09 14:45:42', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15562671137671', 'dcf1e8aa1745937d511743f77ecfc40a', 'week_count_step', 'bar', 0, 'tabs', '统计', 2, '1', '2019-10-04 00:39:31', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15562671142472', 'dcf1e8aa1745937d511743f77ecfc40a', 'project_statistics', 'normal', 1, 'card', '项目', 1, '1', '2019-10-04 00:39:31', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15575624810180', 'bc154d35a1ec3eb4dd0f193dbecfbcb5', 'login_count_charts', 'line', 0, 'card', '', 1, '1', '2020-10-09 14:44:54', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15575624830441', 'bc154d35a1ec3eb4dd0f193dbecfbcb5', 'project_statistics_sql', 'bar', 0, 'card', '', 2, '1', '2020-10-09 14:44:54', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15575712802712', 'bc154d35a1ec3eb4dd0f193dbecfbcb5', 'monthly_growth_analysis', 'bar', 1, 'card', '', 3, '1', '2020-10-09 14:44:54', 'admin', NULL, NULL); +INSERT INTO `onl_graphreport_templet_item` VALUES ('15575712972193', 'bc154d35a1ec3eb4dd0f193dbecfbcb5', 'week_count_step', 'line', 1, 'card', '', 4, '1', '2020-10-09 14:44:54', 'admin', NULL, NULL); + +SET FOREIGN_KEY_CHECKS = 1; + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1461278375076913153', '1455100420297859074', 'Online图表配置', '/online/graphreport', 'super/online/graphreport/GraphreportList', 1, NULL, NULL, 1, NULL, '0', 3.00, 0, NULL, 0, 0, 0, 0, NULL, 'admin', '2021-11-18 18:21:29', NULL, NULL, 0, 0, NULL, 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1535225603236052993', '1461278375076913153', '批量删除', NULL, NULL, 0, NULL, NULL, 2, 'online:graphreport:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-06-10 19:41:21', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1535125603236051993', '1461278375076913153', '删除', NULL, NULL, 0, NULL, NULL, 2, 'online:graphreport:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-06-10 19:41:21', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1535125603236051994', '1461278375076913153', '添加', NULL, NULL, 0, NULL, NULL, 2, 'online:graphreport:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-06-10 19:41:21', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1535125603236051995', '1461278375076913153', '修改', NULL, NULL, 0, NULL, NULL, 2, 'online:graphreport:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-06-10 19:41:21', NULL, NULL, 0, 0, '1', 0); +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1535125603236051996', '1461278375076913153', '解析字段', NULL, NULL, 0, NULL, NULL, 2, 'online:graphreport:parseField', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2022-06-10 19:41:21', NULL, NULL, 0, 0, '1', 0); \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/backup/V20250403.zip b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/backup/V20250403.zip new file mode 100644 index 0000000..aaa6197 Binary files /dev/null and b/test-module-system/test-system-start/src/main/resources/flyway/sql/mysql/backup/V20250403.zip differ diff --git a/test-module-system/test-system-start/src/main/resources/jeecg/jeecg_config.properties b/test-module-system/test-system-start/src/main/resources/jeecg/jeecg_config.properties new file mode 100644 index 0000000..493110e --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/jeecg/jeecg_config.properties @@ -0,0 +1,30 @@ +# Module path to generate in the backend Java project +project_path=F:\\gitcode\\ghbBoot\\test\\test-module\\ghb-module-demo +## Path to generate in the frontend VUE3 project +#ui_project_path=F:\\gitcode\\1test-github\\ghbboot-vue3 +# Business package path +bussi_package=com.ghb.base.modules.demo + +#default code path +#source_root_package=src +#webroot_package=WebRoot + +#maven code path +source_root_package=src.main.java +webroot_package=src.main.webapp + +#ftl resource url +templatepath=/ghb/code-template +system_encoding=utf-8 + +#db Table id [User defined] +db_table_id=id + +#db convert flag[true/false] +db_filed_convert=true + +#page Search Field num [User defined] +page_search_filed_num=1 +#page_filter_fields +page_filter_fields=create_time,create_by,update_time,update_by +exclude_table=act_,ext_act_,design_,onl_,sys_,qrtz_ diff --git a/test-module-system/test-system-start/src/main/resources/jeecg/jeecg_database.properties b/test-module-system/test-system-start/src/main/resources/jeecg/jeecg_database.properties new file mode 100644 index 0000000..9c784e5 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/jeecg/jeecg_database.properties @@ -0,0 +1,28 @@ +#mysql +diver_name=com.mysql.jdbc.Driver +url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8 +username=root +password=root +database_name=test + +#oracle +#diver_name=oracle.jdbc.driver.OracleDriver +#url=jdbc:oracle:thin:@192.168.1.200:1521:ORCL +#username=scott +#password=tiger +#database_name=ORCL + +#postgre +#diver_name=org.postgresql.Driver +#url=jdbc:postgresql://localhost:5432/ghb +#username=postgres +#password=postgres +#database_name=ghb +#schemaName=public + +#SQLServer2005\u4ee5\u4e0a +#diver_name=org.hibernate.dialect.SQLServerDialect +#url=jdbc:sqlserver://192.168.1.200:1433;DatabaseName=ghb +#username=sa +#password=SA +#database_name=ghb \ No newline at end of file diff --git a/test-module-system/test-system-start/src/main/resources/logback-spring.xml b/test-module-system/test-system-start/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..0a74964 --- /dev/null +++ b/test-module-system/test-system-start/src/main/resources/logback-spring.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n + + + + + + + + ${LOG_HOME}/test-%d{yyyy-MM-dd}.%i.log + + 30 + 10MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + + + + + + + + ERROR + + + + %p%d%msg%M%F{32}%L + + + ${LOG_HOME}/error-log.html + + + + + + + + ${LOG_HOME}/test-%d{yyyy-MM-dd}.%i.html + + 30 + 10MB + + + + %p%d%msg%M%F{32}%L + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-module-system/test-system-start/src/test/java/com/ghb/base/TestMain.java b/test-module-system/test-system-start/src/test/java/com/ghb/base/TestMain.java new file mode 100644 index 0000000..341274e --- /dev/null +++ b/test-module-system/test-system-start/src/test/java/com/ghb/base/TestMain.java @@ -0,0 +1,47 @@ +//package com.ghb.base; +// +//import com.alibaba.fastjson.JSONObject; +//import com.ghb.base.common.util.RestUtil; +//import org.springframework.http.HttpHeaders; +//import org.springframework.http.HttpMethod; +//import org.springframework.http.MediaType; +//import org.springframework.http.ResponseEntity; +// +///** +// * @Description: TODO +// * @author: scott +// * @date: 2022年05月10日 14:02 +// */ +//public class TestMain { +// public static void main(String[] args) { +// // 请求地址 +// String url = "https://api3.boot.Ghb.com/sys/user/list"; +// // 请求 Header (用于传递Token) +// HttpHeaders headers = getHeaders(); +// // 请求方式是 GET 代表获取数据 +// HttpMethod method = HttpMethod.GET; +// +// //System.out.println("请求地址:" + url); +// //System.out.println("请求方式:" + method); +// +// // 利用 RestUtil 请求该url +// ResponseEntity result = RestUtil.request(url, method, headers, null, null, JSONObject.class); +// if (result != null && result.getBody() != null) { +// System.out.println("返回结果:" + result.getBody().toJSONString()); +// } else { +// System.out.println("查询失败"); +// } +// } +// private static HttpHeaders getHeaders() { +// String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.50h-g6INOZRVnznExiawFb1U6PPjcVVA4POeYRA5a5Q"; +// System.out.println("请求Token:" + token); +// +// HttpHeaders headers = new HttpHeaders(); +// String mediaType = MediaType.APPLICATION_JSON_VALUE; +// headers.setContentType(MediaType.parseMediaType(mediaType)); +// headers.set("Accept", mediaType); +// headers.set("X-Access-Token", token); +// return headers; +// } +// +//} diff --git a/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/message/test/SendMessageTest.java b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/message/test/SendMessageTest.java new file mode 100644 index 0000000..718942d --- /dev/null +++ b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/message/test/SendMessageTest.java @@ -0,0 +1,136 @@ +//package com.ghb.base.modules.message.test; +// +//import com.alibaba.fastjson.JSONObject; +//import com.aliyuncs.exceptions.ClientException; +//import com.ghb.base.GhbSystemApplication; +//import com.ghb.base.common.api.dto.message.BusMessageDTO; +//import com.ghb.base.common.api.dto.message.BusTemplateMessageDTO; +//import com.ghb.base.common.api.dto.message.MessageDTO; +//import com.ghb.base.common.api.dto.message.TemplateMessageDTO; +//import com.ghb.base.common.constant.CommonConstant; +//import com.ghb.base.common.constant.enums.DySmsEnum; +//import com.ghb.base.common.constant.enums.EmailTemplateEnum; +//import com.ghb.base.common.constant.enums.MessageTypeEnum; +//import com.ghb.base.common.constant.enums.SysAnnmentTypeEnum; +//import com.ghb.base.common.system.api.ISysBaseAPI; +//import com.ghb.base.common.util.DySmsHelper; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +// +//import java.util.HashMap; +//import java.util.Map; +// +///** +// * @Description: 消息推送测试 +// * @Author: lsq +// */ +//@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = GhbSystemApplication.class) +//public class SendMessageTest { +// +// @Autowired +// ISysBaseAPI sysBaseAPI; +// +// /** +// * 发送系统消息 +// */ +// @Test +// public void sendSysAnnouncement() { +// //发送人 +// String fromUser = "admin"; +// //接收人 +// String toUser = "Ghb"; +// //标题 +// String title = "系统消息"; +// //内容 +// String msgContent = "TEST:今日份日程计划已送达!"; +// //发送系统消息 +// sysBaseAPI.sendSysAnnouncement(new MessageDTO(fromUser, toUser, title, msgContent)); +// //消息类型 +// String msgCategory = CommonConstant.MSG_CATEGORY_1; +// //业务类型 +// String busType = SysAnnmentTypeEnum.EMAIL.getType(); +// //业务ID +// String busId = "11111"; +// //发送带业务参数的系统消息 +// BusMessageDTO busMessageDTO = new BusMessageDTO(fromUser, toUser, title, msgContent, msgCategory, busType,busId); +// sysBaseAPI.sendBusAnnouncement(busMessageDTO); +// } +// +// /** +// * 发送模版消息 +// */ +// @Test +// public void sendTemplateAnnouncement() { +// //发送人 +// String fromUser = "admin"; +// //接收人 +// String toUser = "Ghb"; +// //标题 +// String title = "通知公告"; +// //模版编码 +// String templateCode = "412358"; +// //模版参数 +// Map templateParam = new HashMap<>(); +// templateParam.put("realname","Ghb用户"); +// sysBaseAPI.sendTemplateAnnouncement(new TemplateMessageDTO(fromUser,toUser,title,templateParam,templateCode)); +// //业务类型 +// String busType = SysAnnmentTypeEnum.EMAIL.getType(); +// //业务ID +// String busId = "11111"; +// //发送带业务参数的模版消息 +// BusTemplateMessageDTO busMessageDTO = new BusTemplateMessageDTO(fromUser, toUser, title, templateParam ,templateCode, busType,busId); +// sysBaseAPI.sendBusTemplateAnnouncement(busMessageDTO); +// //新发送模版消息 +// MessageDTO messageDTO = new MessageDTO(); +// messageDTO.setType(MessageTypeEnum.XT.getType()); +// messageDTO.setToAll(false); +// messageDTO.setToUser(toUser); +// messageDTO.setTitle("【流程错误】"); +// messageDTO.setFromUser("admin"); +// HashMap data = new HashMap<>(); +// data.put(CommonConstant.NOTICE_MSG_BUS_TYPE, "msg_node"); +// messageDTO.setData(data); +// messageDTO.setContent("TEST:流程执行失败!任务节点未找到"); +// sysBaseAPI.sendTemplateMessage(messageDTO); +// } +// /** +// * 发送邮件 +// */ +// @Test +// public void sendEmailMsg() { +// String title = "【日程提醒】您的日程任务即将开始"; +// String content = "TEST:尊敬的王先生,您购买的演唱会将于本周日10:08分在国家大剧院如期举行,届时请携带好您的门票和身份证到场"; +// String email = "250678106@qq.com"; +// sysBaseAPI.sendEmailMsg(email,title,content); +// } +// /** +// * 发送html模版邮件 +// */ +// @Test +// public void sendTemplateEmailMsg() { +// String title = "收到一个催办"; +// String email = "250678106@qq.com"; +// JSONObject params = new JSONObject(); +// params.put("bpm_name","高级设置"); +// params.put("bpm_task","审批人"); +// params.put("datetime","2023-10-07 18:00:49"); +// params.put("url","http://boot3.Ghb.com/message/template"); +// params.put("remark","快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点快点"); +// sysBaseAPI.sendHtmlTemplateEmail(email,title, EmailTemplateEnum.BPM_CUIBAN_EMAIL,params); +// } +// /** +// * 发送短信 +// */ +// @Test +// public void sendSms() throws ClientException { +// //手机号 +// String mobile = "159***"; +// //消息模版 +// DySmsEnum templateCode = DySmsEnum.LOGIN_TEMPLATE_CODE; +// //模版所需参数 +// JSONObject obj = new JSONObject(); +// obj.put("code", "4XDP"); +// DySmsHelper.sendSms(mobile, obj, templateCode); +// } +//} diff --git a/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/openapi/test/SampleOpenApiTest.java b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/openapi/test/SampleOpenApiTest.java new file mode 100644 index 0000000..5582e4d --- /dev/null +++ b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/openapi/test/SampleOpenApiTest.java @@ -0,0 +1,97 @@ +//package com.ghb.base.modules.openapi.test; +// +//import com.alibaba.fastjson.JSON; +//import com.alibaba.fastjson.JSONObject; +//import org.apache.http.HttpEntity; +//import org.apache.http.client.methods.CloseableHttpResponse; +//import org.apache.http.client.methods.HttpGet; +//import org.apache.http.impl.client.CloseableHttpClient; +//import org.apache.http.impl.client.HttpClients; +//import org.apache.http.util.EntityUtils; +//import org.junit.jupiter.api.Test; +// +//import java.security.MessageDigest; +// +// +//public class SampleOpenApiTest { +// private final String base_url = "http://localhost:8080/Ghb-boot"; +// private final String appKey = "ak-pFjyNHWRsJEFWlu6"; +// private final String searchKey = "4hV5dBrZtmGAtPdbA5yseaeKRYNpzGsS"; +// +// @Test +// public void test() throws Exception { +// // 根据部门ID查询用户 +// String url = base_url+"/openapi/call/TEwcXBlr?id=c6d7cb4deeac411cb3384b1b31278596"; +// JSONObject header = genTimestampAndSignature(); +// HttpGet httpGet = new HttpGet(url); +// // 设置请求头 +// httpGet.setHeader("Content-Type", "application/json"); +// httpGet.setHeader("appkey",appKey); +// httpGet.setHeader("signature",header.get("signature").toString()); +// httpGet.setHeader("timestamp",header.get("timestamp").toString()); +// try (CloseableHttpClient httpClient = HttpClients.createDefault(); +// CloseableHttpResponse response = httpClient.execute(httpGet);) { +// // 获取响应状态码 +// int statusCode = response.getStatusLine().getStatusCode(); +// System.out.println("[debug] 响应状态码: " + statusCode); +// +// HttpEntity entity = response.getEntity(); +// System.out.println(entity); +// // 获取响应内容 +// String responseBody = EntityUtils.toString(response.getEntity()); +// System.out.println("[debug] 响应内容: " + responseBody); +// +// // 解析JSON响应 +// JSONObject res = JSON.parseObject(responseBody); +// //错误日志判断 +// if(res.containsKey("success")){ +// Boolean success = res.getBoolean("success"); +// if(success){ +// System.out.println("[info] 调用成功: " + res.toJSONString()); +// }else{ +// System.out.println("[error] 调用失败: " + res.getString("message")); +// } +// }else{ +// System.out.println("[error] 调用失败: " + res.getString("message")); +// } +// } +// +// } +// private JSONObject genTimestampAndSignature(){ +// JSONObject jsonObject = new JSONObject(); +// long timestamp = System.currentTimeMillis(); +// jsonObject.put("timestamp",timestamp); +// jsonObject.put("signature", md5(appKey + searchKey + timestamp)); +// return jsonObject; +// } +// +// /** +// * 生成md5 +// * @param sourceStr +// * @return +// */ +// protected String md5(String sourceStr) { +// String result = ""; +// try { +// MessageDigest md = MessageDigest.getInstance("MD5"); +// md.update(sourceStr.getBytes("utf-8")); +// byte[] hash = md.digest(); +// int i; +// StringBuffer buf = new StringBuffer(32); +// for (int offset = 0; offset < hash.length; offset++) { +// i = hash[offset]; +// if (i < 0) { +// i += 256; +// } +// if (i < 16) { +// buf.append("0"); +// } +// buf.append(Integer.toHexString(i)); +// } +// result = buf.toString(); +// } catch (Exception e) { +// throw new RuntimeException("sign签名错误", e); +// } +// return result; +// } +//} diff --git a/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/system/test/SysTableWhiteCheckTest.java b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/system/test/SysTableWhiteCheckTest.java new file mode 100644 index 0000000..9bfa739 --- /dev/null +++ b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/system/test/SysTableWhiteCheckTest.java @@ -0,0 +1,87 @@ +//package com.ghb.base.modules.system.test; +// +//import org.aspectj.lang.annotation.Before; +//import com.ghb.base.GhbSystemApplication; +//import com.ghb.base.common.system.api.ISysBaseAPI; +//import com.ghb.base.config.GhbBaseConfig; +//import com.ghb.base.config.firewall.SqlInjection.IDictTableWhiteListHandler; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +// +///** +// * @Description: 系统表白名单测试 +// * @Author: sunjianlei +// */ +//@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = GhbSystemApplication.class) +//public class SysTableWhiteCheckTest { +// +// @Autowired +// IDictTableWhiteListHandler whiteListHandler; +// @Autowired +// ISysBaseAPI sysBaseAPI; +// +// @Autowired +// GhbBaseConfig GhbBaseConfig; +// +// @BeforeEach +// public void before() { +// String lowCodeMode = this.GhbBaseConfig.getFirewall().getLowCodeMode(); +// System.out.println("当前 LowCode 模式为: " + lowCodeMode); +// // 清空缓存,防止影响测试 +// whiteListHandler.clear(); +// } +// +// @Test +// public void testSql() { +// System.out.println("=== 开始测试 SQL 方式 ==="); +// String[] sqlArr = new String[]{ +// "select username from sys_user", +// "select username, CONCAT(realname, SEX) from SYS_USER", +// "select username, CONCAT(realname, sex) from sys_user", +// }; +// for (String sql : sqlArr) { +// System.out.println("- 测试Sql: " + sql); +// try { +// sysBaseAPI.dictTableWhiteListCheckBySql(sql); +// System.out.println("-- 测试通过"); +// } catch (Exception e) { +// System.out.println("-- 测试未通过: " + e.getMessage()); +// } +// } +// System.out.println("=== 结束测试 SQL 方式 ==="); +// } +// +// @Test +// public void testDict() { +// System.out.println("=== 开始测试 DICT 方式 ==="); +// +// String table = "sys_user"; +// String code = "username"; +// String text = "realname"; +// this.testDict(table, code, text); +// +// table = "sys_user"; +// code = "username"; +// text = "CONCAT(realname, sex)"; +// this.testDict(table, code, text); +// +// table = "SYS_USER"; +// code = "username"; +// text = "CONCAT(realname, SEX)"; +// this.testDict(table, code, text); +// +// System.out.println("=== 结束测试 DICT 方式 ==="); +// } +// +// private void testDict(String table, String code, String text) { +// try { +// sysBaseAPI.dictTableWhiteListCheckByDict(table, code, text); +// System.out.println("- 测试通过"); +// } catch (Exception e) { +// System.out.println("- 测试未通过: " + e.getMessage()); +// } +// } +// +//} diff --git a/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/system/test/SysUserApiTest.java b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/system/test/SysUserApiTest.java new file mode 100644 index 0000000..f843616 --- /dev/null +++ b/test-module-system/test-system-start/src/test/java/com/ghb/base/modules/system/test/SysUserApiTest.java @@ -0,0 +1,174 @@ +//package com.ghb.base.modules.system.test; +// +//import com.alibaba.fastjson.JSON; +//import com.alibaba.fastjson.JSONObject; +//import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +//import com.ghb.base.common.api.vo.Result; +//import org.jeecg.common.modules.redis.client.JeecgRedisClient; +//import org.jeecg.common.util.RedisUtil; +//import com.ghb.base.config.GhbBaseConfig; +//import com.ghb.base.modules.base.service.BaseCommonService; +//import com.ghb.base.modules.system.controller.SysUserController; +//import com.ghb.base.modules.system.entity.SysUser; +//import com.ghb.base.modules.system.service.*; +//import org.junit.jupiter.api.Assertions; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +//import org.springframework.boot.test.mock.mockito.MockBean; +//import org.springframework.http.MediaType; +//import org.springframework.test.web.servlet.MockMvc; +// +//import java.util.ArrayList; +//import java.util.List; +// +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.BDDMockito.given; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +// +///** +// * 系统用户单元测试 +// */ +//@WebMvcTest(SysUserController.class) +//public class SysUserApiTest { +// +// @Autowired +// private MockMvc mockMvc; +// +// @MockBean +// private ISysUserService sysUserService; +// +// @MockBean +// private ISysDepartService sysDepartService; +// +// @MockBean +// private ISysUserRoleService sysUserRoleService; +// +// @MockBean +// private ISysUserDepartService sysUserDepartService; +// +// @MockBean +// private ISysDepartRoleUserService departRoleUserService; +// +// @MockBean +// private ISysDepartRoleService departRoleService; +// +// @MockBean +// private RedisUtil redisUtil; +// +// @Value("${ghb.path.upload}") +// private String upLoadPath; +// +// @MockBean +// private BaseCommonService baseCommonService; +// +// @MockBean +// private ISysPositionService sysPositionService; +// +// @MockBean +// private ISysUserTenantService userTenantService; +// +// @MockBean +// private JeecgRedisClient JeecgRedisClient; +// +// @MockBean +// private GhbBaseConfig GhbBaseConfig; +// /** +// * 测试地址:实际使用时替换成你自己的地址 +// */ +// private final String BASE_URL = "/sys/user/"; +// +// /** +// * 测试用例:查询记录 +// */ +// @Test +// public void testQuery() throws Exception{ +// // 请求地址 +// String url = BASE_URL + "list"; +// +// Page sysUserPage = new Page<>(); +// SysUser sysUser = new SysUser(); +// sysUser.setUsername("admin"); +// List users = new ArrayList<>(); +// users.add(sysUser); +// sysUserPage.setRecords(users); +// sysUserPage.setCurrent(1); +// sysUserPage.setSize(10); +// sysUserPage.setTotal(1); +// +// given(this.sysUserService.queryPageList(any(), any(), any(), any())).willReturn(Result.OK(sysUserPage)); +// +// String result = mockMvc.perform(get(url)).andReturn().getResponse().getContentAsString(); +// JSONObject jsonObject = JSON.parseObject(result); +// Assertions.assertEquals("admin", jsonObject.getJSONObject("result").getJSONArray("records").getJSONObject(0).getString("username")); +// } +// +// /** +// * 测试用例:新增 +// */ +// @Test +// public void testAdd() throws Exception { +// // 请求地址 +// String url = BASE_URL + "add" ; +// +// JSONObject params = new JSONObject(); +// params.put("username", "wangwuTest"); +// params.put("password", "123456"); +// params.put("confirmpassword","123456"); +// params.put("realname", "单元测试"); +// params.put("activitiSync", "1"); +// params.put("userIdentity","1"); +// params.put("workNo","0025"); +// +// String result = mockMvc.perform(post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(params.toJSONString())) +// .andReturn().getResponse().getContentAsString(); +// JSONObject jsonObject = JSON.parseObject(result); +// Assertions.assertTrue(jsonObject.getBoolean("success")); +// } +// +// +// /** +// * 测试用例:修改 +// */ +// @Test +// public void testEdit() throws Exception { +// // 数据Id +// String dataId = "1331795062924374018"; +// // 请求地址 +// String url = BASE_URL + "edit"; +// +// JSONObject params = new JSONObject(); +// params.put("username", "wangwuTest"); +// params.put("realname", "单元测试1111"); +// params.put("activitiSync", "1"); +// params.put("userIdentity","1"); +// params.put("workNo","0025"); +// params.put("id",dataId); +// +// SysUser sysUser = new SysUser(); +// sysUser.setUsername("admin"); +// +// given(this.sysUserService.getById(any())).willReturn(sysUser); +// +// String result = mockMvc.perform(put(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(params.toJSONString())) +// .andReturn().getResponse().getContentAsString(); +// JSONObject jsonObject = JSON.parseObject(result); +// Assertions.assertTrue(jsonObject.getBoolean("success")); +// } +// +// +// /** +// * 测试用例:删除 +// */ +// @Test +// public void testDelete() throws Exception { +// // 数据Id +// String dataId = "1331795062924374018"; +// // 请求地址 +// String url = BASE_URL + "delete" + "?id=" + dataId; +// String result = mockMvc.perform(delete(url)).andReturn().getResponse().getContentAsString(); +// JSONObject jsonObject = JSON.parseObject(result); +// Assertions.assertTrue(jsonObject.getBoolean("success")); +// } +//} diff --git a/test-module-system/test-system-start/src/test/java/com/ghb/base/smallTools/TestSqlHandle.java b/test-module-system/test-system-start/src/test/java/com/ghb/base/smallTools/TestSqlHandle.java new file mode 100644 index 0000000..149ed4e --- /dev/null +++ b/test-module-system/test-system-start/src/test/java/com/ghb/base/smallTools/TestSqlHandle.java @@ -0,0 +1,40 @@ +//package com.ghb.base.smallTools; +// +// +//import org.junit.jupiter.api.Test; +// +///** +// * 测试sql分割、替换等操作 +// * +// * @author: scott +// * @date: 2023年09月05日 16:13 +// */ +//public class TestSqlHandle { +// +// /** +// * Where 分割测试 +// */ +// @Test +// public void testSqlSplitWhere() { +// String tableFilterSql = " select * from data.sys_user Where name='12312' and age>100"; +// String[] arr = tableFilterSql.split(" (?i)where "); +// for (String sql : arr) { +// System.out.println("sql片段:" + sql); +// } +// } +// +// +// /** +// * Where 替换 +// */ +// @Test +// public void testSqlWhereReplace() { +// String input = " Where name='12312' and age>100"; +// String pattern = "(?i)where "; // (?i) 表示不区分大小写 +// +// String replacedString = input.replaceAll(pattern, ""); +// +// System.out.println("替换前的字符串:" + input); +// System.out.println("替换后的字符串:" + replacedString); +// } +//} diff --git a/test-module-system/test-system-start/src/test/java/com/ghb/base/smallTools/TestStr.java b/test-module-system/test-system-start/src/test/java/com/ghb/base/smallTools/TestStr.java new file mode 100644 index 0000000..9ebd879 --- /dev/null +++ b/test-module-system/test-system-start/src/test/java/com/ghb/base/smallTools/TestStr.java @@ -0,0 +1,99 @@ +//package com.ghb.base.smallTools; +// +//import com.alibaba.fastjson.JSONArray; +//import org.apache.commons.lang3.StringUtils; +//import com.ghb.base.common.util.DateUtils; +//import org.junit.jupiter.api.Test; +// +//import java.text.MessageFormat; +//import java.time.LocalDate; +//import java.time.LocalDateTime; +//import java.time.LocalTime; +//import java.time.ZoneId; +//import java.util.Arrays; +//import java.util.Base64; +//import java.util.Date; +// +///** +// * 字符串处理测试 +// * +// * @author: scott +// * @date: 2023年03月30日 15:27 +// */ +//public class TestStr { +// +// /** +// * 测试参数格式化的问题,数字值有问题 +// */ +// @Test +// public void testParameterFormat() { +// String url = "/pages/lowApp/process/taskDetail?tenantId={0}&procInsId={1}&taskId={2}&taskDefKey={3}"; +// String cc = MessageFormat.format(url, "6364", "111", "22", "333"); +// System.out.println("参数是字符串:" + cc); +// +// String cc2 = MessageFormat.format(url, 6364, 111, 22, 333); +// System.out.println("参数是数字(出问题):" + cc2); +// } +// +// +// @Test +// public void testStringSplitError() { +// String conditionValue = "qweqwe"; +// String[] conditionValueArray = conditionValue.split(","); +// System.out.println("length = "+ conditionValueArray.length); +// Arrays.stream(conditionValueArray).forEach(System.out::println); +// } +// +// +// @Test +// public void getThisDate() { +// LocalDate d = DateUtils.getLocalDate(); +// System.out.println(d); +// } +// +// +// @Test +// public void firstDayOfLastSixMonths() { +// LocalDate today = LocalDate.now(); // 获取当前日期 +// LocalDate firstDayOfLastSixMonths = today.minusMonths(6).withDayOfMonth(1); // 获取近半年的第一天 +// LocalDateTime firstDateTime = LocalDateTime.of(firstDayOfLastSixMonths, LocalTime.MIN); // 设置时间为当天的最小时间(00:00:00) +// Date date = Date.from(firstDateTime.atZone(ZoneId.systemDefault()).toInstant()); // 将 LocalDateTime 转换为 Date +// System.out.println("近半年的第一天的 00:00:00 时间戳:" + date); +// } +// +// @Test +// public void testJSONArrayJoin() { +// JSONArray valArray = new JSONArray(); +// valArray.add("123"); +// valArray.add("qwe"); +// System.out.println("值: " + StringUtils.join(valArray, ",")); +// } +// +// @Test +// public void testSql() { +// String sql = "select * from sys_user where sex = ${sex}"; +// sql = sql.replaceAll("'?\\$\\{sex}'?","1"); +// System.out.println(sql); +// } +// +// @Test +// public void base64(){ +// String encodedString = "5L+d5a2Y5aSx6LSl77yM5YWN6LS554mI5pyA5aSa5Yib5bu6ezB95p2h6L+e5o6l77yM6K+35Y2H57qn5ZWG5Lia54mI77yB"; +// byte[] decodedBytes = Base64.getDecoder().decode(encodedString); +// String decodedString = new String(decodedBytes); +// String tipMsg = MessageFormat.format(decodedString, 10); +// System.out.println(tipMsg); +// } +// +// /** +// * 正则测试字符串只保存中文和数字和字母 +// */ +// @Test +// public void testSpecialChar() { +// String str = "Hello, World! 你好!这是一段特殊符号的测试,This is__ a test string with special characters: @#$%^&*"; +// // 使用正则表达式替换特殊字符 +// String replacedStr = str.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5]", ""); +// System.out.println("Replaced String: " + replacedStr); +// } +// +//} diff --git a/test-server-cloud/docker-compose.yml b/test-server-cloud/docker-compose.yml new file mode 100644 index 0000000..8959297 --- /dev/null +++ b/test-server-cloud/docker-compose.yml @@ -0,0 +1,135 @@ +version: '2' +services: + jeecg-boot-mysql: + build: + context: ../db + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_ROOT_HOST: '%' + TZ: Asia/Shanghai + restart: always + container_name: jeecg-boot-mysql + command: + --character-set-server=utf8mb4 + --collation-server=utf8mb4_general_ci + --explicit_defaults_for_timestamp=true + --lower_case_table_names=1 + --max_allowed_packet=128M + --default-authentication-plugin=caching_sha2_password + ports: + - 3306:3306 + networks: + - jeecg-boot + + jeecg-boot-redis: + image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0 + ports: + - 6379:6379 + restart: always + container_name: jeecg-boot-redis + hostname: jeecg-boot-redis + networks: + - jeecg-boot + + jeecg-boot-pgvector: + image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector + container_name: jeecg-boot-pgvector + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: vector_db +# ports: +# - 5432:5432 + restart: always + networks: + - jeecg-boot + +# jeecg-boot-rabbitmq: +# image: rabbitmq:3.7.7-management +# ports: +# - 5672:5672 +# - 15672:15672 +# restart: always +# container_name: jeecg-boot-rabbitmq +# hostname: jeecg-boot-rabbitmq +# environment: +# RABBITMQ_DEFAULT_USER: guest +# RABBITMQ_DEFAULT_PASS: guest + + + jeecg-boot-nacos: + restart: always + build: + context: ./jeecg-cloud-nacos + ports: + - 8848:8848 + container_name: jeecg-boot-nacos + hostname: jeecg-boot-nacos + networks: + - jeecg-boot + + jeecg-boot-system: + depends_on: + - jeecg-boot-nacos + build: + context: ./test-system-cloud-start + container_name: test-system-start + hostname: jeecg-boot-system + restart: on-failure + environment: + - TZ=Asia/Shanghai + networks: + - jeecg-boot + + jeecg-boot-demo: + depends_on: + - jeecg-boot-nacos + build: + context: ./test-demo-cloud-start + container_name: test-demo-start + hostname: jeecg-boot-demo + restart: on-failure + environment: + - TZ=Asia/Shanghai + networks: + - jeecg-boot + + jeecg-boot-gateway: + restart: on-failure + build: + context: ./jeecg-cloud-gateway + ports: + - 9999:9999 + depends_on: + - jeecg-boot-nacos + - jeecg-boot-system + container_name: jeecg-boot-gateway + hostname: jeecg-boot-gateway + networks: + - jeecg-boot + +networks: + jeecg-boot: + name: jeecg_boot + +# jeecg-boot-sentinel: +# restart: on-failure +# build: +# context: ./jeecg-visual/jeecg-cloud-sentinel +# ports: +# - 9000:9000 +# depends_on: +# - jeecg-boot-nacos +# - jeecg-boot-demo +# - jeecg-boot-system +# - jeecg-boot-gateway +# container_name: jeecg-boot-sentinel +# hostname: jeecg-boot-sentinel +# +# jeecg-boot-xxljob: +# build: +# context: ./jeecg-visual/jeecg-cloud-xxljob +# ports: +# - 9080:9080 +# container_name: jeecg-boot-xxljob +# hostname: jeecg-boot-xxljob diff --git a/test-server-cloud/pom.xml b/test-server-cloud/pom.xml new file mode 100644 index 0000000..d92a47a --- /dev/null +++ b/test-server-cloud/pom.xml @@ -0,0 +1,25 @@ + + + + test-base-parent + com.ghb + 3.9.2 + + 4.0.0 + + test-server-cloud + pom + GHB SERVER CLOUD + + + test-cloud-gateway + test-cloud-nacos + test-system-cloud-start + + + test-visual + + + diff --git a/test-server-cloud/test-cloud-gateway/Dockerfile b/test-server-cloud/test-cloud-gateway/Dockerfile new file mode 100644 index 0000000..e410642 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/Dockerfile @@ -0,0 +1,52 @@ +# ============================================ +# Gateway 微服务 — 多阶段构建 +# Stage 1: Maven 编译全项目(SpringCloud profile) +# Stage 2: JRE 运行 +# ============================================ + +# ---- Stage 1: 编译 ---- +FROM maven:3.9-eclipse-temurin-17 AS builder + +WORKDIR /build + +# 先复制 pom.xml,利用 Docker 缓存加速依赖下载 +COPY pom.xml ./ +COPY ghb-base-core/pom.xml ghb-base-core/ +COPY ghb-module-system/pom.xml ghb-module-system/ +COPY ghb-module-system/ghb-system-api/pom.xml ghb-module-system/ghb-system-api/ +COPY ghb-module-system/ghb-system-biz/pom.xml ghb-module-system/ghb-system-biz/ +COPY ghb-module-system/ghb-system-start/pom.xml ghb-module-system/ghb-system-start/ +COPY ghb-module-business/pom.xml ghb-module-business/ +COPY ghb-server-cloud/pom.xml ghb-server-cloud/ +COPY test-server-cloud/test-cloud-gateway/pom.xml test-server-cloud/test-cloud-gateway/ +COPY test-server-cloud/test-system-cloud-start/pom.xml test-server-cloud/test-system-cloud-start/ +COPY ghb-server-cloud/ghb-demo-cloud-start/pom.xml ghb-server-cloud/ghb-demo-cloud-start/ +COPY test-server-cloud/test-cloud-nacos/pom.xml test-server-cloud/test-cloud-nacos/ + +# 下载依赖(pom 不变时缓存) +RUN mvn dependency:go-offline -B -P SpringCloud,dev || true + +# 复制全部源码 +COPY . . + +# 编译全项目(SpringCloud profile,跳过测试) +RUN mvn package -P SpringCloud,dev -Dmaven.test.skip=true -T 1C + +# ---- Stage 2: 运行 ---- +FROM eclipse-temurin:17-jre + +LABEL maintainer="ghb-base deploy" + +ENV TZ=Asia/Shanghai +RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime + +WORKDIR /app + +# 从编译阶段复制 gateway jar +COPY --from=builder /build/test-server-cloud/test-cloud-gateway/target/*.jar app.jar + +EXPOSE 9999 + +ENV JAVA_OPTS="-Xms256m -Xmx512m" + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/test-server-cloud/test-cloud-gateway/README.md b/test-server-cloud/test-cloud-gateway/README.md new file mode 100644 index 0000000..78813e6 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/README.md @@ -0,0 +1,3 @@ +http://localhost:9999 + +提示:在未启动服务实例情况下,看的接口文档为空 \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/pom.xml b/test-server-cloud/test-cloud-gateway/pom.xml new file mode 100644 index 0000000..a54f265 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/pom.xml @@ -0,0 +1,100 @@ + + + + test-server-cloud + com.ghb + 3.9.2 + + 4.0.0 + test-cloud-gateway + + + + + org.jeecgframework.boot3 + jeecg-boot-starter-cloud + + + org.jeecgframework.boot3 + test-system-cloud-api + + + + + + org.springframework.cloud + spring-cloud-starter-gateway-server-webflux + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-data-redis-reactive + + + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-gateway + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + fastjson + com.alibaba + + + + + + com.alibaba.csp + sentinel-datasource-nacos + + + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + ${knife4j-spring-boot-starter.version} + + + org.apache.commons + commons-lang3 + 3.18.0 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + src/main/resources + true + + + + \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/GhbGatewayApplication.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/GhbGatewayApplication.java new file mode 100644 index 0000000..0a9041a --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/GhbGatewayApplication.java @@ -0,0 +1,57 @@ +package com.ghb.base; + +import com.ghb.base.loader.DynamicRouteLoader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +import jakarta.annotation.Resource; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; + +/** + * @author Ghb + */ +@EnableFeignClients +@EnableDiscoveryClient +@SpringBootApplication +public class GhbGatewayApplication implements CommandLineRunner { + @Resource + private DynamicRouteLoader dynamicRouteLoader; + + public static void main(String[] args) { + ConfigurableApplicationContext applicationContext = SpringApplication.run(GhbGatewayApplication.class, args); + //String userName = applicationContext.getEnvironment().getProperty("Ghb.test"); + //System.err.println("user name :" +userName); + } + + /** + * 容器初始化后加载路由 + * @param strings + */ + @Override + public void run(String... strings) { + dynamicRouteLoader.refresh(null); + } + + /** + * 接口地址(通过9999端口直接访问) + * 已使用knife4j-gateway支持该功能 + * @param indexHtml + * @return + */ + @Bean + public RouterFunction indexRouter(@Value("classpath:/META-INF/resources/doc.html") final org.springframework.core.io.Resource indexHtml) { + return route(GET("/"), request -> ok().contentType(MediaType.TEXT_HTML).syncBody(indexHtml)); + } +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/GatewayRoutersConfig.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/GatewayRoutersConfig.java new file mode 100644 index 0000000..1d7aac9 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/GatewayRoutersConfig.java @@ -0,0 +1,89 @@ +package com.ghb.base.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +/** + * @author scott + * @date 2020/05/26 + * 路由配置信息 + */ +@Configuration +@RefreshScope +public class GatewayRoutersConfig { + /** + * 路由配置方式:database,yml,nacos + */ + public String dataType; + public String serverAddr; + public String namespace; + public String dataId; + public String routeGroup; + public String username; + public String password; + + @Value("${ghb.route.config.data-type:#{null}}") + public void setDataType(String dataType) { + this.dataType = dataType; + } + + @Value("${ghb.route.config.data-id:#{null}}") + public void setRouteDataId(String dataId) { + this.dataId = dataId + ".json"; + } + + @Value("${spring.cloud.nacos.config.group:DEFAULT_GROUP:#{null}}") + public void setRouteGroup(String routeGroup) { + this.routeGroup = routeGroup; + } + + @Value("${spring.cloud.nacos.discovery.server-addr}") + public void setServerAddr(String serverAddr) { + this.serverAddr = serverAddr; + } + + @Value("${spring.cloud.nacos.config.namespace:#{null}}") + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + @Value("${spring.cloud.nacos.config.username:#{null}}") + public void setUsername(String username) { + this.username = username; + } + + @Value("${spring.cloud.nacos.config.password:#{null}}") + public void setPassword(String password) { + this.password = password; + } + + public String getDataType() { + return dataType; + } + + public String getServerAddr() { + return serverAddr; + } + + public String getNamespace() { + return namespace; + } + + public String getDataId() { + return dataId; + } + + public String getRouteGroup() { + return routeGroup; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RateLimiterConfiguration.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RateLimiterConfiguration.java new file mode 100644 index 0000000..7d5f76b --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RateLimiterConfiguration.java @@ -0,0 +1,43 @@ +package com.ghb.base.config; + +import com.ghb.base.filter.GlobalAccessTokenFilter; +import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import reactor.core.publisher.Mono; + +/** + * @author scott + * @date 2020/5/26 + * 路由限流配置 + */ +@Configuration +public class RateLimiterConfiguration { + /** + * IP限流 (通过exchange对象可以获取到请求信息,这边用了HostName) + */ + @Bean + @Primary + public KeyResolver ipKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); + } + + /** + * 用户限流 (通过exchange对象可以获取到请求信息,获取当前请求的用户 TOKEN) + */ + @Bean + public KeyResolver userKeyResolver() { + //使用这种方式限流,请求Header中必须携带X-Access-Token参数 + return exchange -> Mono.just(exchange.getRequest().getHeaders().getFirst(GlobalAccessTokenFilter.X_ACCESS_TOKEN)); + } + + /** + * 接口限流 (获取请求地址的uri作为限流key) + */ + @Bean + public KeyResolver apiKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getPath().value()); + } + +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RedisUtilConfig.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RedisUtilConfig.java new file mode 100644 index 0000000..fd58334 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RedisUtilConfig.java @@ -0,0 +1,17 @@ +package com.ghb.base.config; + +import org.jeecg.common.util.RedisUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; + +@Configuration +@ConditionalOnBean(RedisTemplate.class) +public class RedisUtilConfig { + + @Bean + public RedisUtil redisUtil() { + return new RedisUtil(); + } +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RouterDataType.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RouterDataType.java new file mode 100644 index 0000000..38f7e7a --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/config/RouterDataType.java @@ -0,0 +1,21 @@ +package com.ghb.base.config; + +/** + * nocos配置方式枚举 + * @author zyf + * @date: 2022/4/21 10:55 + */ +public enum RouterDataType { + /** + * 数据库加载路由配置 + */ + database, + /** + * 本地yml加载路由配置 + */ + yml, + /** + * nacos加载路由配置 + */ + nacos +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/FallbackController.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/FallbackController.java new file mode 100644 index 0000000..eee8f76 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/FallbackController.java @@ -0,0 +1,32 @@ +//package com.ghb.base.fallback; +// +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +//import reactor.core.publisher.Mono; +// +///** +// * 响应超时熔断处理器【升级springboot2.6.6后,此类作废】 +// * +// * @author zyf +// */ +//@RestController +//public class FallbackController { +// +// /** +// * 全局熔断处理 +// * @return +// */ +// @RequestMapping("/fallback") +// public Mono fallback() { +// return Mono.just("访问超时,请稍后再试!"); +// } +// +// /** +// * demo熔断处理 +// * @return +// */ +// @RequestMapping("/demo/fallback") +// public Mono fallback2() { +// return Mono.just("访问超时,请稍后再试!"); +// } +//} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/HystrixFallbackHandler.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/HystrixFallbackHandler.java new file mode 100644 index 0000000..5a85898 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/HystrixFallbackHandler.java @@ -0,0 +1,33 @@ +//package com.ghb.base.fallback; +// +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.http.HttpStatus; +//import org.springframework.stereotype.Component; +//import org.springframework.web.reactive.function.BodyInserters; +//import org.springframework.web.reactive.function.server.HandlerFunction; +//import org.springframework.web.reactive.function.server.ServerRequest; +//import org.springframework.web.reactive.function.server.ServerResponse; +//import reactor.core.publisher.Mono; +// +//import java.util.Optional; +// +//import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR; +// +///** +// * @author scott +// * @date 2020/05/26 +// * Hystrix 降级处理 +// */ +//@Slf4j +//@Component +//public class HystrixFallbackHandler implements HandlerFunction { +// @Override +// public Mono handle(ServerRequest serverRequest) { +// Optional originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR); +// +// originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri)); +// +// return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()) +// .header("Content-Type","text/plain; charset=utf-8").body(BodyInserters.fromObject("访问超时,请稍后再试")); +// } +//} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/sentinel/GatewaySentinelExceptionConfig.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/sentinel/GatewaySentinelExceptionConfig.java new file mode 100644 index 0000000..95e45aa --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/sentinel/GatewaySentinelExceptionConfig.java @@ -0,0 +1,45 @@ +package com.ghb.base.fallback.sentinel; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; +import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; +import org.jeecg.common.enums.SentinelErrorInfoEnum; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.ServerResponse; + +import jakarta.annotation.PostConstruct; +import java.util.HashMap; + +/** + * @Description: 自定义Sentinel全局异常(需要启动Sentinel客户端) + * @author: zyf + * @date: 2022/02/18 + * @version: V1.0 + */ +@Configuration +public class GatewaySentinelExceptionConfig { + + @PostConstruct + public void init() { + + BlockRequestHandler blockRequestHandler = (serverWebExchange, ex) -> { + String msg; + SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(ex); + if (ObjectUtil.isNotEmpty(errorInfoEnum)) { + msg = errorInfoEnum.getError(); + } else { + msg = "未知限流降级"; + } + HashMap map = new HashMap(5); + map.put("code", HttpStatus.TOO_MANY_REQUESTS.toString()); + map.put("message", msg); + //自定义异常处理 + return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(map)); + }; + + GatewayCallbackManager.setBlockHandler(blockRequestHandler); + } +} \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/sentinel/SentinelBlockRequestHandler.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/sentinel/SentinelBlockRequestHandler.java new file mode 100644 index 0000000..26cfffa --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/fallback/sentinel/SentinelBlockRequestHandler.java @@ -0,0 +1,39 @@ +//package com.ghb.base.fallback.sentinel; +//import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; +//import com.alibaba.csp.sentinel.transport.config.TransportConfig; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.cloud.commons.util.InetUtils; +//import org.springframework.http.HttpStatus; +//import org.springframework.http.MediaType; +//import org.springframework.stereotype.Component; +//import org.springframework.web.reactive.function.BodyInserters; +//import org.springframework.web.reactive.function.server.ServerResponse; +//import org.springframework.web.server.ServerWebExchange; +//import reactor.core.publisher.Mono; +// +//import javax.annotation.PostConstruct; +// +///** +// * 自定义限流返回信息 +// * @author scott +// */ +//@Slf4j +//@Component +//public class SentinelBlockRequestHandler implements BlockRequestHandler { +// @Autowired +// private InetUtils inetUtils; +// +// @PostConstruct +// public void doInit() { +// System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, inetUtils.findFirstNonLoopbackAddress().getHostAddress()); +// } +// +// @Override +// public Mono handleRequest(ServerWebExchange exchange, Throwable ex) { +// String resultString = "{\"code\":403,\"message\":\"服务开启限流保护,请稍后再试!\"}"; +// return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS).contentType(MediaType.APPLICATION_JSON_UTF8).body(BodyInserters.fromObject(resultString)); +// } +// +// +//} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/filter/GlobalAccessTokenFilter.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/filter/GlobalAccessTokenFilter.java new file mode 100644 index 0000000..2b5f750 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/filter/GlobalAccessTokenFilter.java @@ -0,0 +1,59 @@ +package com.ghb.base.filter; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import java.util.Arrays; +import java.util.stream.Collectors; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; + +/** +* 全局拦截器,作用所有的微服务 +* +* 1.重写StripPrefix(获取真实的URL) +* 2.将现在的request,添加当前身份 +* @author: scott +* @date: 2022/4/8 10:55 +*/ +@Slf4j +@Component +public class GlobalAccessTokenFilter implements GlobalFilter, Ordered { + public final static String X_ACCESS_TOKEN = "X-Access-Token"; + public final static String X_GATEWAY_BASE_PATH = "X_GATEWAY_BASE_PATH"; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + + String scheme = exchange.getRequest().getURI().getScheme(); + String host = exchange.getRequest().getURI().getHost(); + int port = exchange.getRequest().getURI().getPort(); + // 代码逻辑说明: 地址中没有带端口(http/https默认)时port是-1------------ + String basePath = scheme + "://" + host; + if (port != -1) { + basePath += ":" + port; + } + // 1. 重写StripPrefix(获取真实的URL) + addOriginalRequestUrl(exchange, exchange.getRequest().getURI()); + String rawPath = exchange.getRequest().getURI().getRawPath(); + String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, "/")).skip(1L).collect(Collectors.joining("/")); + ServerHttpRequest newRequest = exchange.getRequest().mutate().path(newPath).build(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI()); + //2.将现在的request,添加当前身份 + ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization-UserName", "").header(X_GATEWAY_BASE_PATH,basePath).build(); + ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build(); + return chain.filter(mutableExchange); + } + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/filter/SentinelFilterContextConfig.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/filter/SentinelFilterContextConfig.java new file mode 100644 index 0000000..9bf3616 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/filter/SentinelFilterContextConfig.java @@ -0,0 +1,25 @@ +//package com.ghb.base.filter; +// +//import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +//import org.springframework.boot.web.servlet.FilterRegistrationBean; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +// +///** 升级spring boot 3后,无法找到平替 +// * @author: zyf +// * @date: 20210715 +// */ +//@Configuration +//public class SentinelFilterContextConfig { +// @Bean +// public FilterRegistrationBean sentinelFilterRegistration() { +// FilterRegistrationBean registration = new FilterRegistrationBean(); +// registration.setFilter(new CommonFilter()); +// registration.addUrlPatterns("/*"); +// // 入口资源关闭聚合 +// registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false"); +// registration.setName("sentinelFilter"); +// registration.setOrder(1); +// return registration; +// } +//} \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/LoderRouderHandler.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/LoderRouderHandler.java new file mode 100644 index 0000000..c0af470 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/LoderRouderHandler.java @@ -0,0 +1,30 @@ +package com.ghb.base.handler; + +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.base.BaseMap; +import org.jeecg.common.constant.GlobalConstants; +import org.jeecg.common.modules.redis.listener.JeecgRedisListener; +import com.ghb.base.loader.DynamicRouteLoader; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 路由刷新监听(实现方式:redis监听handler) + * @author zyf + * @date: 2022/4/21 10:55 + */ +@Slf4j +@Component(GlobalConstants.LODER_ROUDER_HANDLER) +public class LoderRouderHandler implements JeecgRedisListener { + + @Resource + private DynamicRouteLoader dynamicRouteLoader; + + + @Override + public void onMessage(BaseMap message) { + dynamicRouteLoader.refresh(message); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/swagger/MySwaggerResourceProvider.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/swagger/MySwaggerResourceProvider.java new file mode 100644 index 0000000..379fa7f --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/swagger/MySwaggerResourceProvider.java @@ -0,0 +1,158 @@ +package com.ghb.base.handler.swagger; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.nacos.api.naming.NamingFactory; +import com.alibaba.nacos.api.naming.NamingService; +import com.alibaba.nacos.api.naming.pojo.Instance; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import springfox.documentation.swagger.web.SwaggerResource; +import springfox.documentation.swagger.web.SwaggerResourcesProvider; + +import java.util.*; + +/** 已使用knife4j-gateway支持该功能 + * 聚合各个服务的swagger接口 + * @author zyf + * @date: 2022/4/21 10:55 + */ +@Component +@Slf4j +@Primary +public class MySwaggerResourceProvider implements SwaggerResourcesProvider { + /** + * swagger2默认的url后缀 + */ + private static final String SWAGGER2URL = "/v3/api-docs"; + + /** + * 网关路由 + */ + private final RouteLocator routeLocator; + /** + * Nacos名字服务 + */ + private NamingService naming; + + /** + * nacos服务地址 + */ + @Value("${spring.cloud.nacos.discovery.server-addr}") + private String serverAddr; + /** + * nacos namespace + */ + @Value("${spring.cloud.nacos.discovery.namespace:#{null}}") + private String namespace; + + /** + * nacos groupName + */ + @Value("${spring.cloud.nacos.config.group:DEFAULT_GROUP:#{null}}") + private String group; + + /** + * nacos username + */ + @Value("${spring.cloud.nacos.discovery.username:#{null}}") + private String username; + /** + * nacos password + */ + @Value("${spring.cloud.nacos.discovery.password:#{null}}") + private String password; + + /** + * Swagger中需要排除的服务 + */ + private String[] excludeServiceIds=new String[]{"Ghb-cloud-monitor"}; + + + /** + * 网关应用名称 + */ + @Value("${spring.application.name}") + private String self; + + @Autowired + public MySwaggerResourceProvider(RouteLocator routeLocator) { + this.routeLocator = routeLocator; + } + + @Override + public List get() { + List resources = new ArrayList<>(); + List routeHosts = new ArrayList<>(); + // 获取所有可用的host:serviceId + routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null) + .filter(route -> !self.equals(route.getUri().getHost())) + .subscribe(route ->{ + // 代码逻辑说明: 过滤掉无效路由,避免接口文档报错无法打开 + boolean hasRoute=checkRoute(route.getId()); + if(hasRoute){ + routeHosts.add(route.getUri().getHost()); + } + }); + + // 记录已经添加过的server,存在同一个应用注册了多个服务在nacos上 + Set dealed = new HashSet<>(); + routeHosts.forEach(instance -> { + // 拼接url + String url = "/" + instance.toLowerCase() + SWAGGER2URL; + if (!dealed.contains(url)) { + dealed.add(url); + log.info(" Gateway add SwaggerResource: {}",url); + SwaggerResource swaggerResource = new SwaggerResource(); + swaggerResource.setUrl(url); + swaggerResource.setSwaggerVersion("2.0"); + swaggerResource.setName(instance); + //Swagger排除不展示的服务 + if(!ArrayUtil.contains(excludeServiceIds,instance)){ + resources.add(swaggerResource); + } + } + }); + return resources; + } + + /** + * 检测nacos中是否有健康实例 + * @param routeId + * @return + */ + private Boolean checkRoute(String routeId) { + Boolean hasRoute = false; + try { + //修复使用带命名空间启动网关swagger看不到接口文档的问题 + Properties properties=new Properties(); + properties.setProperty("serverAddr",serverAddr); + if(namespace!=null && !"".equals(namespace)){ + log.info("nacos.discovery.namespace = {}", namespace); + properties.setProperty("namespace",namespace); + } + if(username!=null && !"".equals(username)){ + properties.setProperty("username",username); + } + if(password!=null && !"".equals(password)){ + properties.setProperty("password",password); + } + //【issues/5115】因swagger文档导致gateway内存溢出 + if (this.naming == null) { + this.naming = NamingFactory.createNamingService(properties); + } + log.info(" config.group : {}", group); + List list = this.naming.selectInstances(routeId, group , true); + if (ObjectUtil.isNotEmpty(list)) { + hasRoute = true; + } + } catch (Exception e) { + e.printStackTrace(); + } + return hasRoute; + } +} \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/swagger/SwaggerResourceController.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/swagger/SwaggerResourceController.java new file mode 100644 index 0000000..15196bc --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/handler/swagger/SwaggerResourceController.java @@ -0,0 +1,49 @@ +package com.ghb.base.handler.swagger; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import springfox.documentation.swagger.web.*; + +import java.util.ArrayList; +import java.util.List; + +/** 已使用knife4j-gateway支持该功能 + * swagger聚合接口,三个接口都是 doc.html需要访问的接口 + * @author zyf + * @date: 2022/4/21 10:55 + */ +@RestController +@RequestMapping("/swagger-resources") +public class SwaggerResourceController { + private MySwaggerResourceProvider swaggerResourceProvider; + + @Value("${knife4j.gateway.enabled:true}") + private Boolean enableSwagger; + + @Autowired + public SwaggerResourceController(MySwaggerResourceProvider swaggerResourceProvider) { + this.swaggerResourceProvider = swaggerResourceProvider; + } + + @RequestMapping(value = "/configuration/security") + public ResponseEntity securityConfiguration() { + return new ResponseEntity<>(SecurityConfigurationBuilder.builder().build(), HttpStatus.OK); + } + + @RequestMapping(value = "/configuration/ui") + public ResponseEntity uiConfiguration() { + return new ResponseEntity<>(UiConfigurationBuilder.builder().build(), HttpStatus.OK); + } + + @RequestMapping + public ResponseEntity> swaggerResources() { + if (!enableSwagger) { + return new ResponseEntity<>(new ArrayList<>(), HttpStatus.OK); + } + return new ResponseEntity<>(swaggerResourceProvider.get(), HttpStatus.OK); + } +} \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/DynamicRouteLoader.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/DynamicRouteLoader.java new file mode 100644 index 0000000..eb9c025 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/DynamicRouteLoader.java @@ -0,0 +1,386 @@ +package com.ghb.base.loader; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.nacos.api.NacosFactory; +import com.alibaba.nacos.api.config.ConfigService; +import com.alibaba.nacos.api.config.listener.Listener; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.shaded.com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.jeecg.common.base.BaseMap; +import org.jeecg.common.constant.CacheConstant; +import org.jeecg.common.util.RedisUtil; +import com.ghb.base.config.GatewayRoutersConfig; +import com.ghb.base.config.RouterDataType; +import com.ghb.base.loader.repository.DynamicRouteService; +import com.ghb.base.loader.repository.MyInMemoryRouteDefinitionRepository; +import com.ghb.base.loader.vo.MyRouteDefinition; +import com.ghb.base.loader.vo.PredicatesVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.cloud.gateway.filter.FilterDefinition; +import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; +import org.springframework.cloud.gateway.route.RouteDefinition; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.concurrent.Executor; + +/** + * 动态路由加载器 + * + * @author : zyf + * @date :2020-11-10 + */ +@Slf4j +@Component +@RefreshScope +@DependsOn({"gatewayRoutersConfig"}) +public class DynamicRouteLoader implements ApplicationEventPublisherAware { + + public static final long DEFAULT_TIMEOUT = 30000; + @Autowired + private GatewayRoutersConfig gatewayRoutersConfig; + private MyInMemoryRouteDefinitionRepository repository; + private ApplicationEventPublisher publisher; + private DynamicRouteService dynamicRouteService; + private ConfigService configService; + @Autowired(required = false) + private RedisUtil redisUtil; + + + /** + * 需要拼接key的路由条件 + */ + private static String[] GEN_KEY_ROUTERS = new String[]{"Path", "Host", "Method", "After", "Before", "Between", "RemoteAddr"}; + + public DynamicRouteLoader(MyInMemoryRouteDefinitionRepository repository, DynamicRouteService dynamicRouteService) { + this.repository = repository; + this.dynamicRouteService = dynamicRouteService; + } + +// @PostConstruct +// public void init() { +// init(null); +// } + + + public void init(BaseMap baseMap) { + String dataType = gatewayRoutersConfig.getDataType(); + log.info("初始化路由模式,dataType:{}", dataType); + if (RouterDataType.nacos.toString().equals(dataType)) { + loadRoutesByNacos(); + } + //从数据库加载路由 + if (RouterDataType.database.toString().equals(dataType)) { + loadRoutesByRedis(baseMap); + } + } + /** + * 刷新路由 + * + * @return + */ + public Mono refresh(BaseMap baseMap) { + String dataType = gatewayRoutersConfig.getDataType(); + log.info("初始化路由模式,dataType:{}", dataType); + if (dataType != null && !RouterDataType.yml.toString().equals(dataType)) { + this.init(baseMap); + } + return Mono.empty(); + } + + + /** + * 从nacos中读取路由配置 + * + * @return + */ + private void loadRoutesByNacos() { + List routes = Lists.newArrayList(); + configService = createConfigService(); + if (configService == null) { + log.warn("initConfigService fail"); + } + try { + log.info("Ghb.route.config.data-id = {}", gatewayRoutersConfig.getDataId()); + log.info("nacos.config.group = {}", gatewayRoutersConfig.getRouteGroup()); + String configInfo = configService.getConfig(gatewayRoutersConfig.getDataId(), gatewayRoutersConfig.getRouteGroup(), DEFAULT_TIMEOUT); + if (StringUtils.isNotBlank(configInfo)) { + log.info("获取网关当前配置:\r\n{}", configInfo); + routes = JSON.parseArray(configInfo, RouteDefinition.class); + }else{ + log.warn("ERROR: 从Nacos获取网关配置为空,请确认Nacos配置是否正确!"); + } + } catch (NacosException e) { + log.error("初始化网关路由时发生错误", e); + e.printStackTrace(); + } + for (RouteDefinition definition : routes) { + log.info("update route : {}", definition.toString()); + dynamicRouteService.add(definition); + } + this.publisher.publishEvent(new RefreshRoutesEvent(this)); + dynamicRouteByNacosListener(gatewayRoutersConfig.getDataId(), gatewayRoutersConfig.getRouteGroup()); + } + + + /** + * 从redis中读取路由配置 + * + * @return + */ + private void loadRoutesByRedis(BaseMap baseMap) { + if (redisUtil == null) { + log.error("RedisUtil 未初始化,无法从Redis加载路由配置。请检查Redis连接配置。"); + return; + } + List routes = Lists.newArrayList(); + configService = createConfigService(); + if (configService == null) { + log.warn("initConfigService fail"); + } + Object configInfo = redisUtil.get(CacheConstant.GATEWAY_ROUTES); + if (ObjectUtil.isNotEmpty(configInfo)) { + log.info("获取网关当前配置:\r\n{}", configInfo); + JSONArray array = JSON.parseArray(configInfo.toString()); + try { + routes = getRoutesByJson(array); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + }else{ + log.warn("ERROR: 从Redis获取网关配置为空,请确认system服务是否启动成功!"); + } + + for (MyRouteDefinition definition : routes) { + log.info("update route : {}", definition.toString()); + Integer status=definition.getStatus(); + if(status.equals(0)){ + dynamicRouteService.delete(definition.getId()); + }else{ + dynamicRouteService.add(definition); + } + } + if(ObjectUtils.isNotEmpty(baseMap)){ + String delRouterId = baseMap.get("delRouterId"); + if (ObjectUtils.isNotEmpty(delRouterId)) { + dynamicRouteService.delete(delRouterId); + } + } + this.publisher.publishEvent(new RefreshRoutesEvent(this)); + } + + /** + * redis中的信息需要处理下 转成RouteDefinition对象 + * - id: login + * uri: lb://cloud-Ghb-system + * predicates: + * - Path=/Ghb-boot/sys/**, + * + * @param array + * @return + */ + + public static List getRoutesByJson(JSONArray array) throws URISyntaxException { + List ls = new ArrayList<>(); + for (int i = 0; i < array.size(); i++) { + JSONObject obj = array.getJSONObject(i); + MyRouteDefinition route = new MyRouteDefinition(); + route.setId(obj.getString("routerId")); + route.setStatus(obj.getInteger("status")); + Object uri = obj.get("uri"); + if (uri == null) { + route.setUri(new URI("lb://" + obj.getString("name"))); + } else { + route.setUri(new URI(obj.getString("uri"))); + } + Object predicates = obj.get("predicates"); + if (predicates != null) { + // 代码逻辑说明: [issues/5331]网关路由配置问题 + List list = JSON.parseArray(predicates.toString(), PredicatesVo.class); + //获取合并后的Predicates,防止配置多个path导致路径失效的问题 + Map> groupedPredicates = new HashMap<>(); + for (PredicatesVo predicatesVo : list) { + String name = predicatesVo.getName(); + List args = predicatesVo.getArgs(); + groupedPredicates.computeIfAbsent(name, k -> new ArrayList<>()).addAll(args); + } + //合并后的list + list = new ArrayList<>(); + for (Map.Entry> entry : groupedPredicates.entrySet()) { + String name = entry.getKey(); + List args = entry.getValue(); + list.add(new PredicatesVo(name, args)); + } + List predicateDefinitionList = new ArrayList<>(); + for (Object map : list) { + JSONObject json = JSON.parseObject(JSON.toJSONString(map)); + PredicateDefinition predicateDefinition = new PredicateDefinition(); + // 代码逻辑说明: 【VUEN-762】路由条件添加异常问题,原因是部分路由条件参数需要设置固定key + String name=json.getString("name"); + predicateDefinition.setName(name); + //路由条件是否拼接Key + if(ArrayUtil.contains(GEN_KEY_ROUTERS,name)) { + JSONArray jsonArray = json.getJSONArray("args"); + for (int j = 0; j < jsonArray.size(); j++) { + predicateDefinition.addArg("_genkey" + j, jsonArray.get(j).toString()); + } + }else{ + JSONObject jsonObject = json.getJSONObject("args"); + if(ObjectUtil.isNotEmpty(jsonObject)){ + for (Map.Entry entry : jsonObject.entrySet()) { + Object valueObj=entry.getValue(); + if(ObjectUtil.isNotEmpty(valueObj)) { + predicateDefinition.addArg(entry.getKey(), valueObj.toString()); + } + } + } + } + predicateDefinitionList.add(predicateDefinition); + } + route.setPredicates(predicateDefinitionList); + } + + Object filters = obj.get("filters"); + if (filters != null) { + JSONArray list = JSON.parseArray(filters.toString()); + List filterDefinitionList = new ArrayList<>(); + if (ObjectUtil.isNotEmpty(list)) { + for (Object map : list) { + JSONObject json = (JSONObject) map; + JSONArray jsonArray = json.getJSONArray("args"); + String name = json.getString("name"); + FilterDefinition filterDefinition = new FilterDefinition(); + for (Object o : jsonArray) { + JSONObject params = (JSONObject) o; + filterDefinition.addArg(params.getString("key"), params.get("value").toString()); + } + filterDefinition.setName(name); + filterDefinitionList.add(filterDefinition); + } + route.setFilters(filterDefinitionList); + } + } + ls.add(route); + } + return ls; + } + + +// private void loadRoutesByDataBase() { +// List routeList = jdbcTemplate.query(SELECT_ROUTES, new RowMapper() { +// @Override +// public GatewayRouteVo mapRow(ResultSet rs, int i) throws SQLException { +// GatewayRouteVo result = new GatewayRouteVo(); +// result.setId(rs.getString("id")); +// result.setName(rs.getString("name")); +// result.setUri(rs.getString("uri")); +// result.setStatus(rs.getInt("status")); +// result.setRetryable(rs.getInt("retryable")); +// result.setPredicates(rs.getString("predicates")); +// result.setStripPrefix(rs.getInt("strip_prefix")); +// result.setPersist(rs.getInt("persist")); +// return result; +// } +// }); +// if (ObjectUtil.isNotEmpty(routeList)) { +// // 加载路由 +// routeList.forEach(route -> { +// RouteDefinition definition = new RouteDefinition(); +// List predicatesList = Lists.newArrayList(); +// List filtersList = Lists.newArrayList(); +// definition.setId(route.getId()); +// String predicates = route.getPredicates(); +// String filters = route.getFilters(); +// if (StringUtils.isNotEmpty(predicates)) { +// predicatesList = JSON.parseArray(predicates, PredicateDefinition.class); +// definition.setPredicates(predicatesList); +// } +// if (StringUtils.isNotEmpty(filters)) { +// filtersList = JSON.parseArray(filters, FilterDefinition.class); +// definition.setFilters(filtersList); +// } +// URI uri = UriComponentsBuilder.fromUriString(route.getUri()).build().toUri(); +// definition.setUri(uri); +// this.repository.save(Mono.just(definition)).subscribe(); +// }); +// log.info("加载路由:{}==============", routeList.size()); +// Mono.empty(); +// } +// } + + + /** + * 监听Nacos下发的动态路由配置 + * + * @param dataId + * @param group + */ + public void dynamicRouteByNacosListener(String dataId, String group) { + try { + configService.addListener(dataId, group, new Listener() { + @Override + public void receiveConfigInfo(String configInfo) { + log.info("进行网关更新:\n\r{}", configInfo); + List definitionList = JSON.parseArray(configInfo, MyRouteDefinition.class); + for (MyRouteDefinition definition : definitionList) { + log.info("update route : {}", definition.toString()); + dynamicRouteService.update(definition); + } + } + + @Override + public Executor getExecutor() { + log.info("getExecutor\n\r"); + return null; + } + }); + } catch (Exception e) { + log.error("从nacos接收动态路由配置出错!!!", e); + } + } + + /** + * 创建ConfigService + * + * @return + */ + private ConfigService createConfigService() { + try { + Properties properties = new Properties(); + properties.setProperty("serverAddr", gatewayRoutersConfig.getServerAddr()); + if(StringUtils.isNotBlank(gatewayRoutersConfig.getNamespace())){ + properties.setProperty("namespace", gatewayRoutersConfig.getNamespace()); + } + if(StringUtils.isNotBlank( gatewayRoutersConfig.getUsername())){ + properties.setProperty("username", gatewayRoutersConfig.getUsername()); + } + if(StringUtils.isNotBlank(gatewayRoutersConfig.getPassword())){ + properties.setProperty("password", gatewayRoutersConfig.getPassword()); + } + return configService = NacosFactory.createConfigService(properties); + } catch (Exception e) { + log.error("创建ConfigService异常", e); + return null; + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/repository/DynamicRouteService.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/repository/DynamicRouteService.java new file mode 100644 index 0000000..4a0dafa --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/repository/DynamicRouteService.java @@ -0,0 +1,89 @@ +package com.ghb.base.loader.repository; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.loader.repository.MyInMemoryRouteDefinitionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.cloud.gateway.route.RouteDefinition; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +/** + * 动态更新路由网关service + * 1)实现一个Spring提供的事件推送接口ApplicationEventPublisherAware + * 2)提供动态路由的基础方法,可通过获取bean操作该类的方法。该类提供新增路由、更新路由、删除路由,然后实现发布的功能。 + * + * @author zyf + */ +@Slf4j +@Service +public class DynamicRouteService implements ApplicationEventPublisherAware { + + @Autowired + private MyInMemoryRouteDefinitionRepository repository; + + /** + * 发布事件 + */ + + private ApplicationEventPublisher publisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + /** + * 删除路由 + * + * @param id + * @return + */ + public synchronized void delete(String id) { + try { + repository.delete(Mono.just(id)).subscribe(); + this.publisher.publishEvent(new RefreshRoutesEvent(this)); + }catch (Exception e){ + log.warn(e.getMessage(),e); + } + } + + /** + * 更新路由 + * + * @param definition + * @return + */ + public synchronized String update(RouteDefinition definition) { + try { + log.info("gateway update route {}", definition); + } catch (Exception e) { + return "update fail,not find route routeId: " + definition.getId(); + } + try { + repository.save(Mono.just(definition)).subscribe(); + this.publisher.publishEvent(new RefreshRoutesEvent(this)); + return "success"; + } catch (Exception e) { + return "update route fail"; + } + } + + /** + * 增加路由 + * + * @param definition + * @return + */ + public synchronized String add(RouteDefinition definition) { + log.info("gateway add route {}", definition); + try { + repository.save(Mono.just(definition)).subscribe(); + } catch (Exception e) { + log.warn(e.getMessage(),e); + } + return "success"; + } +} \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/repository/MyInMemoryRouteDefinitionRepository.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/repository/MyInMemoryRouteDefinitionRepository.java new file mode 100644 index 0000000..fa4bb84 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/repository/MyInMemoryRouteDefinitionRepository.java @@ -0,0 +1,68 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.ghb.base.loader.repository; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import ch.qos.logback.classic.Logger; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.route.RouteDefinition; +import org.springframework.cloud.gateway.route.RouteDefinitionRepository; +import org.springframework.cloud.gateway.support.NotFoundException; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author qinfeng + */ +@Slf4j +@Component +public class MyInMemoryRouteDefinitionRepository implements RouteDefinitionRepository { + private final Map routes = Collections.synchronizedMap(new LinkedHashMap()); + + public MyInMemoryRouteDefinitionRepository() { + } + + @Override + public Mono save(Mono route) { + return route.flatMap((r) -> { + if (ObjectUtils.isEmpty(r.getId())) { + return Mono.error(new IllegalArgumentException("id may not be empty")); + } else { + this.routes.put(r.getId(), r); + return Mono.empty(); + } + }); + } + + @Override + public Mono delete(Mono routeId) { + return routeId.flatMap((id) -> { + if (this.routes.containsKey(id)) { + this.routes.remove(id); + return Mono.empty(); + } else { + log.warn("RouteDefinition not found: " + routeId); + return Mono.empty(); +// return Mono.defer(() -> { +// return Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)); +// }); + } + }); + } + + @Override + public Flux getRouteDefinitions() { + Map routesSafeCopy = new LinkedHashMap(this.routes); + return Flux.fromIterable(routesSafeCopy.values()); + } +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/GatewayRouteVo.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/GatewayRouteVo.java new file mode 100644 index 0000000..5c19a6e --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/GatewayRouteVo.java @@ -0,0 +1,21 @@ +package com.ghb.base.loader.vo; + +import lombok.Data; + +/** + * 路由参数模型 + * @author zyf + * @date: 2022/4/21 10:55 + */ +@Data +public class GatewayRouteVo { + private String id; + private String name; + private String uri; + private String predicates; + private String filters; + private Integer stripPrefix; + private Integer retryable; + private Integer persist; + private Integer status; +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/MyRouteDefinition.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/MyRouteDefinition.java new file mode 100644 index 0000000..14843a3 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/MyRouteDefinition.java @@ -0,0 +1,22 @@ +package com.ghb.base.loader.vo; + +import org.springframework.cloud.gateway.route.RouteDefinition; + +/** + * 自定义RouteDefinition + * @author zyf + */ +public class MyRouteDefinition extends RouteDefinition { + /** + * 路由状态 + */ + private Integer status; + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } +} diff --git a/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/PredicatesVo.java b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/PredicatesVo.java new file mode 100644 index 0000000..777939f --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/java/com/ghb/base/loader/vo/PredicatesVo.java @@ -0,0 +1,20 @@ +package com.ghb.base.loader.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 路由配置VO + * @author lsq + * @Date 2023/10/15 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PredicatesVo { + private String name; + private List args; +} \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/main/resources/application.yml b/test-server-cloud/test-cloud-gateway/src/main/resources/application.yml new file mode 100644 index 0000000..29af865 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/resources/application.yml @@ -0,0 +1,136 @@ +server: + port: 9999 + +knife4j: + gateway: + enabled: true + +management: + endpoints: + web: + exposure: + include: gateway, health, info + +# 路由兜底配置(本地开发时 Nacos 配置可能缺失) +ghb: + route: + config: + data-type: nacos + data-id: jeecg-gateway-router + +spring: + application: + name: test-gateway + main: + allow-circular-references: true + config: + import: + - optional:nacos:${spring.application.name}-@profile.name@.yaml + # Redis兜底配置(本地开发时Nacos不可用,确保RedisTemplate可创建) + data: + redis: + host: ${REDIS_HOST:127.0.0.1} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASS:} + database: 0 + cloud: + nacos: + config: + server-addr: @config.server-addr@ + group: @config.group@ + namespace: @config.namespace@ + username: @config.username@ + password: @config.password@ + discovery: + server-addr: ${spring.cloud.nacos.config.server-addr} + group: @config.group@ + namespace: @config.namespace@ + username: @config.username@ + password: @config.password@ + gateway: + discovery: + locator: + enabled: true + lower-case-service-id: true + globalcors: + cors-configurations: + '[/**]': + allow-credentials: true + allowed-origin-patterns: + - "*" + allowed-methods: + - "*" + allowed-headers: + - "*" + #Sentinel配置 + sentinel: + transport: + dashboard: jeecg-boot-sentinel:9000 + # 支持链路限流 + web-context-unify: false + filter: + enabled: false + # 取消Sentinel控制台懒加载 + eager: false + datasource: + #流控规则 + flow: # 指定数据源名称 + # 指定nacos数据源 + nacos: + server-addr: @config.server-addr@ + # 指定配置文件 + dataId: ${spring.application.name}-flow-rules + # 指定分组 + groupId: SENTINEL_GROUP + # 指定配置文件规则类型 + rule-type: flow + # 指定配置文件数据格式 + data-type: json + #降级规则 + degrade: + nacos: + server-addr: @config.server-addr@ + dataId: ${spring.application.name}-degrade-rules + groupId: SENTINEL_GROUP + rule-type: degrade + data-type: json + #系统规则 + system: + nacos: + server-addr: @config.server-addr@ + dataId: ${spring.application.name}-system-rules + groupId: SENTINEL_GROUP + rule-type: system + data-type: json + #授权规则 + authority: + nacos: + server-addr: @config.server-addr@ + dataId: ${spring.application.name}-authority-rules + groupId: SENTINEL_GROUP + rule-type: authority + data-type: json + #热点参数 + param-flow: + nacos: + server-addr: @config.server-addr@ + dataId: ${spring.application.name}-param-rules + groupId: SENTINEL_GROUP + rule-type: param-flow + data-type: json + #网关流控规则 + gw-flow: + nacos: + server-addr: @config.server-addr@ + dataId: ${spring.application.name}-flow-rules + groupId: SENTINEL_GROUP + rule-type: gw-flow + data-type: json + #API流控规则 + gw-api-group: + nacos: + server-addr: @config.server-addr@ + dataId: ${spring.application.name}-api-rules + groupId: SENTINEL_GROUP + rule-type: gw-api-group + data-type: json diff --git a/test-server-cloud/test-cloud-gateway/src/main/resources/logback-spring.xml b/test-server-cloud/test-cloud-gateway/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..50b9921 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/main/resources/logback-spring.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n + + + + + + + + ${LOG_HOME}/jeecg-gateway-%d{yyyy-MM-dd}.%i.log + + 30 + 10MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-cloud-gateway/src/test/java/TestRoutes.java b/test-server-cloud/test-cloud-gateway/src/test/java/TestRoutes.java new file mode 100644 index 0000000..99596c2 --- /dev/null +++ b/test-server-cloud/test-cloud-gateway/src/test/java/TestRoutes.java @@ -0,0 +1,37 @@ +import com.ghb.base.loader.vo.PredicatesVo; +import org.junit.jupiter.api.Test; + +import java.util.*; + +/** + * @Description: 测试 + * @author: lsq + * @date: 2023年10月13日 11:32 + */ +public class TestRoutes { + + @Test + public void TestRoutes() { + List list = new ArrayList<>(); + PredicatesVo a = new PredicatesVo(); + a.setName("path"); + String[] aArr={"/sys/**","/eoa/**"}; + a.setArgs(Arrays.asList(aArr)); + list.add(a); + + PredicatesVo b = new PredicatesVo(); + b.setName("path"); + String[] bArr={"/sys/**","/demo/**"}; + b.setArgs(Arrays.asList(bArr)); + list.add(b); + + Map> groupedPredicates = new HashMap<>(); + for (PredicatesVo predicatesVo : list) { + String name = predicatesVo.getName(); + List args1 = predicatesVo.getArgs(); + groupedPredicates.computeIfAbsent(name, k -> new ArrayList<>()).addAll(args1); + } + System.out.println(groupedPredicates); + } + +} diff --git a/test-server-cloud/test-cloud-nacos/Dockerfile b/test-server-cloud/test-cloud-nacos/Dockerfile new file mode 100644 index 0000000..7c2b3e4 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/Dockerfile @@ -0,0 +1,45 @@ +# ============================================ +# Nacos 微服务 — 多阶段构建(SB 2.7.18 独立编译) +# Nacos 使用 Spring Boot 2.7.18 作 parent, +# 与主项目(SB 3.5.5)不在同一 reactor,需单独编译 +# ============================================ + +# ---- Stage 1: 编译 ---- +FROM maven:3.9-eclipse-temurin-17 AS builder + +WORKDIR /build + +# 只复制 nacos 模块(独立编译,不依赖项目内其他模块) +COPY test-server-cloud/test-cloud-nacos/pom.xml ./ + +# 下载依赖 +RUN mvn dependency:go-offline -B || true + +# 复制 nacos 源码 +COPY test-server-cloud/test-cloud-nacos/src ./src + +# 编译 nacos 模块 +RUN mvn package -Dmaven.test.skip=true + +# ---- Stage 2: 运行 ---- +FROM eclipse-temurin:17-jre + +LABEL maintainer="ghb-base deploy" + +ENV TZ=Asia/Shanghai +RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime + +WORKDIR /app + +# 从编译阶段复制 nacos jar +COPY --from=builder /build/target/*.jar app.jar + +# 启动脚本(根据环境变量生成 application.properties) +COPY test-server-cloud/test-cloud-nacos/startup.sh /app/startup.sh +RUN chmod +x /app/startup.sh + +EXPOSE 8848 + +ENV JAVA_OPTS="-Xms512m -Xmx512m" + +ENTRYPOINT ["/app/startup.sh"] diff --git a/test-server-cloud/test-cloud-nacos/README.md b/test-server-cloud/test-cloud-nacos/README.md new file mode 100644 index 0000000..e8031c3 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/README.md @@ -0,0 +1,16 @@ +访问地址: http://localhost:8848/nacos +账号密码:nacos/nacos + + +# 使用方法 + +- 1、目前只做了关闭鉴权模式 +- 2、此项目与官方同步,只是为了简化微服务部署 +- 3、如何不用此模块,使用自己的naocs,请创建下面目录中的配置文件 + 目录:jeecg-cloud-nacos/docs/config + 配置文件: YAML + + +# 常见问题 +- UnsupportedOperationException: Cannot determine JNI library name for ARCH='x86' OS='windows 10' + 解决方案:http://t.zoukankan.com/mindzone-p-15808190.html \ No newline at end of file diff --git a/test-server-cloud/test-cloud-nacos/docs/config/jeecg-dev.yaml b/test-server-cloud/test-cloud-nacos/docs/config/jeecg-dev.yaml new file mode 100644 index 0000000..5ccc32b --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/docs/config/jeecg-dev.yaml @@ -0,0 +1,147 @@ +spring: + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: + initial-size: 5 + min-idle: 5 + maxActive: 20 + maxWait: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + filters: stat,wall,slf4j + connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000 + datasource: + master: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + database: 0 + host: jeecg-boot-redis + password: + port: 6379 + rabbitmq: + host: jeecg-boot-rabbitmq + username: guest + password: guest + port: 5672 + publisher-confirms: true + publisher-returns: true + virtual-host: / + listener: + simple: + acknowledge-mode: manual + concurrency: 1 + max-concurrency: 1 + retry: + enabled: true +minidao: + base-package: com.ghb.base.modules.jmreport.*,com.ghb.base.modules.drag.* +jeecg: + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys + uploadType: local + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + upload: /opt/upFiles + webapp: /opt/webapp + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/** + oss: + endpoint: oss-cn-beijing.aliyuncs.com + accessKey: ?? + secretKey: ?? + bucketName: jeecgdev + staticDomain: ?? + elasticsearch: + cluster-name: jeecg-ES + cluster-nodes: jeecg-boot-es:9200 + check-enabled: false + file-view-domain: 127.0.0.1:8012 + minio: + minio_url: http://minio.jeecg.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + jmreport: + mode: dev + is_verify_token: false + verify_methods: remove,delete,save,add,update + wps: + domain: https://wwo.wps.cn/office/ + appid: ?? + appsecret: ?? + xxljob: + enabled: false + adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '' + logPath: logs/jeecg/job/jobhandler/ + logRetentionDays: 30 + redisson: + address: jeecg-boot-redis:6379 + password: + type: STANDALONE + enabled: true +logging: + level: + com.ghb.base.modules.system.mapper : info +cas: + prefixUrl: http://localhost:8888/cas +knife4j: + production: false + basic: + enable: false + username: jeecg + password: jeecg1314 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback + cache: + type: default + prefix: 'demo::' + timeout: 1h +third-app: + enabled: false + type: + WECHAT_ENTERPRISE: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ?? + DINGTALK: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ?? \ No newline at end of file diff --git a/test-server-cloud/test-cloud-nacos/docs/config/jeecg-gateway-dev.yaml b/test-server-cloud/test-cloud-nacos/docs/config/jeecg-gateway-dev.yaml new file mode 100644 index 0000000..6ecb34d --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/docs/config/jeecg-gateway-dev.yaml @@ -0,0 +1,14 @@ +jeecg: + route: + config: + #type:database nacos yml + data-type: database + group: DEFAULT_GROUP + data-id: test-gateway-router +spring: + data: + redis: + database: 0 + host: jeecg-boot-redis + port: 6379 + password: \ No newline at end of file diff --git a/test-server-cloud/test-cloud-nacos/docs/config/jeecg-gateway-router.json b/test-server-cloud/test-cloud-nacos/docs/config/jeecg-gateway-router.json new file mode 100644 index 0000000..27434f5 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/docs/config/jeecg-gateway-router.json @@ -0,0 +1,52 @@ +[{ + "id": "test-system", + "order": 0, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/sys/**", + "_genkey_1": "/jmreport/**", + "_genkey_3": "/online/**", + "_genkey_4": "/generic/**" + } + }], + "filters": [], + "uri": "lb://test-system" +}, { + "id": "test-demo", + "order": 1, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/mock/**", + "_genkey_1": "/test/**", + "_genkey_2": "/bigscreen/template1/**", + "_genkey_3": "/bigscreen/template2/**" + } + }], + "filters": [], + "uri": "lb://test-demo" +}, { + "id": "test-system-websocket", + "order": 2, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/websocket/**", + "_genkey_1": "/newsWebsocket/**" + } + }], + "filters": [], + "uri": "lb:ws://test-system" +}, { + "id": "test-demo-websocket", + "order": 3, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/vxeSocket/**" + } + }], + "filters": [], + "uri": "lb:ws://test-demo" +}] \ No newline at end of file diff --git a/test-server-cloud/test-cloud-nacos/docs/config/jeecg.yaml b/test-server-cloud/test-cloud-nacos/docs/config/jeecg.yaml new file mode 100644 index 0000000..26de44d --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/docs/config/jeecg.yaml @@ -0,0 +1,100 @@ +server: + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* +management: + health: + mail: + enabled: false + endpoints: + web: + exposure: + include: "*" + health: + sensitive: true + endpoint: + health: + show-details: ALWAYS +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + host: smtp.163.com + username: jeecgos@163.com + password: ?? + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + quartz: + job-store-type: jdbc + initialize-schema: embedded + auto-startup: false + startup-delay: 1s + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + activiti: + check-process-definitions: false + async-executor-activate: false + job-executor-activate: false + jpa: + open-in-view: false + freemarker: + suffix: .ftl + content-type: text/html + charset: UTF-8 + cache: false + prefer-file-system-access: false + template-loader-path: + - classpath:/templates + mvc: + static-path-pattern: /** + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure +mybatis-plus: + mapper-locations: classpath*:org/jeecg/modules/**/xml/*Mapper.xml + global-config: + banner: false + db-config: + id-type: ASSIGN_ID + table-underline: true + configuration: + call-setters-on-nulls: true \ No newline at end of file diff --git a/test-server-cloud/test-cloud-nacos/docs/db/nacos_dm.sql b/test-server-cloud/test-cloud-nacos/docs/db/nacos_dm.sql new file mode 100644 index 0000000..7f2b6a3 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/docs/db/nacos_dm.sql @@ -0,0 +1,3275 @@ +CREATE TABLE "NACOS"."CONFIG_INFO" +( + "ID" BIGINT IDENTITY(1,1) NOT NULL, + "DATA_ID" VARCHAR(255) NOT NULL, + "GROUP_ID" VARCHAR(128) NULL, + "CONTENT" CLOB NOT NULL, + "MD5" VARCHAR(32) NULL, + "GMT_CREATE" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "GMT_MODIFIED" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "SRC_USER" TEXT NULL, + "SRC_IP" VARCHAR(50) NULL, + "APP_NAME" VARCHAR(128) NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NULL, + "C_DESC" VARCHAR(256) NULL, + "C_USE" VARCHAR(64) NULL, + "EFFECT" VARCHAR(64) NULL, + "TYPE" VARCHAR(64) NULL, + "C_SCHEMA" TEXT NULL, + "ENCRYPTED_DATA_KEY" TEXT NOT NULL +); +CREATE TABLE "NACOS"."CONFIG_INFO_AGGR" +( + "ID" BIGINT IDENTITY(1,1) NOT NULL, + "DATA_ID" VARCHAR(255) NOT NULL, + "GROUP_ID" VARCHAR(128) NOT NULL, + "DATUM_ID" VARCHAR(255) NOT NULL, + "CONTENT" CLOB NOT NULL, + "GMT_MODIFIED" TIMESTAMP(0) NOT NULL, + "APP_NAME" VARCHAR(128) NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NULL +); +CREATE TABLE "NACOS"."CONFIG_INFO_BETA" +( + "ID" BIGINT IDENTITY(1,1) NOT NULL, + "DATA_ID" VARCHAR(255) NOT NULL, + "GROUP_ID" VARCHAR(128) NOT NULL, + "APP_NAME" VARCHAR(128) NULL, + "CONTENT" CLOB NOT NULL, + "BETA_IPS" VARCHAR(1024) NULL, + "MD5" VARCHAR(32) NULL, + "GMT_CREATE" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "GMT_MODIFIED" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "SRC_USER" TEXT NULL, + "SRC_IP" VARCHAR(50) NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NULL, + "ENCRYPTED_DATA_KEY" TEXT NOT NULL +); +CREATE TABLE "NACOS"."CONFIG_INFO_TAG" +( + "ID" BIGINT IDENTITY(1,1) NOT NULL, + "DATA_ID" VARCHAR(255) NOT NULL, + "GROUP_ID" VARCHAR(128) NOT NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NULL, + "TAG_ID" VARCHAR(128) NOT NULL, + "APP_NAME" VARCHAR(128) NULL, + "CONTENT" CLOB NOT NULL, + "MD5" VARCHAR(32) NULL, + "GMT_CREATE" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "GMT_MODIFIED" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "SRC_USER" TEXT NULL, + "SRC_IP" VARCHAR(50) NULL +); +CREATE TABLE "NACOS"."CONFIG_TAGS_RELATION" +( + "ID" BIGINT NOT NULL, + "TAG_NAME" VARCHAR(128) NOT NULL, + "TAG_TYPE" VARCHAR(64) NULL, + "DATA_ID" VARCHAR(255) NOT NULL, + "GROUP_ID" VARCHAR(128) NOT NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NULL, + "NID" BIGINT IDENTITY(1,1) NOT NULL +); +CREATE TABLE "NACOS"."GROUP_CAPACITY" +( + "ID" BIGINT IDENTITY(1,1) NOT NULL, + "GROUP_ID" VARCHAR(128) DEFAULT '' + NOT NULL, + "QUOTA" BIGINT DEFAULT 0 + NOT NULL, + "USAGE" BIGINT DEFAULT 0 + NOT NULL, + "MAX_SIZE" BIGINT DEFAULT 0 + NOT NULL, + "MAX_AGGR_COUNT" BIGINT DEFAULT 0 + NOT NULL, + "MAX_AGGR_SIZE" BIGINT DEFAULT 0 + NOT NULL, + "MAX_HISTORY_COUNT" BIGINT DEFAULT 0 + NOT NULL, + "GMT_CREATE" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "GMT_MODIFIED" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL +); +CREATE TABLE "NACOS"."HIS_CONFIG_INFO" +( + "ID" DECIMAL(20,0) NOT NULL, + "NID" BIGINT IDENTITY(1,1) NOT NULL, + "DATA_ID" VARCHAR(255) NOT NULL, + "GROUP_ID" VARCHAR(128) NOT NULL, + "APP_NAME" VARCHAR(128) NULL, + "CONTENT" CLOB NOT NULL, + "MD5" VARCHAR(32) NULL, + "GMT_CREATE" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "GMT_MODIFIED" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "SRC_USER" TEXT NULL, + "SRC_IP" VARCHAR(50) NULL, + "OP_TYPE" CHAR(10) NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NULL, + "ENCRYPTED_DATA_KEY" TEXT NOT NULL +); +CREATE TABLE "NACOS"."PERMISSIONS" +( + "ROLE" VARCHAR(50) NOT NULL, + "RESOURCE" VARCHAR(255) NOT NULL, + "ACTION" VARCHAR(8) NOT NULL +); +CREATE TABLE "NACOS"."ROLES" +( + "USERNAME" VARCHAR(50) NOT NULL, + "ROLE" VARCHAR(50) NOT NULL +); +CREATE TABLE "NACOS"."TENANT_CAPACITY" +( + "ID" BIGINT IDENTITY(1,1) NOT NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NOT NULL, + "QUOTA" BIGINT DEFAULT 0 + NOT NULL, + "USAGE" BIGINT DEFAULT 0 + NOT NULL, + "MAX_SIZE" BIGINT DEFAULT 0 + NOT NULL, + "MAX_AGGR_COUNT" BIGINT DEFAULT 0 + NOT NULL, + "MAX_AGGR_SIZE" BIGINT DEFAULT 0 + NOT NULL, + "MAX_HISTORY_COUNT" BIGINT DEFAULT 0 + NOT NULL, + "GMT_CREATE" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL, + "GMT_MODIFIED" TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP() + NOT NULL +); +CREATE TABLE "NACOS"."TENANT_INFO" +( + "ID" BIGINT IDENTITY(1,1) NOT NULL, + "KP" VARCHAR(128) NOT NULL, + "TENANT_ID" VARCHAR(128) DEFAULT '' + NULL, + "TENANT_NAME" VARCHAR(128) DEFAULT '' + NULL, + "TENANT_DESC" VARCHAR(256) NULL, + "CREATE_SOURCE" VARCHAR(32) NULL, + "GMT_CREATE" BIGINT NOT NULL, + "GMT_MODIFIED" BIGINT NOT NULL +); +CREATE TABLE "NACOS"."USERS" +( + "USERNAME" VARCHAR(50) NOT NULL, + "PASSWORD" VARCHAR(500) NOT NULL, + "ENABLED" TINYINT NOT NULL +); +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO" ON; +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(2,'jeecg-dev.yaml','DEFAULT_GROUP','spring: + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,''wall''用于防火墙 + filters: stat,wall,slf4j + wall: + selectWhereAlwayTrueCheck: false + stat: + merge-sql: true + slow-sql-millis: 5000 + + datasource: + master: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecgbootsy3_6?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + # 多数据源配置 + #multi-datasource1: + #url: jdbc:mysql://localhost:3306/jeecgboot2?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + #username: root + #password: root + #driver-class-name: com.mysql.cj.jdbc.Driver + #redis 配置 + redis: + database: 0 + host: jeecg-boot-redis + lettuce: + pool: + max-active: 8 #最大连接数据库连接数,设 0 为没有限制 + max-idle: 8 #最大等待连接中的数量,设 0 为没有限制 + max-wait: -1ms #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 + min-idle: 0 #最小等待连接中的数量,设 0 为没有限制 + shutdown-timeout: 100ms + password: + port: 6379 + #mongodb + data: + mongodb: + #有密码连接 账号密码包含特殊字符的需要用URLEncoder编码 库名必填 + #uri: mongodb://jeecgdev:jeecgd_89@111.225.222.176:27017/jeecgdev + uri: mongodb://jeecg:123456@jeecg-boot-mongo:27017/jeecg?readPreference=secondaryPreferred&maxIdleTimeMS=60000&waitQueueTimeoutMS=2000&minPoolSize=5&maxPoolSize=100&maxLifeTimeMS=0&connectTimeoutMS=2000&socketTimeoutMS=2000 + #集群方式 + #uri: mongodb://192.168.0.221:27017,192.168.0.221:27018/imgdb + print: true #是否打印查询语句 + slowQuery: true #是否记录慢查询到数据库中 + slowTime: 1000 #慢查询最短时间,默认为1000毫秒 + #rabbitmq配置 + rabbitmq: + host: jeecg-boot-rabbitmq + username: guest + password: guest + port: 5672 + publisher-confirms: true + publisher-returns: true + virtual-host: / + listener: + simple: + acknowledge-mode: manual + #消费者的最小数量 + concurrency: 1 + #消费者的最大数量 + max-concurrency: 1 + #是否支持重试 + retry: + enabled: true +#jeecg专用配置 +minidao: + base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.* +jeecg: + firewall: + dataSourceSafe: false + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + # 本地:local\Minio:minio\阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path : + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储配置 + oss: + endpoint: oss-cn-beijing.aliyuncs.com + accessKey: ?? + secretKey: ?? + bucketName: jeecgdev + staticDomain: ?? + # ElasticSearch 6设置 + elasticsearch: + cluster-name: jeecg-ES + cluster-nodes: 127.0.0.1:9200 + check-enabled: false + # 表单设计器配置 + desform: + # 主题颜色(仅支持 16进制颜色代码) + theme-color: "#1890ff" + # 文件、图片上传方式,可选项:qiniu(七牛云)、system(跟随系统配置) + upload-type: system + map: + # 配置百度地图的AK,申请地址:https://lbs.baidu.com/apiconsole/key?application=key#/home + baidu: ?? + # 在线预览文件服务器地址配置 + file-view-domain: 127.0.0.1:8012 + # minio文件上传 + minio: + minio_url: http://minio.jeecg.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + saasMode: + firewall: + dataSourceSafe: false + lowCodeMode: dev + ai-chat: + enabled: false + apiKey: "????" + apiHost: "https://api.openai.com" + timeout: 60 + #Wps在线文档 + wps: + domain: https://wwo.wps.cn/office/ + appid: ?? + appsecret: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '''' + logPath: logs/jeecg/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: jeecg-boot-redis:6379 + password: + type: STANDALONE + enabled: true +#Mybatis输出sql日志 +logging: + level: + org.jeecg.modules.system.mapper: info +#cas单点登录 +cas: + prefixUrl: http://localhost:8888/cas +#swagger +knife4j: + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: jeecg + password: jeecg1314 + +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback + agent-id: 1000002 + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback + cache: + type: default + prefix: ''demo::'' + timeout: 1h +#第三方APP对接 +third-app: + enabled: false + type: + #企业微信 + WECHAT_ENTERPRISE: + enabled: false + #CORP_ID + client-id: ?? + #SECRET + client-secret: ?? + agent-id: ?? + #自建应用秘钥(新版企微需要配置) + # agent-app-secret: ?? + #钉钉 + DINGTALK: + enabled: false + # appKey + client-id: ?? + # appSecret + client-secret: ?? + agent-id: ??','350e31a280673586f2203956da576136',TO_DATE('2024-07-09 14:30:06','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:06','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','','',null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(3,'jeecg.yaml','DEFAULT_GROUP','server: + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* +management: + health: + mail: + enabled: false + endpoints: + web: + exposure: + include: "*" #暴露所有节点 + health: + sensitive: true #关闭过滤敏感信息 + endpoint: + health: + show-details: ALWAYS #显示详细信息 +flowable: + # 自动部署验证设置:true-开启(默认)、false-关闭 + check-process-definitions: false + #配置项可以设置流程引擎启动和关闭时数据库执行的策略 + database-schema-update: false + #保存历史数据级别设置为full最高级别,便于历史数据的追溯 + history-level: full + #开启定时任务 + async-executor-activate: true +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + host: smtp.163.com + username: jeecgos@163.com + password: ?? + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + initialize-schema: embedded + #设置自动启动,默认为 true + auto-startup: false + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + #class: org.quartz.impl.jdbcjobstore.JobStoreTX + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + jpa: + open-in-view: false + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+ 手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true','94755a848afefef22e34ff83668ec4f7',TO_DATE('2024-07-09 14:30:06','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:06','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','','',null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(4,'jeecg-gateway-router.json','DEFAULT_GROUP','[{ + "id": "jeecg-system", + "order": 0, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/sys/**", + "_genkey_1": "/eoa/**", + "_genkey_2": "/joa/**", + "_genkey_3": "/jmreport/**", + "_genkey_4": "/bigscreen/**", + "_genkey_5": "/desform/**", + "_genkey_6": "/online/**", + "_genkey_8": "/act/**", + "_genkey_9": "/plug-in/**", + "_genkey_10": "/generic/**", + "_genkey_11": "/v1/**", + "_genkey_12": "/desflow/**" + } + }], + "filters": [], + "uri": "lb://jeecg-system" +}, { + "id": "jeecg-demo", + "order": 1, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/mock/**", + "_genkey_1": "/test/**", + "_genkey_2": "/bigscreen/template1/**", + "_genkey_3": "/bigscreen/template2/**" + } + }], + "filters": [], + "uri": "lb://jeecg-demo" +}, { + "id": "jeecg-system-websocket", + "order": 2, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/websocket/**", + "_genkey_1": "/eoaSocket/**", + "_genkey_2": "/newsWebsocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-system" +}, { + "id": "jeecg-demo-websocket", + "order": 3, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/vxeSocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-demo" +}]','c9eff51f264ebe266c07ad1c5b6778e2',TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','','',null,null,'json',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(5,'jeecg-gateway-dev.yaml','DEFAULT_GROUP','jeecg: + route: + config: + #路由加载模式: database、nacos、yml + data-type: database + #Nacos模式,读取配置文件jeecg-gateway-router.json(固定) + group: DEFAULT_GROUP + data-id: jeecg-gateway-router +spring: + #redis配置 + redis: + database: 0 + host: jeecg-boot-redis + port: 6379 + password: +#swagger +knife4j: + #开启生产环境屏蔽 + production: false','8fea1277e460b477987521aecf432150',TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','','',null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(6,'jeecg-sharding.yaml','DEFAULT_GROUP','spring: + shardingsphere: + datasource: + names: ds0 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: root + type: com.alibaba.druid.pool.DruidDataSource + props: + sql-show: true + rules: + sharding: + binding-tables: sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + tables: + sys_log: + actual-data-nodes: ds0.sys_log$->{0..1} + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','5d7aad99a23e68589e93facd1b221aea',TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','',null,null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(7,'jeecg-sharding-multi.yaml','DEFAULT_GROUP','spring: + shardingsphere: + datasource: + names: ds0,ds1 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + ds1: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + props: + sql-show: true + rules: + replica-query: + load-balancers: + round-robin: + type: ROUND_ROBIN + props: + default: 0 + data-sources: + prds: + primary-data-source-name: ds0 + replica-data-source-names: ds1 + load-balancer-name: round_robin + sharding: + binding-tables: + - sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + database-inline: + type: INLINE + props: + algorithm-expression: ds$->{operate_type % 2} + tables: + sys_log: + actual-data-nodes: ds$->{0..1}.sys_log$->{0..1} + database-strategy: + standard: + sharding-column: operate_type + sharding-algorithm-name: database-inline + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','ef2f42fb2dda43cd0d4397a820f3144e',TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','',null,null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(14,'jeecg-dev.yaml','DEFAULT_GROUP','spring: + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: + initial-size: 5 + min-idle: 5 + maxActive: 20 + maxWait: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + filters: stat,wall,slf4j + wall: + selectWhereAlwayTrueCheck: false + stat: + merge-sql: true + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + database: 0 + host: jeecg-boot-redis + password: + port: 6379 + rabbitmq: + host: jeecg-boot-rabbitmq + username: guest + password: guest + port: 5672 + publisher-confirms: true + publisher-returns: true + virtual-host: / + listener: + simple: + acknowledge-mode: manual + concurrency: 1 + max-concurrency: 1 + retry: + enabled: true + flyway: + enabled: false + encoding: UTF-8 + locations: classpath:flyway/sql/mysql + sql-migration-prefix: V + sql-migration-separator: __ + placeholder-prefix: ''#('' + placeholder-suffix: ) + sql-migration-suffixes: .sql + validate-on-migrate: true + baseline-on-migrate: true + clean-disabled: true +minidao: + base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.* +jeecg: + firewall: + dataSourceSafe: false + lowCodeMode: dev + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys + uploadType: local + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + upload: /opt/upFiles + webapp: /opt/webapp + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/** + oss: + endpoint: oss-cn-beijing.aliyuncs.com + accessKey: ?? + secretKey: ?? + bucketName: jeecgdev + staticDomain: ?? + elasticsearch: + cluster-name: jeecg-ES + cluster-nodes: jeecg-boot-es:9200 + check-enabled: false + file-view-domain: 127.0.0.1:8012 + minio: + minio_url: http://minio.jeecg.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + jmreport: + saasMode: + firewall: + dataSourceSafe: false + lowCodeMode: dev + wps: + domain: https://wwo.wps.cn/office/ + appid: ?? + appsecret: ?? + xxljob: + enabled: false + adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '''' + logPath: logs/jeecg/job/jobhandler/ + logRetentionDays: 30 + redisson: + address: jeecg-boot-redis:6379 + password: + type: STANDALONE + enabled: true + ai-chat: + enabled: false + apiKey: "????" + apiHost: "https://api.openai.com" + timeout: 60 +logging: + level: + org.jeecg.modules.system.mapper : info +cas: + prefixUrl: http://localhost:8888/cas +knife4j: + production: false + basic: + enable: false + username: jeecg + password: jeecg1314 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback + cache: + type: default + prefix: ''demo::'' + timeout: 1h +third-app: + enabled: false + type: + WECHAT_ENTERPRISE: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ?? + DINGTALK: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ??','91c29720dfb424916a769201a25200cf',TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','springboot3','',null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(15,'jeecg.yaml','DEFAULT_GROUP','server: + undertow: + # max-http-post-size: 10MB + worker-threads: 16 + buffers: + websocket: 8192 + io: 16384 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* +management: + health: + mail: + enabled: false + endpoints: + web: + exposure: + include: "*" + health: + sensitive: true + endpoint: + health: + show-details: ALWAYS +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + host: smtp.163.com + username: jeecgos@163.com + password: ?? + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + quartz: + job-store-type: jdbc + initialize-schema: embedded + auto-startup: false + startup-delay: 1s + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + activiti: + check-process-definitions: false + async-executor-activate: false + job-executor-activate: false + jpa: + open-in-view: false + freemarker: + suffix: .ftl + content-type: text/html + charset: UTF-8 + cache: false + prefer-file-system-access: false + template-loader-path: + - classpath:/templates + mvc: + static-path-pattern: /** + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration +mybatis-plus: + mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml + global-config: + banner: false + db-config: + id-type: ASSIGN_ID + table-underline: true + configuration: + call-setters-on-nulls: true','ce1ca3b6f8431e884aed94ab29be43a9',TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','springboot3','',null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(16,'jeecg-gateway-router.json','DEFAULT_GROUP','[{ + "id": "jeecg-system", + "order": 0, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/sys/**", + "_genkey_1": "/jmreport/**", + "_genkey_3": "/online/**", + "_genkey_4": "/generic/**", + "_genkey_5": "/oauth2/**", + "_genkey_6": "/drag/**", + "_genkey_7": "/actuator/**" + } + }], + "filters": [], + "uri": "lb://jeecg-system" +}, { + "id": "jeecg-demo", + "order": 1, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/mock/**", + "_genkey_1": "/test/**", + "_genkey_2": "/bigscreen/template1/**", + "_genkey_3": "/bigscreen/template2/**" + } + }], + "filters": [], + "uri": "lb://jeecg-demo" +}, { + "id": "jeecg-system-websocket", + "order": 2, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/websocket/**", + "_genkey_1": "/newsWebsocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-system" +}, { + "id": "jeecg-demo-websocket", + "order": 3, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/vxeSocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-demo" +}]','9794beb09d30bc6b835f2ee870781587',TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','springboot3','',null,null,'json',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(17,'jeecg-sharding.yaml','DEFAULT_GROUP','spring: + shardingsphere: + datasource: + names: ds0 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: root + type: com.alibaba.druid.pool.DruidDataSource + props: + sql-show: true + rules: + sharding: + binding-tables: sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + tables: + sys_log: + actual-data-nodes: ds0.sys_log$->{0..1} + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','a93fa455c32cd37ca84631d2bbe13005',TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','springboot3','',null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(18,'jeecg-gateway-dev.yaml','DEFAULT_GROUP','jeecg: + route: + config: + #type:database nacos yml + data-type: database + data-id: jeecg-gateway-router +spring: + data: + redis: + database: 0 + host: jeecg-boot-redis + port: 6379 + password: +knife4j: + production: false','19d7cd93eeb85a582c8a6942d499c7f7',TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','springboot3','',null,null,'yaml',null,''); +INSERT INTO "NACOS"."CONFIG_INFO"("ID","DATA_ID","GROUP_ID","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","APP_NAME","TENANT_ID","C_DESC","C_USE","EFFECT","TYPE","C_SCHEMA","ENCRYPTED_DATA_KEY") VALUES(19,'jeecg-sharding-multi.yaml','DEFAULT_GROUP','spring: + shardingsphere: + datasource: + names: ds0,ds1 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + ds1: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + props: + sql-show: true + rules: + replica-query: + load-balancers: + round-robin: + type: ROUND_ROBIN + props: + default: 0 + data-sources: + prds: + primary-data-source-name: ds0 + replica-data-source-names: ds1 + load-balancer-name: round_robin + sharding: + binding-tables: + - sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + database-inline: + type: INLINE + props: + algorithm-expression: ds$->{operate_type % 2} + tables: + sys_log: + actual-data-nodes: ds$->{0..1}.sys_log$->{0..1} + database-strategy: + standard: + sharding-column: operate_type + sharding-algorithm-name: database-inline + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','0fc2b030ca8c0008f148c84ecbd2a8c7',TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','','springboot3','',null,null,'yaml',null,''); + +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO" OFF; +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO_AGGR" ON; +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO_AGGR" OFF; +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO_BETA" ON; +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO_BETA" OFF; +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO_TAG" ON; +SET IDENTITY_INSERT "NACOS"."CONFIG_INFO_TAG" OFF; +SET IDENTITY_INSERT "NACOS"."CONFIG_TAGS_RELATION" ON; +SET IDENTITY_INSERT "NACOS"."CONFIG_TAGS_RELATION" OFF; +SET IDENTITY_INSERT "NACOS"."GROUP_CAPACITY" ON; +SET IDENTITY_INSERT "NACOS"."GROUP_CAPACITY" OFF; +SET IDENTITY_INSERT "NACOS"."HIS_CONFIG_INFO" ON; +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,1,'1','DEFAULT_GROUP','','1','c4ca4238a0b923820dcc509a6f75849b',TO_DATE('2024-07-09 14:24:05','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:24:06','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(1,2,'1','DEFAULT_GROUP','','1','c4ca4238a0b923820dcc509a6f75849b',TO_DATE('2024-07-09 14:24:07','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:24:08','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','D','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,3,'jeecg-dev.yaml','DEFAULT_GROUP','','spring: + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置) + # 连接池的配置信息 + # 初始化大小,最小,最大 + initial-size: 5 + min-idle: 5 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 打开PSCache,并且指定每个连接上PSCache的大小 + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,''wall''用于防火墙 + filters: stat,wall,slf4j + wall: + selectWhereAlwayTrueCheck: false + stat: + merge-sql: true + slow-sql-millis: 5000 + + datasource: + master: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecgbootsy3_6?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + # 多数据源配置 + #multi-datasource1: + #url: jdbc:mysql://localhost:3306/jeecgboot2?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + #username: root + #password: root + #driver-class-name: com.mysql.cj.jdbc.Driver + #redis 配置 + redis: + database: 0 + host: jeecg-boot-redis + lettuce: + pool: + max-active: 8 #最大连接数据库连接数,设 0 为没有限制 + max-idle: 8 #最大等待连接中的数量,设 0 为没有限制 + max-wait: -1ms #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 + min-idle: 0 #最小等待连接中的数量,设 0 为没有限制 + shutdown-timeout: 100ms + password: + port: 6379 + #mongodb + data: + mongodb: + #有密码连接 账号密码包含特殊字符的需要用URLEncoder编码 库名必填 + #uri: mongodb://jeecgdev:jeecgd_89@111.225.222.176:27017/jeecgdev + uri: mongodb://jeecg:123456@jeecg-boot-mongo:27017/jeecg?readPreference=secondaryPreferred&maxIdleTimeMS=60000&waitQueueTimeoutMS=2000&minPoolSize=5&maxPoolSize=100&maxLifeTimeMS=0&connectTimeoutMS=2000&socketTimeoutMS=2000 + #集群方式 + #uri: mongodb://192.168.0.221:27017,192.168.0.221:27018/imgdb + print: true #是否打印查询语句 + slowQuery: true #是否记录慢查询到数据库中 + slowTime: 1000 #慢查询最短时间,默认为1000毫秒 + #rabbitmq配置 + rabbitmq: + host: jeecg-boot-rabbitmq + username: guest + password: guest + port: 5672 + publisher-confirms: true + publisher-returns: true + virtual-host: / + listener: + simple: + acknowledge-mode: manual + #消费者的最小数量 + concurrency: 1 + #消费者的最大数量 + max-concurrency: 1 + #是否支持重试 + retry: + enabled: true +#jeecg专用配置 +minidao: + base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.* +jeecg: + firewall: + dataSourceSafe: false + lowCodeMode: dev + # 签名密钥串(前后端要一致,正式发布请自行修改) + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + # 本地:local\Minio:minio\阿里云:alioss + uploadType: local + # 前端访问地址 + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path : + #文件上传根目录 设置 + upload: /opt/upFiles + #webapp文件路径 + webapp: /opt/webapp + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/** + #阿里云oss存储配置 + oss: + endpoint: oss-cn-beijing.aliyuncs.com + accessKey: ?? + secretKey: ?? + bucketName: jeecgdev + staticDomain: ?? + # ElasticSearch 6设置 + elasticsearch: + cluster-name: jeecg-ES + cluster-nodes: 127.0.0.1:9200 + check-enabled: false + # 表单设计器配置 + desform: + # 主题颜色(仅支持 16进制颜色代码) + theme-color: "#1890ff" + # 文件、图片上传方式,可选项:qiniu(七牛云)、system(跟随系统配置) + upload-type: system + map: + # 配置百度地图的AK,申请地址:https://lbs.baidu.com/apiconsole/key?application=key#/home + baidu: ?? + # 在线预览文件服务器地址配置 + file-view-domain: 127.0.0.1:8012 + # minio文件上传 + minio: + minio_url: http://minio.jeecg.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + #大屏报表参数设置 + jmreport: + saasMode: + firewall: + dataSourceSafe: false + lowCodeMode: dev + ai-chat: + enabled: false + apiKey: "????" + apiHost: "https://api.openai.com" + timeout: 60 + #Wps在线文档 + wps: + domain: https://wwo.wps.cn/office/ + appid: ?? + appsecret: ?? + #xxl-job配置 + xxljob: + enabled: false + adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '''' + logPath: logs/jeecg/job/jobhandler/ + logRetentionDays: 30 + #分布式锁配置 + redisson: + address: jeecg-boot-redis:6379 + password: + type: STANDALONE + enabled: true +#Mybatis输出sql日志 +logging: + level: + org.jeecg.modules.system.mapper: info +#cas单点登录 +cas: + prefixUrl: http://localhost:8888/cas +#swagger +knife4j: + enable: true + #开启生产环境屏蔽 + production: false + basic: + enable: false + username: jeecg + password: jeecg1314 + +#第三方登录 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback + agent-id: 1000002 + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback + cache: + type: default + prefix: ''demo::'' + timeout: 1h +#第三方APP对接 +third-app: + enabled: false + type: + #企业微信 + WECHAT_ENTERPRISE: + enabled: false + #CORP_ID + client-id: ?? + #SECRET + client-secret: ?? + agent-id: ?? + #自建应用秘钥(新版企微需要配置) + # agent-app-secret: ?? + #钉钉 + DINGTALK: + enabled: false + # appKey + client-id: ?? + # appSecret + client-secret: ?? + agent-id: ??','350e31a280673586f2203956da576136',TO_DATE('2024-07-09 14:30:05','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:06','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,4,'jeecg.yaml','DEFAULT_GROUP','','server: + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* +management: + health: + mail: + enabled: false + endpoints: + web: + exposure: + include: "*" #暴露所有节点 + health: + sensitive: true #关闭过滤敏感信息 + endpoint: + health: + show-details: ALWAYS #显示详细信息 +flowable: + # 自动部署验证设置:true-开启(默认)、false-关闭 + check-process-definitions: false + #配置项可以设置流程引擎启动和关闭时数据库执行的策略 + database-schema-update: false + #保存历史数据级别设置为full最高级别,便于历史数据的追溯 + history-level: full + #开启定时任务 + async-executor-activate: true +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + host: smtp.163.com + username: jeecgos@163.com + password: ?? + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + ## quartz定时任务,采用数据库方式 + quartz: + job-store-type: jdbc + initialize-schema: embedded + #设置自动启动,默认为 true + auto-startup: false + #延迟1秒启动定时任务 + startup-delay: 1s + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + #class: org.quartz.impl.jdbcjobstore.JobStoreTX + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + #json 时间戳统一转换 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + jpa: + open-in-view: false + #配置freemarker + freemarker: + # 设置模板后缀名 + suffix: .ftl + # 设置文档类型 + content-type: text/html + # 设置页面编码格式 + charset: UTF-8 + # 设置页面缓存 + cache: false + prefer-file-system-access: false + # 设置ftl文件路径 + template-loader-path: + - classpath:/templates + template_update_delay: 0 + # 设置静态文件路径,js,css等 + mvc: + static-path-pattern: /** + #Spring Boot 2.6+ 手动指定为ant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration +#mybatis plus 设置 +mybatis-plus: + mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; + id-type: ASSIGN_ID + # 默认数据库表下划线命名 + table-underline: true + configuration: + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 返回类型为Map,显示null对应的字段 + call-setters-on-nulls: true','94755a848afefef22e34ff83668ec4f7',TO_DATE('2024-07-09 14:30:05','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,5,'jeecg-gateway-router.json','DEFAULT_GROUP','','[{ + "id": "jeecg-system", + "order": 0, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/sys/**", + "_genkey_1": "/eoa/**", + "_genkey_2": "/joa/**", + "_genkey_3": "/jmreport/**", + "_genkey_4": "/bigscreen/**", + "_genkey_5": "/desform/**", + "_genkey_6": "/online/**", + "_genkey_8": "/act/**", + "_genkey_9": "/plug-in/**", + "_genkey_10": "/generic/**", + "_genkey_11": "/v1/**", + "_genkey_12": "/desflow/**" + } + }], + "filters": [], + "uri": "lb://jeecg-system" +}, { + "id": "jeecg-demo", + "order": 1, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/mock/**", + "_genkey_1": "/test/**", + "_genkey_2": "/bigscreen/template1/**", + "_genkey_3": "/bigscreen/template2/**" + } + }], + "filters": [], + "uri": "lb://jeecg-demo" +}, { + "id": "jeecg-system-websocket", + "order": 2, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/websocket/**", + "_genkey_1": "/eoaSocket/**", + "_genkey_2": "/newsWebsocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-system" +}, { + "id": "jeecg-demo-websocket", + "order": 3, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/vxeSocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-demo" +}]','c9eff51f264ebe266c07ad1c5b6778e2',TO_DATE('2024-07-09 14:30:05','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,6,'jeecg-gateway-dev.yaml','DEFAULT_GROUP','','jeecg: + route: + config: + #路由加载模式: database、nacos、yml + data-type: database + #Nacos模式,读取配置文件jeecg-gateway-router.json(固定) + group: DEFAULT_GROUP + data-id: jeecg-gateway-router +spring: + #redis配置 + redis: + database: 0 + host: jeecg-boot-redis + port: 6379 + password: +#swagger +knife4j: + #开启生产环境屏蔽 + production: false','8fea1277e460b477987521aecf432150',TO_DATE('2024-07-09 14:30:05','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,7,'jeecg-sharding.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: root + type: com.alibaba.druid.pool.DruidDataSource + props: + sql-show: true + rules: + sharding: + binding-tables: sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + tables: + sys_log: + actual-data-nodes: ds0.sys_log$->{0..1} + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','5d7aad99a23e68589e93facd1b221aea',TO_DATE('2024-07-09 14:30:05','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,8,'jeecg-sharding-multi.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0,ds1 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + ds1: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + props: + sql-show: true + rules: + replica-query: + load-balancers: + round-robin: + type: ROUND_ROBIN + props: + default: 0 + data-sources: + prds: + primary-data-source-name: ds0 + replica-data-source-names: ds1 + load-balancer-name: round_robin + sharding: + binding-tables: + - sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + database-inline: + type: INLINE + props: + algorithm-expression: ds$->{operate_type % 2} + tables: + sys_log: + actual-data-nodes: ds$->{0..1}.sys_log$->{0..1} + database-strategy: + standard: + sharding-column: operate_type + sharding-algorithm-name: database-inline + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','ef2f42fb2dda43cd0d4397a820f3144e',TO_DATE('2024-07-09 14:30:05','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:07','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,9,'jeecg-dev.yaml','DEFAULT_GROUP','','spring: + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: + initial-size: 5 + min-idle: 5 + maxActive: 20 + maxWait: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + filters: stat,wall,slf4j + wall: + selectWhereAlwayTrueCheck: false + stat: + merge-sql: true + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + database: 0 + host: jeecg-boot-redis + password: + port: 6379 + rabbitmq: + host: jeecg-boot-rabbitmq + username: guest + password: guest + port: 5672 + publisher-confirms: true + publisher-returns: true + virtual-host: / + listener: + simple: + acknowledge-mode: manual + concurrency: 1 + max-concurrency: 1 + retry: + enabled: true + flyway: + enabled: false + encoding: UTF-8 + locations: classpath:flyway/sql/mysql + sql-migration-prefix: V + sql-migration-separator: __ + placeholder-prefix: ''#('' + placeholder-suffix: ) + sql-migration-suffixes: .sql + validate-on-migrate: true + baseline-on-migrate: true + clean-disabled: true +minidao: + base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.* +jeecg: + firewall: + dataSourceSafe: false + lowCodeMode: dev + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + uploadType: local + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + upload: /opt/upFiles + webapp: /opt/webapp + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/** + oss: + endpoint: oss-cn-beijing.aliyuncs.com + accessKey: ?? + secretKey: ?? + bucketName: jeecgdev + staticDomain: ?? + elasticsearch: + cluster-name: jeecg-ES + cluster-nodes: jeecg-boot-es:9200 + check-enabled: false + file-view-domain: 127.0.0.1:8012 + minio: + minio_url: http://minio.jeecg.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + jmreport: + saasMode: + firewall: + dataSourceSafe: false + lowCodeMode: dev + wps: + domain: https://wwo.wps.cn/office/ + appid: ?? + appsecret: ?? + xxljob: + enabled: true + adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '''' + logPath: logs/jeecg/job/jobhandler/ + logRetentionDays: 30 + redisson: + address: jeecg-boot-redis:6379 + password: + type: STANDALONE + enabled: true + ai-chat: + enabled: false + apiKey: "????" + apiHost: "https://api.openai.com" + timeout: 60 +logging: + level: + org.jeecg.modules.system.mapper : info +cas: + prefixUrl: http://localhost:8888/cas +knife4j: + production: false + basic: + enable: false + username: jeecg + password: jeecg1314 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback + cache: + type: default + prefix: ''demo::'' + timeout: 1h +third-app: + enabled: false + type: + WECHAT_ENTERPRISE: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ?? + DINGTALK: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ??','822f70f7a278a503a02568186582ceaa',TO_DATE('2024-07-09 14:30:19','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:20','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,10,'jeecg.yaml','DEFAULT_GROUP','','server: + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* +management: + health: + mail: + enabled: false + endpoints: + web: + exposure: + include: "*" + health: + sensitive: true + endpoint: + health: + show-details: ALWAYS +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + host: smtp.163.com + username: jeecgos@163.com + password: ?? + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + quartz: + job-store-type: jdbc + initialize-schema: embedded + auto-startup: false + startup-delay: 1s + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + activiti: + check-process-definitions: false + async-executor-activate: false + job-executor-activate: false + jpa: + open-in-view: false + freemarker: + suffix: .ftl + content-type: text/html + charset: UTF-8 + cache: false + prefer-file-system-access: false + template-loader-path: + - classpath:/templates + mvc: + static-path-pattern: /** + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration +mybatis-plus: + mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml + global-config: + banner: false + db-config: + id-type: ASSIGN_ID + table-underline: true + configuration: + call-setters-on-nulls: true','94dbdad61f7e2e3ace5a4fc07bb8c2a2',TO_DATE('2024-07-09 14:30:19','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:20','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,11,'jeecg-gateway-router.json','DEFAULT_GROUP','','[{ + "id": "jeecg-system", + "order": 0, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/sys/**", + "_genkey_1": "/jmreport/**", + "_genkey_3": "/online/**", + "_genkey_4": "/generic/**", + "_genkey_5": "/drag/**", + "_genkey_6": "/actuator/**" + } + }], + "filters": [], + "uri": "lb://jeecg-system" +}, { + "id": "jeecg-demo", + "order": 1, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/mock/**", + "_genkey_1": "/test/**", + "_genkey_2": "/bigscreen/template1/**", + "_genkey_3": "/bigscreen/template2/**" + } + }], + "filters": [], + "uri": "lb://jeecg-demo" +}, { + "id": "jeecg-system-websocket", + "order": 2, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/websocket/**", + "_genkey_1": "/newsWebsocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-system" +}, { + "id": "jeecg-demo-websocket", + "order": 3, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/vxeSocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-demo" +}]','708c0948118bdb96bdfaa87200a14432',TO_DATE('2024-07-09 14:30:19','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:20','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,12,'jeecg-sharding.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: root + type: com.alibaba.druid.pool.DruidDataSource + props: + sql-show: true + rules: + sharding: + binding-tables: sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + tables: + sys_log: + actual-data-nodes: ds0.sys_log$->{0..1} + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','a93fa455c32cd37ca84631d2bbe13005',TO_DATE('2024-07-09 14:30:19','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:20','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,13,'jeecg-gateway-dev.yaml','DEFAULT_GROUP','','jeecg: + route: + config: + #type:database nacos yml + data-type: database + data-id: jeecg-gateway-router +spring: + redis: + database: 0 + host: jeecg-boot-redis + port: 6379 + password: +knife4j: + production: false','98e211c54b43a73f7189d92f1c77f815',TO_DATE('2024-07-09 14:30:19','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:20','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,14,'jeecg-sharding-multi.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0,ds1 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + ds1: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + props: + sql-show: true + rules: + replica-query: + load-balancers: + round-robin: + type: ROUND_ROBIN + props: + default: 0 + data-sources: + prds: + primary-data-source-name: ds0 + replica-data-source-names: ds1 + load-balancer-name: round_robin + sharding: + binding-tables: + - sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + database-inline: + type: INLINE + props: + algorithm-expression: ds$->{operate_type % 2} + tables: + sys_log: + actual-data-nodes: ds$->{0..1}.sys_log$->{0..1} + database-strategy: + standard: + sharding-column: operate_type + sharding-algorithm-name: database-inline + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','0fc2b030ca8c0008f148c84ecbd2a8c7',TO_DATE('2024-07-09 14:30:19','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:30:20','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(8,15,'jeecg-dev.yaml','DEFAULT_GROUP','','spring: + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: + initial-size: 5 + min-idle: 5 + maxActive: 20 + maxWait: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + filters: stat,wall,slf4j + wall: + selectWhereAlwayTrueCheck: false + stat: + merge-sql: true + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + database: 0 + host: jeecg-boot-redis + password: + port: 6379 + rabbitmq: + host: jeecg-boot-rabbitmq + username: guest + password: guest + port: 5672 + publisher-confirms: true + publisher-returns: true + virtual-host: / + listener: + simple: + acknowledge-mode: manual + concurrency: 1 + max-concurrency: 1 + retry: + enabled: true + flyway: + enabled: false + encoding: UTF-8 + locations: classpath:flyway/sql/mysql + sql-migration-prefix: V + sql-migration-separator: __ + placeholder-prefix: ''#('' + placeholder-suffix: ) + sql-migration-suffixes: .sql + validate-on-migrate: true + baseline-on-migrate: true + clean-disabled: true +minidao: + base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.* +jeecg: + firewall: + dataSourceSafe: false + lowCodeMode: dev + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys,/sys/sendChangePwdSms,/sys/user/sendChangePhoneSms,/sys/sms,/desform/api/sendVerifyCode + uploadType: local + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + upload: /opt/upFiles + webapp: /opt/webapp + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/** + oss: + endpoint: oss-cn-beijing.aliyuncs.com + accessKey: ?? + secretKey: ?? + bucketName: jeecgdev + staticDomain: ?? + elasticsearch: + cluster-name: jeecg-ES + cluster-nodes: jeecg-boot-es:9200 + check-enabled: false + file-view-domain: 127.0.0.1:8012 + minio: + minio_url: http://minio.jeecg.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + jmreport: + saasMode: + firewall: + dataSourceSafe: false + lowCodeMode: dev + wps: + domain: https://wwo.wps.cn/office/ + appid: ?? + appsecret: ?? + xxljob: + enabled: true + adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '''' + logPath: logs/jeecg/job/jobhandler/ + logRetentionDays: 30 + redisson: + address: jeecg-boot-redis:6379 + password: + type: STANDALONE + enabled: true + ai-chat: + enabled: false + apiKey: "????" + apiHost: "https://api.openai.com" + timeout: 60 +logging: + level: + org.jeecg.modules.system.mapper : info +cas: + prefixUrl: http://localhost:8888/cas +knife4j: + production: false + basic: + enable: false + username: jeecg + password: jeecg1314 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback + cache: + type: default + prefix: ''demo::'' + timeout: 1h +third-app: + enabled: false + type: + WECHAT_ENTERPRISE: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ?? + DINGTALK: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ??','822f70f7a278a503a02568186582ceaa',TO_DATE('2024-07-09 14:34:25','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:27','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','D','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(9,16,'jeecg.yaml','DEFAULT_GROUP','','server: + tomcat: + max-swallow-size: -1 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* +management: + health: + mail: + enabled: false + endpoints: + web: + exposure: + include: "*" + health: + sensitive: true + endpoint: + health: + show-details: ALWAYS +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + host: smtp.163.com + username: jeecgos@163.com + password: ?? + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + quartz: + job-store-type: jdbc + initialize-schema: embedded + auto-startup: false + startup-delay: 1s + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + activiti: + check-process-definitions: false + async-executor-activate: false + job-executor-activate: false + jpa: + open-in-view: false + freemarker: + suffix: .ftl + content-type: text/html + charset: UTF-8 + cache: false + prefer-file-system-access: false + template-loader-path: + - classpath:/templates + mvc: + static-path-pattern: /** + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration +mybatis-plus: + mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml + global-config: + banner: false + db-config: + id-type: ASSIGN_ID + table-underline: true + configuration: + call-setters-on-nulls: true','94dbdad61f7e2e3ace5a4fc07bb8c2a2',TO_DATE('2024-07-09 14:34:25','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:27','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','D','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(10,17,'jeecg-gateway-router.json','DEFAULT_GROUP','','[{ + "id": "jeecg-system", + "order": 0, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/sys/**", + "_genkey_1": "/jmreport/**", + "_genkey_3": "/online/**", + "_genkey_4": "/generic/**", + "_genkey_5": "/drag/**", + "_genkey_6": "/actuator/**" + } + }], + "filters": [], + "uri": "lb://jeecg-system" +}, { + "id": "jeecg-demo", + "order": 1, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/mock/**", + "_genkey_1": "/test/**", + "_genkey_2": "/bigscreen/template1/**", + "_genkey_3": "/bigscreen/template2/**" + } + }], + "filters": [], + "uri": "lb://jeecg-demo" +}, { + "id": "jeecg-system-websocket", + "order": 2, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/websocket/**", + "_genkey_1": "/newsWebsocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-system" +}, { + "id": "jeecg-demo-websocket", + "order": 3, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/vxeSocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-demo" +}]','708c0948118bdb96bdfaa87200a14432',TO_DATE('2024-07-09 14:34:25','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:27','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','D','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(11,18,'jeecg-sharding.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: root + type: com.alibaba.druid.pool.DruidDataSource + props: + sql-show: true + rules: + sharding: + binding-tables: sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + tables: + sys_log: + actual-data-nodes: ds0.sys_log$->{0..1} + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','a93fa455c32cd37ca84631d2bbe13005',TO_DATE('2024-07-09 14:34:25','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:27','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','D','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(12,19,'jeecg-gateway-dev.yaml','DEFAULT_GROUP','','jeecg: + route: + config: + #type:database nacos yml + data-type: database + data-id: jeecg-gateway-router +spring: + redis: + database: 0 + host: jeecg-boot-redis + port: 6379 + password: +knife4j: + production: false','98e211c54b43a73f7189d92f1c77f815',TO_DATE('2024-07-09 14:34:25','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:27','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','D','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(13,20,'jeecg-sharding-multi.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0,ds1 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + ds1: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + props: + sql-show: true + rules: + replica-query: + load-balancers: + round-robin: + type: ROUND_ROBIN + props: + default: 0 + data-sources: + prds: + primary-data-source-name: ds0 + replica-data-source-names: ds1 + load-balancer-name: round_robin + sharding: + binding-tables: + - sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + database-inline: + type: INLINE + props: + algorithm-expression: ds$->{operate_type % 2} + tables: + sys_log: + actual-data-nodes: ds$->{0..1}.sys_log$->{0..1} + database-strategy: + standard: + sharding-column: operate_type + sharding-algorithm-name: database-inline + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','0fc2b030ca8c0008f148c84ecbd2a8c7',TO_DATE('2024-07-09 14:34:25','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:27','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','D','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,21,'jeecg-dev.yaml','DEFAULT_GROUP','','spring: + datasource: + druid: + stat-view-servlet: + enabled: true + loginUsername: admin + loginPassword: 123456 + allow: + web-stat-filter: + enabled: true + dynamic: + druid: + initial-size: 5 + min-idle: 5 + maxActive: 20 + maxWait: 60000 + timeBetweenEvictionRunsMillis: 60000 + minEvictableIdleTimeMillis: 300000 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + poolPreparedStatements: true + maxPoolPreparedStatementPerConnectionSize: 20 + filters: stat,wall,slf4j + wall: + selectWhereAlwayTrueCheck: false + stat: + merge-sql: true + slow-sql-millis: 5000 + datasource: + master: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + database: 0 + host: jeecg-boot-redis + password: + port: 6379 + rabbitmq: + host: jeecg-boot-rabbitmq + username: guest + password: guest + port: 5672 + publisher-confirms: true + publisher-returns: true + virtual-host: / + listener: + simple: + acknowledge-mode: manual + concurrency: 1 + max-concurrency: 1 + retry: + enabled: true + flyway: + enabled: false + encoding: UTF-8 + locations: classpath:flyway/sql/mysql + sql-migration-prefix: V + sql-migration-separator: __ + placeholder-prefix: ''#('' + placeholder-suffix: ) + sql-migration-suffixes: .sql + validate-on-migrate: true + baseline-on-migrate: true + clean-disabled: true +minidao: + base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.* +jeecg: + firewall: + dataSourceSafe: false + lowCodeMode: dev + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys + uploadType: local + domainUrl: + pc: http://localhost:3100 + app: http://localhost:8051 + path: + upload: /opt/upFiles + webapp: /opt/webapp + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/** + oss: + endpoint: oss-cn-beijing.aliyuncs.com + accessKey: ?? + secretKey: ?? + bucketName: jeecgdev + staticDomain: ?? + elasticsearch: + cluster-name: jeecg-ES + cluster-nodes: jeecg-boot-es:9200 + check-enabled: false + file-view-domain: 127.0.0.1:8012 + minio: + minio_url: http://minio.jeecg.com + minio_name: ?? + minio_pass: ?? + bucketName: otatest + jmreport: + saasMode: + firewall: + dataSourceSafe: false + lowCodeMode: dev + wps: + domain: https://wwo.wps.cn/office/ + appid: ?? + appsecret: ?? + xxljob: + enabled: false + adminAddresses: http://jeecg-boot-xxljob:9080/xxl-job-admin + appname: ${spring.application.name} + accessToken: '''' + logPath: logs/jeecg/job/jobhandler/ + logRetentionDays: 30 + redisson: + address: jeecg-boot-redis:6379 + password: + type: STANDALONE + enabled: true + ai-chat: + enabled: false + apiKey: "????" + apiHost: "https://api.openai.com" + timeout: 60 +logging: + level: + org.jeecg.modules.system.mapper : info +cas: + prefixUrl: http://localhost:8888/cas +knife4j: + production: false + basic: + enable: false + username: jeecg + password: jeecg1314 +justauth: + enabled: true + type: + GITHUB: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/github/callback + WECHAT_ENTERPRISE: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/wechat_enterprise/callback + agent-id: ?? + DINGTALK: + client-id: ?? + client-secret: ?? + redirect-uri: http://sso.test.com:8080/jeecg-boot/thirdLogin/dingtalk/callback + cache: + type: default + prefix: ''demo::'' + timeout: 1h +third-app: + enabled: false + type: + WECHAT_ENTERPRISE: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ?? + DINGTALK: + enabled: false + client-id: ?? + client-secret: ?? + agent-id: ??','91c29720dfb424916a769201a25200cf',TO_DATE('2024-07-09 14:34:32','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,22,'jeecg.yaml','DEFAULT_GROUP','','server: + undertow: + # max-http-post-size: 10MB + worker-threads: 16 + buffers: + websocket: 8192 + io: 16384 + error: + include-exception: true + include-stacktrace: ALWAYS + include-message: ALWAYS + compression: + enabled: true + min-response-size: 1024 + mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/* +management: + health: + mail: + enabled: false + endpoints: + web: + exposure: + include: "*" + health: + sensitive: true + endpoint: + health: + show-details: ALWAYS +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mail: + host: smtp.163.com + username: jeecgos@163.com + password: ?? + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + quartz: + job-store-type: jdbc + initialize-schema: embedded + auto-startup: false + startup-delay: 1s + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: MyScheduler + instanceId: AUTO + jobStore: + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + tablePrefix: QRTZ_ + isClustered: true + misfireThreshold: 12000 + clusterCheckinInterval: 15000 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + aop: + proxy-target-class: true + activiti: + check-process-definitions: false + async-executor-activate: false + job-executor-activate: false + jpa: + open-in-view: false + freemarker: + suffix: .ftl + content-type: text/html + charset: UTF-8 + cache: false + prefer-file-system-access: false + template-loader-path: + - classpath:/templates + mvc: + static-path-pattern: /** + pathmatch: + matching-strategy: ant_path_matcher + resource: + static-locations: classpath:/static/,classpath:/public/ + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure + - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration +mybatis-plus: + mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml + global-config: + banner: false + db-config: + id-type: ASSIGN_ID + table-underline: true + configuration: + call-setters-on-nulls: true','ce1ca3b6f8431e884aed94ab29be43a9',TO_DATE('2024-07-09 14:34:32','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,23,'jeecg-gateway-router.json','DEFAULT_GROUP','','[{ + "id": "jeecg-system", + "order": 0, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/sys/**", + "_genkey_1": "/jmreport/**", + "_genkey_3": "/online/**", + "_genkey_4": "/generic/**", + "_genkey_5": "/oauth2/**", + "_genkey_6": "/drag/**", + "_genkey_7": "/actuator/**" + } + }], + "filters": [], + "uri": "lb://jeecg-system" +}, { + "id": "jeecg-demo", + "order": 1, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/mock/**", + "_genkey_1": "/test/**", + "_genkey_2": "/bigscreen/template1/**", + "_genkey_3": "/bigscreen/template2/**" + } + }], + "filters": [], + "uri": "lb://jeecg-demo" +}, { + "id": "jeecg-system-websocket", + "order": 2, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/websocket/**", + "_genkey_1": "/newsWebsocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-system" +}, { + "id": "jeecg-demo-websocket", + "order": 3, + "predicates": [{ + "name": "Path", + "args": { + "_genkey_0": "/vxeSocket/**" + } + }], + "filters": [], + "uri": "lb:ws://jeecg-demo" +}]','9794beb09d30bc6b835f2ee870781587',TO_DATE('2024-07-09 14:34:32','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,24,'jeecg-sharding.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: root + type: com.alibaba.druid.pool.DruidDataSource + props: + sql-show: true + rules: + sharding: + binding-tables: sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + tables: + sys_log: + actual-data-nodes: ds0.sys_log$->{0..1} + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','a93fa455c32cd37ca84631d2bbe13005',TO_DATE('2024-07-09 14:34:32','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,25,'jeecg-gateway-dev.yaml','DEFAULT_GROUP','','jeecg: + route: + config: + #type:database nacos yml + data-type: database + data-id: jeecg-gateway-router +spring: + data: + redis: + database: 0 + host: jeecg-boot-redis + port: 6379 + password: +knife4j: + production: false','19d7cd93eeb85a582c8a6942d499c7f7',TO_DATE('2024-07-09 14:34:32','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); +INSERT INTO "NACOS"."HIS_CONFIG_INFO"("ID","NID","DATA_ID","GROUP_ID","APP_NAME","CONTENT","MD5","GMT_CREATE","GMT_MODIFIED","SRC_USER","SRC_IP","OP_TYPE","TENANT_ID","ENCRYPTED_DATA_KEY") VALUES(0,26,'jeecg-sharding-multi.yaml','DEFAULT_GROUP','','spring: + shardingsphere: + datasource: + names: ds0,ds1 + ds0: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + ds1: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + type: com.alibaba.druid.pool.DruidDataSource + username: root + password: root + props: + sql-show: true + rules: + replica-query: + load-balancers: + round-robin: + type: ROUND_ROBIN + props: + default: 0 + data-sources: + prds: + primary-data-source-name: ds0 + replica-data-source-names: ds1 + load-balancer-name: round_robin + sharding: + binding-tables: + - sys_log + key-generators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + sharding-algorithms: + table-classbased: + props: + strategy: standard + algorithmClassName: org.jeecg.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + type: CLASS_BASED + database-inline: + type: INLINE + props: + algorithm-expression: ds$->{operate_type % 2} + tables: + sys_log: + actual-data-nodes: ds$->{0..1}.sys_log$->{0..1} + database-strategy: + standard: + sharding-column: operate_type + sharding-algorithm-name: database-inline + table-strategy: + standard: + sharding-algorithm-name: table-classbased + sharding-column: log_type','0fc2b030ca8c0008f148c84ecbd2a8c7',TO_DATE('2024-07-09 14:34:32','YYYY-MM-DD HH24:MI:SS.FF'),TO_DATE('2024-07-09 14:34:33','YYYY-MM-DD HH24:MI:SS.FF'),null,'192.168.1.11','I','springboot3',''); + +SET IDENTITY_INSERT "NACOS"."HIS_CONFIG_INFO" OFF; +INSERT INTO "NACOS"."ROLES"("USERNAME","ROLE") VALUES('nacos','ROLE_ADMIN'); + +SET IDENTITY_INSERT "NACOS"."TENANT_CAPACITY" ON; +SET IDENTITY_INSERT "NACOS"."TENANT_CAPACITY" OFF; +SET IDENTITY_INSERT "NACOS"."TENANT_INFO" ON; +INSERT INTO "NACOS"."TENANT_INFO"("ID","KP","TENANT_ID","TENANT_NAME","TENANT_DESC","CREATE_SOURCE","GMT_CREATE","GMT_MODIFIED") VALUES(1,'1','springboot3','springboot3','springboot3版本配置文件,与springboot2有很大区别','nacos',1720506551826,1720506551826); + +SET IDENTITY_INSERT "NACOS"."TENANT_INFO" OFF; +INSERT INTO "NACOS"."USERS"("USERNAME","PASSWORD","ENABLED") VALUES('nacos','$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu',1); + +ALTER TABLE "NACOS"."CONFIG_INFO" ADD CONSTRAINT PRIMARY KEY("ID") ; + +ALTER TABLE "NACOS"."CONFIG_INFO_AGGR" ADD CONSTRAINT PRIMARY KEY("ID") ; + +ALTER TABLE "NACOS"."CONFIG_INFO_BETA" ADD CONSTRAINT PRIMARY KEY("ID") ; + +ALTER TABLE "NACOS"."CONFIG_INFO_TAG" ADD CONSTRAINT PRIMARY KEY("ID") ; + +ALTER TABLE "NACOS"."CONFIG_TAGS_RELATION" ADD CONSTRAINT PRIMARY KEY("NID") ; + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CONSTRAINT PRIMARY KEY("ID") ; + +ALTER TABLE "NACOS"."HIS_CONFIG_INFO" ADD CONSTRAINT PRIMARY KEY("NID") ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CONSTRAINT PRIMARY KEY("ID") ; + +ALTER TABLE "NACOS"."TENANT_INFO" ADD CONSTRAINT PRIMARY KEY("ID") ; + +ALTER TABLE "NACOS"."USERS" ADD CONSTRAINT PRIMARY KEY("USERNAME") ; + +ALTER TABLE "NACOS"."HIS_CONFIG_INFO" ADD CHECK("ID" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."CONFIG_INFO" ADD CONSTRAINT "UK_CONFIGINFO_DATAGROUPTENANT" UNIQUE("DATA_ID","GROUP_ID","TENANT_ID") ; + +ALTER TABLE "NACOS"."CONFIG_INFO_AGGR" ADD CONSTRAINT "UK_CONFIGINFOAGGR_DATAGROUPTENANTDATUM" UNIQUE("DATA_ID","GROUP_ID","TENANT_ID","DATUM_ID") ; + +ALTER TABLE "NACOS"."CONFIG_INFO_BETA" ADD CONSTRAINT "UK_CONFIGINFOBETA_DATAGROUPTENANT" UNIQUE("DATA_ID","GROUP_ID","TENANT_ID") ; + +ALTER TABLE "NACOS"."CONFIG_INFO_TAG" ADD CONSTRAINT "UK_CONFIGINFOTAG_DATAGROUPTENANTTAG" UNIQUE("DATA_ID","GROUP_ID","TENANT_ID","TAG_ID") ; + +ALTER TABLE "NACOS"."CONFIG_TAGS_RELATION" ADD CONSTRAINT "UK_CONFIGTAGRELATION_CONFIGIDTAG" UNIQUE("ID","TAG_NAME","TAG_TYPE") ; + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CONSTRAINT "UK_GROUP_ID" UNIQUE("GROUP_ID") ; + +ALTER TABLE "NACOS"."PERMISSIONS" ADD CONSTRAINT "UK_ROLE_PERMISSION" UNIQUE("ROLE","RESOURCE","ACTION") ; + +ALTER TABLE "NACOS"."ROLES" ADD CONSTRAINT "IDX_USER_ROLE" UNIQUE("USERNAME","ROLE") ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CONSTRAINT "UK_TENANT_ID" UNIQUE("TENANT_ID") ; + +ALTER TABLE "NACOS"."TENANT_INFO" ADD CONSTRAINT "UK_TENANT_INFO_KPTENANTID" UNIQUE("KP","TENANT_ID") ; + +COMMENT ON TABLE "NACOS"."CONFIG_INFO" IS 'config_info'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."ID" IS 'id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."DATA_ID" IS 'data_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."CONTENT" IS 'content'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."MD5" IS 'md5'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."GMT_CREATE" IS '创建时间'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."GMT_MODIFIED" IS '修改时间'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."SRC_USER" IS 'source user'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."SRC_IP" IS 'source ip'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."TENANT_ID" IS '租户字段'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO"."ENCRYPTED_DATA_KEY" IS '密钥'; + +COMMENT ON TABLE "NACOS"."CONFIG_INFO_AGGR" IS '增加租户字段'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_AGGR"."ID" IS 'id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_AGGR"."DATA_ID" IS 'data_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_AGGR"."GROUP_ID" IS 'group_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_AGGR"."DATUM_ID" IS 'datum_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_AGGR"."CONTENT" IS '内容'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_AGGR"."GMT_MODIFIED" IS '修改时间'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_AGGR"."TENANT_ID" IS '租户字段'; + +COMMENT ON TABLE "NACOS"."CONFIG_INFO_BETA" IS 'config_info_beta'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."ID" IS 'id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."DATA_ID" IS 'data_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."GROUP_ID" IS 'group_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."APP_NAME" IS 'app_name'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."CONTENT" IS 'content'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."BETA_IPS" IS 'betaIps'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."MD5" IS 'md5'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."GMT_CREATE" IS '创建时间'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."GMT_MODIFIED" IS '修改时间'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."SRC_USER" IS 'source user'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."SRC_IP" IS 'source ip'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."TENANT_ID" IS '租户字段'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_BETA"."ENCRYPTED_DATA_KEY" IS '密钥'; + +COMMENT ON TABLE "NACOS"."CONFIG_INFO_TAG" IS 'config_info_tag'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."ID" IS 'id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."DATA_ID" IS 'data_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."GROUP_ID" IS 'group_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."TENANT_ID" IS 'tenant_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."TAG_ID" IS 'tag_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."APP_NAME" IS 'app_name'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."CONTENT" IS 'content'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."MD5" IS 'md5'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."GMT_CREATE" IS '创建时间'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."GMT_MODIFIED" IS '修改时间'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."SRC_USER" IS 'source user'; + +COMMENT ON COLUMN "NACOS"."CONFIG_INFO_TAG"."SRC_IP" IS 'source ip'; + +COMMENT ON TABLE "NACOS"."CONFIG_TAGS_RELATION" IS 'config_tag_relation'; + +COMMENT ON COLUMN "NACOS"."CONFIG_TAGS_RELATION"."ID" IS 'id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_TAGS_RELATION"."TAG_NAME" IS 'tag_name'; + +COMMENT ON COLUMN "NACOS"."CONFIG_TAGS_RELATION"."TAG_TYPE" IS 'tag_type'; + +COMMENT ON COLUMN "NACOS"."CONFIG_TAGS_RELATION"."DATA_ID" IS 'data_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_TAGS_RELATION"."GROUP_ID" IS 'group_id'; + +COMMENT ON COLUMN "NACOS"."CONFIG_TAGS_RELATION"."TENANT_ID" IS 'tenant_id'; + +CREATE INDEX "IDX_DID" +ON "NACOS"."HIS_CONFIG_INFO"("DATA_ID"); + +CREATE INDEX "IDX_GMT_CREATE" +ON "NACOS"."HIS_CONFIG_INFO"("GMT_CREATE"); + +CREATE INDEX "IDX_GMT_MODIFIED" +ON "NACOS"."HIS_CONFIG_INFO"("GMT_MODIFIED"); + +CREATE INDEX "IDX_TENANT_ID" +ON "NACOS"."TENANT_INFO"("TENANT_ID"); + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CHECK("USAGE" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CHECK("MAX_SIZE" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CHECK("MAX_AGGR_COUNT" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CHECK("MAX_AGGR_SIZE" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CHECK("MAX_HISTORY_COUNT" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."GROUP_CAPACITY" ADD CHECK("QUOTA" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CHECK("QUOTA" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CHECK("USAGE" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CHECK("MAX_SIZE" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CHECK("MAX_AGGR_COUNT" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CHECK("MAX_AGGR_SIZE" >= 0) ENABLE ; + +ALTER TABLE "NACOS"."TENANT_CAPACITY" ADD CHECK("MAX_HISTORY_COUNT" >= 0) ENABLE ; + +COMMENT ON TABLE "NACOS"."GROUP_CAPACITY" IS '集群、各Group容量信息表'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."ID" IS '主键ID'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."GROUP_ID" IS 'Group ID,空字符表示整个集群'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."QUOTA" IS '配额,0表示使用默认值'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."USAGE" IS '使用量'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."MAX_SIZE" IS '单个配置大小上限,单位为字节,0表示使用默认值'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."MAX_AGGR_COUNT" IS '聚合子配置最大个数,,0表示使用默认值'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."MAX_AGGR_SIZE" IS '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."MAX_HISTORY_COUNT" IS '最大变更历史数量'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."GMT_CREATE" IS '创建时间'; + +COMMENT ON COLUMN "NACOS"."GROUP_CAPACITY"."GMT_MODIFIED" IS '修改时间'; + +COMMENT ON TABLE "NACOS"."HIS_CONFIG_INFO" IS '多租户改造'; + +COMMENT ON COLUMN "NACOS"."HIS_CONFIG_INFO"."APP_NAME" IS 'app_name'; + +COMMENT ON COLUMN "NACOS"."HIS_CONFIG_INFO"."TENANT_ID" IS '租户字段'; + +COMMENT ON COLUMN "NACOS"."HIS_CONFIG_INFO"."ENCRYPTED_DATA_KEY" IS '密钥'; + +COMMENT ON TABLE "NACOS"."TENANT_CAPACITY" IS '租户容量信息表'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."ID" IS '主键ID'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."TENANT_ID" IS 'Tenant ID'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."QUOTA" IS '配额,0表示使用默认值'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."USAGE" IS '使用量'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."MAX_SIZE" IS '单个配置大小上限,单位为字节,0表示使用默认值'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."MAX_AGGR_COUNT" IS '聚合子配置最大个数'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."MAX_AGGR_SIZE" IS '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."MAX_HISTORY_COUNT" IS '最大变更历史数量'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."GMT_CREATE" IS '创建时间'; + +COMMENT ON COLUMN "NACOS"."TENANT_CAPACITY"."GMT_MODIFIED" IS '修改时间'; + +COMMENT ON TABLE "NACOS"."TENANT_INFO" IS 'tenant_info'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."ID" IS 'id'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."KP" IS 'kp'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."TENANT_ID" IS 'tenant_id'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."TENANT_NAME" IS 'tenant_name'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."TENANT_DESC" IS 'tenant_desc'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."CREATE_SOURCE" IS 'create_source'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."GMT_CREATE" IS '创建时间'; + +COMMENT ON COLUMN "NACOS"."TENANT_INFO"."GMT_MODIFIED" IS '修改时间'; + diff --git a/test-server-cloud/test-cloud-nacos/pom.xml b/test-server-cloud/test-cloud-nacos/pom.xml new file mode 100644 index 0000000..93fb310 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + test-cloud-nacos + jeecg-cloud-nacos + nacos启动模块 + 3.9.2 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + + + aliyun + aliyun Repository + https://maven.aliyun.com/repository/public + + false + + + + jeecg + jeecg Repository + https://maven.jeecg.org/nexus/content/repositories/jeecg + + false + + + + + + 2.17.0 + 2.3.2 + 8.1.3.140 + + + + + org.springframework.boot + spring-boot-starter + + + org.apache.tomcat.embed + tomcat-embed-jasper + + + org.springframework.boot + spring-boot-starter-security + + + org.jeecgframework.nacos + nacos-naming + ${nacos.version} + + + org.jeecgframework.nacos + nacos-istio + ${nacos.version} + + + org.jeecgframework.nacos + nacos-config + ${nacos.version} + + + org.jeecgframework.nacos + nacos-console + ${nacos.version} + + + + + com.dameng + DmJdbcDriver18 + ${dm8.version} + + + com.dameng + DmDialect-for-hibernate5.0 + ${dm8.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/test-server-cloud/test-cloud-nacos/src/main/java/com/alibaba/nacos/GhbNacosApplication.java b/test-server-cloud/test-cloud-nacos/src/main/java/com/alibaba/nacos/GhbNacosApplication.java new file mode 100644 index 0000000..beae7e4 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/src/main/java/com/alibaba/nacos/GhbNacosApplication.java @@ -0,0 +1,50 @@ +package com.alibaba.nacos; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import javax.servlet.http.HttpServletResponse; + + +/** + * Nacos 启动类 + * + * @author zyf + */ +@SpringBootApplication(scanBasePackages = "com.alibaba.nacos") +@ServletComponentScan +@EnableScheduling +public class GhbNacosApplication { + + /** 是否单机模式启动 */ + private static String standalone = "true"; + /** 是否开启鉴权 */ + private static String enabled = "false"; + + public static void main(String[] args) { + System.setProperty("nacos.standalone", standalone); + System.setProperty("nacos.core.auth.enabled", enabled); +// //一旦Nacos初始化,用户名nacos将不能被修改,但你可以通过控制台或API来修改密码 https://nacos.io/en/blog/faq/nacos-user-question-history8420 +// System.setProperty("nacos.core.auth.default.username", "nacos"); +// System.setProperty("nacos.core.auth.default.password", "nacos"); + System.setProperty("server.tomcat.basedir","logs"); + //自定义启动端口号 + System.setProperty("server.port","8848"); + SpringApplication.run(GhbNacosApplication.class, args); + } + + /** + * 默认跳转首页 + * + * @param model + * @return + */ + @GetMapping("/") + public String index(Model model, HttpServletResponse response) { + // 视图重定向 - 跳转 + return "/nacos"; + } +} diff --git a/test-server-cloud/test-cloud-nacos/src/main/resources/application-dm.yml b/test-server-cloud/test-cloud-nacos/src/main/resources/application-dm.yml new file mode 100644 index 0000000..f11246e --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/src/main/resources/application-dm.yml @@ -0,0 +1,63 @@ +server: + servlet: + contextPath: /nacos + tomcat: + accesslog: + enabled: true + pattern: '%h %l %u %t "%r" %s %b %D %{User-Agent}i %{Request-Source}i' + basedir: '' +spring: + sql: + init: + platform: dm +db: + pool: + config: + driverClassName: dm.jdbc.driver.DmDriver + num: 1 + password: + '0': SYSDBA + url: + '0': jdbc:dm://192.168.1.188:30236/DMSERVER?schema=NACOS&compatibleMode=mysql&ignoreCase=true&ENCODING=utf-8 + user: + '0': SYSDBA +management: + metrics: + export: + elastic: + enabled: false + influx: + enabled: false +nacos: + core: + auth: + enabled: false + caching: + enabled: true + server: + identity: + key: example + value: example + plugin: + nacos: + token: + expire: + seconds: 18000 + secret: + key: SecretKey01234567890123456789012345345678999987654901234567890123456789 + system: + type: nacos + istio: + mcp: + server: + enabled: false + naming: + empty-service: + auto-clean: true + clean: + initial-delay-ms: 50000 + period-time-ms: 30000 + security: + ignore: + urls: /,/error,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-ui/public/**,/v1/auth/**,/v1/console/health/**,/actuator/**,/v1/console/server/** + standalone: true \ No newline at end of file diff --git a/test-server-cloud/test-cloud-nacos/src/main/resources/application-mysql.yml b/test-server-cloud/test-cloud-nacos/src/main/resources/application-mysql.yml new file mode 100644 index 0000000..f23fab9 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/src/main/resources/application-mysql.yml @@ -0,0 +1,60 @@ +server: + servlet: + contextPath: /nacos + tomcat: + accesslog: + enabled: true + pattern: '%h %l %u %t "%r" %s %b %D %{User-Agent}i %{Request-Source}i' + basedir: '' +spring: + sql: + init: + platform: mysql +db: + num: 1 + password: + '0': ${MYSQL-PWD:root} + url: + '0': jdbc:mysql://${MYSQL-HOST:127.0.0.1}:${MYSQL-PORT:3307}/${MYSQL-DB:nacos}?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true + user: + '0': ${MYSQL-USER:root} +management: + metrics: + export: + elastic: + enabled: false + influx: + enabled: false +nacos: + core: + auth: + enabled: false + caching: + enabled: true + server: + identity: + key: nacos + value: nacos + plugin: + nacos: + token: + expire: + seconds: 18000 + secret: + key: VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg= + system: + type: nacos + istio: + mcp: + server: + enabled: false + naming: + empty-service: + auto-clean: true + clean: + initial-delay-ms: 50000 + period-time-ms: 30000 + security: + ignore: + urls: /,/error,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-ui/public/**,/v1/auth/**,/v1/console/health/**,/actuator/**,/v1/console/server/** + standalone: true diff --git a/test-server-cloud/test-cloud-nacos/src/main/resources/application.yml b/test-server-cloud/test-cloud-nacos/src/main/resources/application.yml new file mode 100644 index 0000000..dc1be31 --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: mysql \ No newline at end of file diff --git a/test-server-cloud/test-cloud-nacos/startup.sh b/test-server-cloud/test-cloud-nacos/startup.sh new file mode 100644 index 0000000..e9ae92d --- /dev/null +++ b/test-server-cloud/test-cloud-nacos/startup.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# ============================================ +# Nacos 启动脚本 — 根据环境变量生成 application.properties +# 环境变量: MYSQL-HOST MYSQL-PORT MYSQL-DB MYSQL-USER MYSQL-PWD +# ============================================ +set -e + +CONF_DIR="${NACOS_HOME:-/app}/conf" +mkdir -p "$CONF_DIR" + +MYSQL_HOST="${MYSQL_HOST:-mysql}" +MYSQL_PORT="${MYSQL_PORT:-3306}" +MYSQL_DB="${MYSQL_DB:-nacos}" +MYSQL_USER="${MYSQL_USER:-root}" +MYSQL_PWD="${MYSQL_PWD:-root}" + +echo ">>> Nacos 数据库配置:" +echo " Host: ${MYSQL_HOST}:${MYSQL_PORT}" +echo " DB: ${MYSQL_DB}" +echo " User: ${MYSQL_USER}" + +cat > "$CONF_DIR/application.properties" << EOF +# Nacos 数据源配置(由 startup.sh 自动生成) +server.port=8848 +spring.sql.init.platform=mysql +db.num=1 +db.url.0=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DB}?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai +db.user.0=${MYSQL_USER} +db.password.0=${MYSQL_PWD} +EOF + +echo ">>> 启动 Nacos..." +exec java ${JAVA_OPTS:-"-Xms512m -Xmx512m"} -jar /app/app.jar diff --git a/test-server-cloud/test-demo-cloud-start/Dockerfile b/test-server-cloud/test-demo-cloud-start/Dockerfile new file mode 100644 index 0000000..4b4d191 --- /dev/null +++ b/test-server-cloud/test-demo-cloud-start/Dockerfile @@ -0,0 +1,52 @@ +# ============================================ +# Demo Cloud 微服务 — 多阶段构建 +# Stage 1: Maven 编译全项目(SpringCloud profile) +# Stage 2: JRE 运行 +# ============================================ + +# ---- Stage 1: 编译 ---- +FROM maven:3.9-eclipse-temurin-17 AS builder + +WORKDIR /build + +# 先复制 pom.xml,利用 Docker 缓存加速依赖下载 +COPY pom.xml ./ +COPY ghb-base-core/pom.xml ghb-base-core/ +COPY ghb-module-system/pom.xml ghb-module-system/ +COPY ghb-module-system/ghb-system-api/pom.xml ghb-module-system/ghb-system-api/ +COPY ghb-module-system/ghb-system-biz/pom.xml ghb-module-system/ghb-system-biz/ +COPY ghb-module-system/ghb-system-start/pom.xml ghb-module-system/ghb-system-start/ +COPY ghb-module-business/pom.xml ghb-module-business/ +COPY ghb-server-cloud/pom.xml ghb-server-cloud/ +COPY test-server-cloud/test-cloud-gateway/pom.xml test-server-cloud/test-cloud-gateway/ +COPY test-server-cloud/test-system-cloud-start/pom.xml test-server-cloud/test-system-cloud-start/ +COPY ghb-server-cloud/ghb-demo-cloud-start/pom.xml ghb-server-cloud/ghb-demo-cloud-start/ +COPY test-server-cloud/test-cloud-nacos/pom.xml test-server-cloud/test-cloud-nacos/ + +# 下载依赖(pom 不变时缓存) +RUN mvn dependency:go-offline -B -P SpringCloud,dev || true + +# 复制全部源码 +COPY . . + +# 编译全项目(SpringCloud profile,跳过测试) +RUN mvn package -P SpringCloud,dev -Dmaven.test.skip=true -T 1C + +# ---- Stage 2: 运行 ---- +FROM eclipse-temurin:17-jre + +LABEL maintainer="ghb-base deploy" + +ENV TZ=Asia/Shanghai +RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime + +WORKDIR /app + +# 从编译阶段复制 demo-cloud jar +COPY --from=builder /build/ghb-server-cloud/ghb-demo-cloud-start/target/*.jar app.jar + +EXPOSE 7002 + +ENV JAVA_OPTS="-Xms256m -Xmx512m" + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/test-server-cloud/test-demo-cloud-start/README.md b/test-server-cloud/test-demo-cloud-start/README.md new file mode 100644 index 0000000..9d1c1ff --- /dev/null +++ b/test-server-cloud/test-demo-cloud-start/README.md @@ -0,0 +1,3 @@ +采用jar启动必须设置-Dfile.encoding=utf-8 ,不然会加载不到naocs文件 + +java -Dfile.encoding=utf-8 -jar xxxx.jar \ No newline at end of file diff --git a/test-server-cloud/test-demo-cloud-start/pom.xml b/test-server-cloud/test-demo-cloud-start/pom.xml new file mode 100644 index 0000000..f9836b6 --- /dev/null +++ b/test-server-cloud/test-demo-cloud-start/pom.xml @@ -0,0 +1,48 @@ + + + + test-server-cloud + com.ghb + 3.9.2 + + 4.0.0 + + test-demo-cloud-start + Demo微服务启动 + + + + + org.jeecgframework.boot3 + jeecg-boot-starter-cloud + + + + com.ghb + test-system-cloud-api + + + + org.jeecgframework.boot3 + jeecg-boot-starter-job + + + + + org.jeecgframework.boot3 + jeecg-module-demo + ${jeecgboot.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/test-server-cloud/test-demo-cloud-start/src/main/java/com/ghb/base/GhbDemoCloudApplication.java b/test-server-cloud/test-demo-cloud-start/src/main/java/com/ghb/base/GhbDemoCloudApplication.java new file mode 100644 index 0000000..bfbd54a --- /dev/null +++ b/test-server-cloud/test-demo-cloud-start/src/main/java/com/ghb/base/GhbDemoCloudApplication.java @@ -0,0 +1,38 @@ +package com.ghb.base; + +import com.xkcoding.justauth.autoconfigure.JustAuthAutoConfiguration; +import org.jeecg.common.base.BaseMap; +import org.jeecg.common.constant.GlobalConstants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.data.redis.core.RedisTemplate; + +@SpringBootApplication +@EnableFeignClients +@ImportAutoConfiguration(JustAuthAutoConfiguration.class) // spring boot 3.x justauth 兼容性处理 +public class GhbDemoCloudApplication implements CommandLineRunner { + @Autowired + private RedisTemplate redisTemplate; + + public static void main(String[] args) { + SpringApplication.run(GhbDemoCloudApplication.class, args); + } + + /** + * 启动的时候,触发下gateway网关刷新 + * + * 解决: 先启动gateway后启动服务,Swagger接口文档访问不通的问题 + * @param args + */ + @Override + public void run(String... args) { + BaseMap params = new BaseMap(); + params.put(GlobalConstants.HANDLER_NAME, GlobalConstants.LODER_ROUDER_HANDLER); + //刷新网关 + redisTemplate.convertAndSend(GlobalConstants.REDIS_TOPIC_NAME, params); + } +} diff --git a/test-server-cloud/test-demo-cloud-start/src/main/resources/application.yml b/test-server-cloud/test-demo-cloud-start/src/main/resources/application.yml new file mode 100644 index 0000000..5732505 --- /dev/null +++ b/test-server-cloud/test-demo-cloud-start/src/main/resources/application.yml @@ -0,0 +1,24 @@ +server: + port: 7002 + +spring: + application: + name: test-demo + cloud: + nacos: + config: + server-addr: @config.server-addr@ + group: @config.group@ + namespace: @config.namespace@ + username: @config.username@ + password: @config.password@ + discovery: + server-addr: ${spring.cloud.nacos.config.server-addr} + group: @config.group@ + namespace: @config.namespace@ + username: @config.username@ + password: @config.password@ + config: + import: + - optional:nacos:jeecg.yaml + - optional:nacos:jeecg-@profile.name@.yaml \ No newline at end of file diff --git a/test-server-cloud/test-demo-cloud-start/src/main/resources/logback-spring.xml b/test-server-cloud/test-demo-cloud-start/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..9e63633 --- /dev/null +++ b/test-server-cloud/test-demo-cloud-start/src/main/resources/logback-spring.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n + + + + + + + + ${LOG_HOME}/jeecg-demo-%d{yyyy-MM-dd}.%i.log + + 30 + 10MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + + + + + + + + ERROR + + + + %p%d%msg%M%F{32}%L + + + ${LOG_HOME}/error-log.html + + + + + + + + ${LOG_HOME}/jeecg-demo-%d{yyyy-MM-dd}.%i.html + + 30 + 10MB + + + + %p%d%msg%M%F{32}%L + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-system-cloud-start/Dockerfile b/test-server-cloud/test-system-cloud-start/Dockerfile new file mode 100644 index 0000000..10f8128 --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/Dockerfile @@ -0,0 +1,52 @@ +# ============================================ +# System Cloud 微服务 — 多阶段构建 +# Stage 1: Maven 编译全项目(SpringCloud profile) +# Stage 2: JRE 运行 +# ============================================ + +# ---- Stage 1: 编译 ---- +FROM maven:3.9-eclipse-temurin-17 AS builder + +WORKDIR /build + +# 先复制 pom.xml,利用 Docker 缓存加速依赖下载 +COPY pom.xml ./ +COPY ghb-base-core/pom.xml ghb-base-core/ +COPY ghb-module-system/pom.xml ghb-module-system/ +COPY ghb-module-system/ghb-system-api/pom.xml ghb-module-system/ghb-system-api/ +COPY ghb-module-system/ghb-system-biz/pom.xml ghb-module-system/ghb-system-biz/ +COPY ghb-module-system/ghb-system-start/pom.xml ghb-module-system/ghb-system-start/ +COPY ghb-module-business/pom.xml ghb-module-business/ +COPY ghb-server-cloud/pom.xml ghb-server-cloud/ +COPY test-server-cloud/test-cloud-gateway/pom.xml test-server-cloud/test-cloud-gateway/ +COPY test-server-cloud/test-system-cloud-start/pom.xml test-server-cloud/test-system-cloud-start/ +COPY ghb-server-cloud/ghb-demo-cloud-start/pom.xml ghb-server-cloud/ghb-demo-cloud-start/ +COPY test-server-cloud/test-cloud-nacos/pom.xml test-server-cloud/test-cloud-nacos/ + +# 下载依赖(pom 不变时缓存) +RUN mvn dependency:go-offline -B -P SpringCloud,dev || true + +# 复制全部源码 +COPY . . + +# 编译全项目(SpringCloud profile,跳过测试) +RUN mvn package -P SpringCloud,dev -Dmaven.test.skip=true -T 1C + +# ---- Stage 2: 运行 ---- +FROM eclipse-temurin:17-jre + +LABEL maintainer="ghb-base deploy" + +ENV TZ=Asia/Shanghai +RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime + +WORKDIR /app + +# 从编译阶段复制 system-cloud jar +COPY --from=builder /build/test-server-cloud/test-system-cloud-start/target/*.jar app.jar + +EXPOSE 7001 + +ENV JAVA_OPTS="-Xms512m -Xmx512m" + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/test-server-cloud/test-system-cloud-start/README.md b/test-server-cloud/test-system-cloud-start/README.md new file mode 100644 index 0000000..9d1c1ff --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/README.md @@ -0,0 +1,3 @@ +采用jar启动必须设置-Dfile.encoding=utf-8 ,不然会加载不到naocs文件 + +java -Dfile.encoding=utf-8 -jar xxxx.jar \ No newline at end of file diff --git a/test-server-cloud/test-system-cloud-start/pom.xml b/test-server-cloud/test-system-cloud-start/pom.xml new file mode 100644 index 0000000..b48efd6 --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/pom.xml @@ -0,0 +1,111 @@ + + + + test-server-cloud + com.ghb + 3.9.2 + + 4.0.0 + test-system-cloud-start + System项目微服务启动 + + + + + org.jeecgframework.boot3 + jeecg-boot-starter-cloud + + + + + com.ghb + test-system-biz + + + + com.ghb + jeecg-module-demo + + + org.jeecgframework.boot3 + jeecg-online + + + + + + com.ghb + test-module-business + + + + org.jeecgframework.boot3 + jeecg-boot-starter-job + + + + + com.github.xiaoymin + knife4j-openapi3-ui + ${knife4j-spring-boot-starter.version} + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.7.0 + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/test-server-cloud/test-system-cloud-start/src/main/java/com/ghb/base/GhbSystemCloudApplication.java b/test-server-cloud/test-system-cloud-start/src/main/java/com/ghb/base/GhbSystemCloudApplication.java new file mode 100644 index 0000000..2818d5d --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/src/main/java/com/ghb/base/GhbSystemCloudApplication.java @@ -0,0 +1,92 @@ +package com.ghb.base; + +import com.xkcoding.justauth.autoconfigure.JustAuthAutoConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.base.BaseMap; +import org.jeecg.common.constant.GlobalConstants; +import com.ghb.base.common.util.oConvertUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 微服务启动类(采用此类启动项目为微服务模式) + * 特别提醒: + * 1、需要先初始化Nacos的数据库脚本,db/tables_nacos.sql + * 2.需要集成mogodb请删除 exclude={MongoAutoConfiguration.class} + * + * @author Ghb + * @date: 2022/4/21 10:55 + */ +@Slf4j +@SpringBootApplication(exclude = MongoAutoConfiguration.class) +@ComponentScan(basePackages = { + "com.ghb.base", + "org.jeecg.common.util", + "org.jeecg.common.modules.redis", + "org.jeecg.common.config", + "org.jeecg.common.constant", + "org.jeecg.common.enums", + "org.jeecg.common.exception", + "org.jeecg.common.base", + "org.jeecg.common.annotation" +}) +@EnableDiscoveryClient +@EnableFeignClients(basePackages = {"com.ghb.base"}) +@EnableScheduling +@EnableAutoConfiguration(exclude={MongoAutoConfiguration.class}) +@ImportAutoConfiguration(JustAuthAutoConfiguration.class) // spring boot 3.x justauth 兼容性处理 +public class GhbSystemCloudApplication extends SpringBootServletInitializer implements CommandLineRunner { + + @Autowired + private RedisTemplate redisTemplate; + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(GhbSystemCloudApplication.class); + } + + public static void main(String[] args) throws UnknownHostException { + ConfigurableApplicationContext application = SpringApplication.run(GhbSystemCloudApplication.class, args); + Environment env = application.getEnvironment(); + String ip = InetAddress.getLocalHost().getHostAddress(); + String port = env.getProperty("server.port"); + String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path")); + log.info("\n----------------------------------------------------------\n\t" + + "Application Ghb-Boot is running! Access URLs:\n\t" + + "Local: \t\thttp://localhost:" + port + path + "/doc.html\n" + + "External: \thttp://" + ip + ":" + port + path + "/doc.html\n" + + "Swagger文档: \thttp://" + ip + ":" + port + path + "/doc.html\n" + + "----------------------------------------------------------"); + + } + + /** + * 启动的时候,触发下gateway网关刷新 + * + * 解决: 先启动gateway后启动服务,Swagger接口文档访问不通的问题 + * @param args + */ + @Override + public void run(String... args) { + BaseMap params = new BaseMap(); + params.put(GlobalConstants.HANDLER_NAME, GlobalConstants.LODER_ROUDER_HANDLER); + //刷新网关 + redisTemplate.convertAndSend(GlobalConstants.REDIS_TOPIC_NAME, params); + } +} diff --git a/test-server-cloud/test-system-cloud-start/src/main/resources/application.yml b/test-server-cloud/test-system-cloud-start/src/main/resources/application.yml new file mode 100644 index 0000000..ec9e6f1 --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/src/main/resources/application.yml @@ -0,0 +1,59 @@ +server: + port: 7001 + +spring: + application: + name: test-system + cloud: + nacos: + config: + server-addr: @config.server-addr@ + group: @config.group@ + namespace: @config.namespace@ + username: @config.username@ + password: @config.password@ + discovery: + server-addr: ${spring.cloud.nacos.config.server-addr} + group: @config.group@ + namespace: @config.namespace@ + username: @config.username@ + password: @config.password@ + config: + import: + - optional:classpath:config/application-liteflow.yml + - optional:nacos:jeecg.yaml + - optional:nacos:jeecg-@profile.name@.yaml + +# ghb 兜底配置(Nacos 配置加载失败时使用) +ghb: + path: + upload: /opt/upFiles + webapp: /opt/webapp + uploadType: local + signatureSecret: dd05f1c54d63749eda95f9fa6d49v442a + signUrls: /sys/dict/getDictItems/*,/sys/dict/loadDict/*,/sys/dict/loadDictOrderByValue/*,/sys/dict/loadDictItem/*,/sys/dict/loadTreeData,/sys/api/queryTableDictItemsByCode,/sys/api/queryFilterTableDictInfo,/sys/api/queryTableDictByKeys,/sys/api/translateDictFromTable,/sys/api/translateDictFromTableByKeys + shiro: + excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/category/**,/visual/**,/map/**,/jmreport/bigscreen2/** + +# 低代码模块 jeecg-online 从 ghb-system-biz 排除但可能从其他依赖引入 +# 需要扫描原始 org.jeecg 包以匹配外部 JAR 的包结构 +minidao: + base-package: org.jeecg.modules.jmreport.*,org.jeecg.modules.drag.* + +# Knife4j 兜底配置(本地开发无 Nacos 时确保文档可用) +knife4j: + production: false +springdoc: + api-docs: + enabled: true +logging: + level: + org.springdoc: DEBUG + org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE +# #shardingjdbc数据源 +# datasource: +# dynamic: +# datasource: +# sharding-db: +# driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver +# url: jdbc:shardingsphere:nacos:sharding.yaml?serverAddr=@config.server-addr@&namespace=@config.namespace@&group=@config.group@ \ No newline at end of file diff --git a/test-server-cloud/test-system-cloud-start/src/main/resources/jeecg/jeecg_config.properties b/test-server-cloud/test-system-cloud-start/src/main/resources/jeecg/jeecg_config.properties new file mode 100644 index 0000000..07c6d59 --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/src/main/resources/jeecg/jeecg_config.properties @@ -0,0 +1,29 @@ +#code_generate_project_path +project_path=E:\\workspace\\jeecg-boot +#bussi_package[User defined] +bussi_package=com.ghb.base.modules.demo + + +#default code path +#source_root_package=src +#webroot_package=WebRoot + +#maven code path +source_root_package=src.main.java +webroot_package=src.main.webapp + +#ftl resource url +templatepath=/jeecg/code-template +system_encoding=utf-8 + +#db Table id [User defined] +db_table_id=id + +#db convert flag[true/false] +db_filed_convert=true + +#page Search Field num [User defined] +page_search_filed_num=1 +#page_filter_fields +page_filter_fields=create_time,create_by,update_time,update_by +exclude_table=act_,ext_act_,design_,onl_,sys_,qrtz_ diff --git a/test-server-cloud/test-system-cloud-start/src/main/resources/jeecg/jeecg_database.properties b/test-server-cloud/test-system-cloud-start/src/main/resources/jeecg/jeecg_database.properties new file mode 100644 index 0000000..45b49f6 --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/src/main/resources/jeecg/jeecg_database.properties @@ -0,0 +1,27 @@ +#mysql +diver_name=com.mysql.jdbc.Driver +url=jdbc:mysql://localhost:3306/jeecg-boot?useUnicode=true&characterEncoding=UTF-8 +username=root +password=root +database_name=jeecg-boot + +#oracle +#diver_name=oracle.jdbc.driver.OracleDriver +#url=jdbc:oracle:thin:@192.168.1.200:1521:ORCL +#username=scott +#password=tiger +#database_name=ORCL + +#postgre +#diver_name=org.postgresql.Driver +#url=jdbc:postgresql://localhost:5432/jeecg +#username=postgres +#password=postgres +#database_name=jeecg + +#SQLServer2005\u4ee5\u4e0a +#diver_name=org.hibernate.dialect.SQLServerDialect +#url=jdbc:sqlserver://192.168.1.200:1433;DatabaseName=jeecg +#username=sa +#password=SA +#database_name=jeecg \ No newline at end of file diff --git a/test-server-cloud/test-system-cloud-start/src/main/resources/logback-spring.xml b/test-server-cloud/test-system-cloud-start/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..60d4c7d --- /dev/null +++ b/test-server-cloud/test-system-cloud-start/src/main/resources/logback-spring.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n + + + + + + + + ${LOG_HOME}/jeecg-system-%d{yyyy-MM-dd}.%i.log + + 30 + 10MB + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n + + + + + + + + ERROR + + + + %p%d%msg%M%F{32}%L + + + ${LOG_HOME}/error-log.html + + + + + + + + ${LOG_HOME}/jeecg-system-%d{yyyy-MM-dd}.%i.html + + 30 + 10MB + + + + %p%d%msg%M%F{32}%L + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/pom.xml b/test-server-cloud/test-visual/pom.xml new file mode 100644 index 0000000..5cd435f --- /dev/null +++ b/test-server-cloud/test-visual/pom.xml @@ -0,0 +1,25 @@ + + + + test-server-cloud + com.ghb + 3.9.2 + + 4.0.0 + + test-visual + pom + + + + test-cloud-sentinel + test-cloud-monitor + test-cloud-xxljob + + test-cloud-test + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-monitor/README.md b/test-server-cloud/test-visual/test-cloud-monitor/README.md new file mode 100644 index 0000000..2ed8dfd --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-monitor/README.md @@ -0,0 +1,2 @@ +http://localhost:9111 +账号密码:admin/admin \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-monitor/pom.xml b/test-server-cloud/test-visual/test-cloud-monitor/pom.xml new file mode 100644 index 0000000..3a6fcfd --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-monitor/pom.xml @@ -0,0 +1,71 @@ + + + + test-visual + com.ghb + 3.9.2 + + 4.0.0 + test-cloud-monitor + + + + + org.springframework.boot + spring-boot-starter-actuator + + + de.codecentric + spring-boot-admin-starter-server + 3.5.2 + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.boot + spring-boot-properties-migrator + runtime + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + src/main/resources + true + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-monitor/src/main/java/com/ghb/base/monitor/GhbMonitorApplication.java b/test-server-cloud/test-visual/test-cloud-monitor/src/main/java/com/ghb/base/monitor/GhbMonitorApplication.java new file mode 100644 index 0000000..465359d --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-monitor/src/main/java/com/ghb/base/monitor/GhbMonitorApplication.java @@ -0,0 +1,18 @@ +package com.ghb.base.monitor; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 监控服务 + * @author zyf + * @date: 2022/4/21 10:55 + */ +@SpringBootApplication +@EnableAdminServer +public class GhbMonitorApplication { + public static void main(String[] args) { + SpringApplication.run(GhbMonitorApplication.class, args); + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-monitor/src/main/java/com/ghb/base/monitor/config/SecuritySecureConfig.java b/test-server-cloud/test-visual/test-cloud-monitor/src/main/java/com/ghb/base/monitor/config/SecuritySecureConfig.java new file mode 100644 index 0000000..cd2a4ee --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-monitor/src/main/java/com/ghb/base/monitor/config/SecuritySecureConfig.java @@ -0,0 +1,61 @@ +package com.ghb.base.monitor.config; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; + +/** + * @author scott + */ +@Configuration +public class SecuritySecureConfig { + + private final String adminContextPath; + + public SecuritySecureConfig(AdminServerProperties adminServerProperties) { + this.adminContextPath = adminServerProperties.getContextPath(); + } + + + public SecurityFilterChain configure(HttpSecurity http) throws Exception { + // 登录成功处理类 + SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(adminContextPath + "/"); + + http.authorizeRequests(authorize -> { + try { + authorize + + //静态文件允许访问 + .requestMatchers(adminContextPath + "/assets/**").permitAll() + //登录页面允许访问 + .requestMatchers(adminContextPath + "/login", "/css/**", "/js/**", "/image/*").permitAll() + //其他所有请求需要登录 + .anyRequest().authenticated() + .and() + //登录页面配置,用于替换security默认页面 + .formLogin(formLogin -> formLogin.loginPage(adminContextPath + "/login").successHandler(successHandler)) + //登出页面配置,用于替换security默认页面 + .logout(logout -> logout.logoutUrl(adminContextPath + "/logout")) + .httpBasic(Customizer.withDefaults()) + .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .ignoringRequestMatchers( + "/instances", + "/actuator/**") + ); + } catch (Exception e) { + e.printStackTrace(); + } + } + ); + + return http.build(); + + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-monitor/src/main/resources/application.yml b/test-server-cloud/test-visual/test-cloud-monitor/src/main/resources/application.yml new file mode 100644 index 0000000..f137704 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-monitor/src/main/resources/application.yml @@ -0,0 +1,38 @@ +server: + port: 9111 +spring: + boot: + admin: + ui: + title: JeecgCloud监控中心 + client: + instance: + metadata: + tags: + environment: local + security: + user: + name: "admin" + password: "admin" + application: + name: jeecg-monitor + cloud: + nacos: + discovery: + server-addr: @config.server-addr@ + namespace: @config.namespace@ + metadata: + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} +# 服务端点检查 +management: + httpexchanges: + recording: + enabled: true + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/Dockerfile b/test-server-cloud/test-visual/test-cloud-sentinel/Dockerfile new file mode 100644 index 0000000..7fee925 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/Dockerfile @@ -0,0 +1,25 @@ +FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:17-anolis + +MAINTAINER jeecgos@163.com + +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime + +# 解决linuxkit 精简镜像对 locale 裁剪导致中文乱码问题 java:17-anolis基于anolis(CentOS/RHEL 系)应当使用yum +RUN yum install -y --setopt=tsflags=nodocs \ + glibc-langpack-en \ + glibc-common \ + && yum clean all + +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 +ENV JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8" + +RUN mkdir -p /jeecg-cloud-sentinel + +WORKDIR /jeecg-cloud-sentinel + +EXPOSE 8848 + +ADD ./target/jeecg-cloud-sentinel-3.9.2.jar ./ + +CMD sleep 5 && exec java -Djava.security.egd=file:/dev/./urandom -jar jeecg-cloud-sentinel-3.9.2.jar diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/README.md b/test-server-cloud/test-visual/test-cloud-sentinel/README.md new file mode 100644 index 0000000..b3f2b77 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/README.md @@ -0,0 +1,9 @@ +访问地址: http://localhost:9000 +账号密码:sentinel/sentinel + + +# 使用方法 + +- 1、第一次登录sentinel内容是空的,必须访问了微服务实例的请求才会出现配置 +- 2、sentinel做了深度改造,支持持久化到nacos中 +- 3、目前只针对gateway做的控制,其他服务不需要 diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/pom.xml b/test-server-cloud/test-visual/test-cloud-sentinel/pom.xml new file mode 100644 index 0000000..c70f493 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/pom.xml @@ -0,0 +1,233 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + test-cloud-sentinel + jeecg-cloud-sentinel + sentinel启动模块 + 3.9.2 + + + + org.projectlombok + lombok + + + org.jeecgframework.cloud + sentinel-dashboard + 1.8.3 + + + sentinel-web-servlet + com.alibaba.csp + + + sentinel-transport-simple-http + com.alibaba.csp + + + sentinel-parameter-flow-control + com.alibaba.csp + + + sentinel-core + com.alibaba.csp + + + sentinel-api-gateway-adapter-common + com.alibaba.csp + + + + + com.alibaba.csp + sentinel-datasource-nacos + 1.8.3 + + + sentinel-core + com.alibaba.csp + + + + + com.alibaba.csp + sentinel-core + 1.8.3 + + + com.alibaba.csp + sentinel-web-servlet + 1.8.3 + + + sentinel-core + com.alibaba.csp + + + + + com.alibaba.csp + sentinel-transport-simple-http + 1.8.3 + + + com.alibaba.csp + sentinel-parameter-flow-control + 1.8.3 + + + sentinel-core + com.alibaba.csp + + + + + com.alibaba.csp + sentinel-api-gateway-adapter-common + 1.8.3 + + + sentinel-parameter-flow-control + com.alibaba.csp + + + sentinel-core + com.alibaba.csp + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-undertow + + + + commons-lang + commons-lang + 2.6 + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + org.apache.httpcomponents + httpcore + 4.4.5 + + + org.apache.httpcomponents + httpasyncclient + 4.1.3 + + + org.apache.httpcomponents + httpcore-nio + 4.4.6 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + + dev + + + true + + + + dev + + jeecg-boot-nacos:8848 + + springboot3 + + DEFAULT_GROUP + + + + + + + + + test + + + test + + jeecg-boot-nacos:8848 + + springboot3 + + DEFAULT_GROUP + + + + + + + + + docker + + + docker + + jeecg-boot-nacos:8848 + + springboot3 + + DEFAULT_GROUP + + + + + + + + + prod + + + prod + + jeecg-boot-nacos:8848 + + springboot3 + + DEFAULT_GROUP + + + + + + + + + diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/GhbSentinelApplication.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/GhbSentinelApplication.java new file mode 100644 index 0000000..6e3f5f4 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/GhbSentinelApplication.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.Environment; +import com.alibaba.csp.sentinel.init.InitExecutor; +import lombok.extern.slf4j.Slf4j; + +/** + * Sentinel dashboard application. + * + * @author Carpenter Lee + */ +@SpringBootApplication +@Slf4j +public class GhbSentinelApplication { + + public static void main(String[] args) { + System.setProperty("csp.sentinel.app.type", "1"); + triggerSentinelInit(); + ConfigurableApplicationContext application = SpringApplication.run(GhbSentinelApplication.class, args); + Environment env = application.getEnvironment(); + // 目前Ghb-sentinel 1.8.3 版本存在alibaba-sentinel 1.8.3版本 启动nacos数据源导致配置不生效的问题,以下为临时处理办法 + System.getProperties().setProperty("sentinel.dashboard.auth.username", env.getProperty("sentinel.dashboard.auth.username")); + System.getProperties().setProperty("sentinel.dashboard.auth.password", env.getProperty("sentinel.dashboard.auth.password")); + String port = env.getProperty("server.port"); + log.info("\n----------------------------------------------------------\n\t" + + "Application SentinelDashboard is running! Access URLs:\n\t" + + "Local: \t\thttp://localhost:" + port + "/\n\t" + + "----------------------------------------------------------"); + } + + private static void triggerSentinelInit() { + new Thread(() -> InitExecutor.doInit()).start(); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/constants/SentinelConStants.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/constants/SentinelConStants.java new file mode 100644 index 0000000..acb3498 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/constants/SentinelConStants.java @@ -0,0 +1,39 @@ +package com.alibaba.csp.sentinel.dashboard.constants; + +/** + * sentinel常量配置 + * @author zyf + */ +public class SentinelConStants { + public static final String GROUP_ID = "SENTINEL_GROUP"; + + /** + * 流控规则 + */ + public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules"; + /** + * 热点参数 + */ + public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules"; + /** + * 降级规则 + */ + public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules"; + /** + * 系统规则 + */ + public static final String SYSTEM_DATA_ID_POSTFIX = "-system-rules"; + /** + * 授权规则 + */ + public static final String AUTHORITY_DATA_ID_POSTFIX = "-authority-rules"; + + /** + * 网关API + */ + public static final String GETEWAY_API_DATA_ID_POSTFIX = "-api-rules"; + /** + * 网关流控规则 + */ + public static final String GETEWAY_FLOW_DATA_ID_POSTFIX = "-flow-rules"; +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java new file mode 100644 index 0000000..a6da643 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AuthorityRuleController.java @@ -0,0 +1,181 @@ +package com.alibaba.csp.sentinel.dashboard.controller; + + +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.controller.base.BaseRuleController; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 授权规则控制器 + * + * @author zyf + * @date 2022-04-13 + */ +@RestController +@RequestMapping(value = "/authority") +public class AuthorityRuleController extends BaseRuleController { + + private final Logger logger = LoggerFactory.getLogger(AuthorityRuleController.class); + + @Autowired + private RuleRepository repository; + @Autowired + @Qualifier("authorityRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + @Autowired + @Qualifier("authorityRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, + @RequestParam String ip, + @RequestParam Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip cannot be null or empty"); + } + if (port == null || port <= 0) { + return Result.ofFail(-1, "Invalid parameter: port"); + } + try { + List rules = ruleProvider.getRules(app); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("Error when querying authority rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private Result checkEntityInternal(AuthorityRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "bad rule body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "port can't be null"); + } + if (entity.getRule() == null) { + return Result.ofFail(-1, "rule can't be null"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource name cannot be null or empty"); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp should be valid"); + } + if (entity.getStrategy() != RuleConstant.AUTHORITY_WHITE + && entity.getStrategy() != RuleConstant.AUTHORITY_BLACK) { + return Result.ofFail(-1, "Unknown strategy (must be blacklist or whitelist)"); + } + return null; + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddAuthorityRule(@RequestBody AuthorityRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(null); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + } catch (Throwable throwable) { + logger.error("Failed to add authority rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, + @RequestBody AuthorityRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(null); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + if (entity == null) { + return Result.ofFail(-1, "Failed to save authority rule"); + } + publishRules(entity.getApp()); + } catch (Throwable throwable) { + logger.error("Failed to save authority rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id cannot be null"); + } + AuthorityRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + publishRules(oldEntity.getApp()); + } catch (Exception e) { + return Result.ofFail(-1, e.getMessage()); + } + return Result.ofSuccess(id); + } + + private void publishRules(String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + //延迟加载 + delayTime(); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java new file mode 100644 index 0000000..1a451c2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/DegradeController.java @@ -0,0 +1,209 @@ +package com.alibaba.csp.sentinel.dashboard.controller; + + +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.controller.base.BaseRuleController; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy; +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 降级规则控制器 + * + * @author zyf + * @date 2022-04-13 + */ +@RestController +@RequestMapping("/degrade") +public class DegradeController extends BaseRuleController { + + private final Logger logger = LoggerFactory.getLogger(DegradeController.class); + + @Autowired + private RuleRepository repository; + @Autowired + @Qualifier("degradeRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + @Autowired + @Qualifier("degradeRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/rules.json") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + try { + List rules = ruleProvider.getRules(app); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("queryApps error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/rule") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAddRule(@RequestBody DegradeRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + } catch (Throwable t) { + logger.error("Failed to add new degrade rule, app={}, ip={}", entity.getApp(), entity.getIp(), t); + return Result.ofThrowable(-1, t); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateRule(@PathVariable("id") Long id, + @RequestBody DegradeRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "id can't be null or negative"); + } + DegradeRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "Degrade rule does not exist, id=" + id); + } + entity.setApp(oldEntity.getApp()); + entity.setIp(oldEntity.getIp()); + entity.setPort(oldEntity.getPort()); + entity.setId(oldEntity.getId()); + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(new Date()); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + } catch (Throwable t) { + logger.error("Failed to save degrade rule, id={}, rule={}", id, entity, t); + return Result.ofThrowable(-1, t); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result delete(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + DegradeRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + publishRules(oldEntity.getApp()); + } catch (Throwable throwable) { + logger.error("Failed to delete degrade rule, id={}", id, throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(id); + } + + private void publishRules(/*@NonNull*/ String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + //延迟加载 + delayTime(); + } + + private Result checkEntityInternal(DegradeRuleEntity entity) { + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be blank"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "invalid port: " + entity.getPort()); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + Double threshold = entity.getCount(); + if (threshold == null || threshold < 0) { + return Result.ofFail(-1, "invalid threshold: " + threshold); + } + Integer recoveryTimeoutSec = entity.getTimeWindow(); + if (recoveryTimeoutSec == null || recoveryTimeoutSec <= 0) { + return Result.ofFail(-1, "recoveryTimeout should be positive"); + } + Integer strategy = entity.getGrade(); + if (strategy == null) { + return Result.ofFail(-1, "circuit breaker strategy cannot be null"); + } + if (strategy < CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType() + || strategy > RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) { + return Result.ofFail(-1, "Invalid circuit breaker strategy: " + strategy); + } + if (entity.getMinRequestAmount() == null || entity.getMinRequestAmount() <= 0) { + return Result.ofFail(-1, "Invalid minRequestAmount"); + } + if (entity.getStatIntervalMs() == null || entity.getStatIntervalMs() <= 0) { + return Result.ofFail(-1, "Invalid statInterval"); + } + if (strategy == RuleConstant.DEGRADE_GRADE_RT) { + Double slowRatio = entity.getSlowRatioThreshold(); + if (slowRatio == null) { + return Result.ofFail(-1, "SlowRatioThreshold is required for slow request ratio strategy"); + } else if (slowRatio < 0 || slowRatio > 1) { + return Result.ofFail(-1, "SlowRatioThreshold should be in range: [0.0, 1.0]"); + } + } else if (strategy == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { + if (threshold > 1) { + return Result.ofFail(-1, "Ratio threshold should be in range: [0.0, 1.0]"); + } + } + return null; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java new file mode 100644 index 0000000..1c40256 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/ParamFlowRuleController.java @@ -0,0 +1,253 @@ +package com.alibaba.csp.sentinel.dashboard.controller; + + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.client.CommandNotFoundException; +import com.alibaba.csp.sentinel.dashboard.controller.base.BaseRuleController; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.dashboard.util.VersionUtils; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +/** + * 热点参数规则控制器 + * + * @author zyf + * @date 2022-04-13 + */ +@RestController +@RequestMapping(value = "/paramFlow") +public class ParamFlowRuleController extends BaseRuleController { + + private final Logger logger = LoggerFactory.getLogger(ParamFlowRuleController.class); + + @Autowired + private AppManagement appManagement; + @Autowired + private RuleRepository repository; + @Autowired + @Qualifier("paramFlowRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + @Autowired + @Qualifier("paramFlowRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + private boolean checkIfSupported(String app, String ip, int port) { + try { + return Optional.ofNullable(appManagement.getDetailApp(app)) + .flatMap(e -> e.getMachine(ip, port)) + .flatMap(m -> VersionUtils.parseVersion(m.getVersion()) + .map(v -> v.greaterOrEqual(version020))) + .orElse(true); + // If error occurred or cannot retrieve machine info, return true. + } catch (Exception ex) { + return true; + } + } + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryAllRulesForMachine(@RequestParam String app, + @RequestParam String ip, + @RequestParam Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app cannot be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip cannot be null or empty"); + } + if (port == null || port <= 0) { + return Result.ofFail(-1, "Invalid parameter: port"); + } + if (!checkIfSupported(app, ip, port)) { + return unsupportedVersion(); + } + try { + List rules = ruleProvider.getRules(app); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (ExecutionException ex) { + logger.error("Error when querying parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } else { + return Result.ofThrowable(-1, ex.getCause()); + } + } catch (Throwable throwable) { + logger.error("Error when querying parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private boolean isNotSupported(Throwable ex) { + return ex instanceof CommandNotFoundException; + } + + @PostMapping("/rule") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result apiAddParamFlowRule(@RequestBody ParamFlowRuleEntity entity) { + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) { + return unsupportedVersion(); + } + entity.setId(null); + entity.getRule().setResource(entity.getResource().trim()); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + return Result.ofSuccess(entity); + } catch (ExecutionException ex) { + logger.error("Error when adding new parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } else { + return Result.ofThrowable(-1, ex.getCause()); + } + } catch (Throwable throwable) { + logger.error("Error when adding new parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private Result checkEntityInternal(ParamFlowRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "bad rule body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getIp())) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (entity.getPort() == null || entity.getPort() <= 0) { + return Result.ofFail(-1, "port can't be null"); + } + if (entity.getRule() == null) { + return Result.ofFail(-1, "rule can't be null"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource name cannot be null or empty"); + } + if (entity.getCount() < 0) { + return Result.ofFail(-1, "count should be valid"); + } + if (entity.getGrade() != RuleConstant.FLOW_GRADE_QPS) { + return Result.ofFail(-1, "Unknown mode (blockGrade) for parameter flow control"); + } + if (entity.getParamIdx() == null || entity.getParamIdx() < 0) { + return Result.ofFail(-1, "paramIdx should be valid"); + } + if (entity.getDurationInSec() <= 0) { + return Result.ofFail(-1, "durationInSec should be valid"); + } + if (entity.getControlBehavior() < 0) { + return Result.ofFail(-1, "controlBehavior should be valid"); + } + return null; + } + + @PutMapping("/rule/{id}") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result apiUpdateParamFlowRule(@PathVariable("id") Long id, + @RequestBody ParamFlowRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + ParamFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "id " + id + " does not exist"); + } + + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) { + return unsupportedVersion(); + } + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + return Result.ofSuccess(entity); + } catch (ExecutionException ex) { + logger.error("Error when updating parameter flow rules, id=" + id, ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } else { + return Result.ofThrowable(-1, ex.getCause()); + } + } catch (Throwable throwable) { + logger.error("Error when updating parameter flow rules, id=" + id, throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null) { + return Result.ofFail(-1, "id cannot be null"); + } + ParamFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + publishRules(oldEntity.getApp()); + return Result.ofSuccess(id); + } catch (ExecutionException ex) { + logger.error("Error when deleting parameter flow rules", ex.getCause()); + if (isNotSupported(ex.getCause())) { + return unsupportedVersion(); + } else { + return Result.ofThrowable(-1, ex.getCause()); + } + } catch (Throwable throwable) { + logger.error("Error when deleting parameter flow rules", throwable); + return Result.ofFail(-1, throwable.getMessage()); + } + } + + private void publishRules(String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + //延迟加载 + delayTime(); + } + + private Result unsupportedVersion() { + return Result.ofFail(4041, + "Sentinel client not supported for parameter flow control (unsupported version or dependency absent)"); + } + + private final SentinelVersion version020 = new SentinelVersion().setMinorVersion(2); +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java new file mode 100644 index 0000000..74ba453 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/SystemController.java @@ -0,0 +1,242 @@ +package com.alibaba.csp.sentinel.dashboard.controller; + + +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.controller.base.BaseRuleController; +import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.SystemRuleEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +/** + * 系统规则控制器 + * + * @author zyf + * @date 2022-04-13 + */ +@RestController +@RequestMapping("/system") +public class SystemController extends BaseRuleController { + + private final Logger logger = LoggerFactory.getLogger(SystemController.class); + + @Autowired + private RuleRepository repository; + @Autowired + @Qualifier("systemRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + @Autowired + @Qualifier("systemRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + private Result checkBasicParams(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + if (port <= 0 || port > 65535) { + return Result.ofFail(-1, "port should be in (0, 65535)"); + } + return null; + } + + @GetMapping("/rules.json") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(String app, String ip, + Integer port) { + Result> checkResult = checkBasicParams(app, ip, port); + if (checkResult != null) { + return checkResult; + } + try { + List rules = ruleProvider.getRules(app); + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("Query machine system rules error", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + private int countNotNullAndNotNegative(Number... values) { + int notNullCount = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] != null && values[i].doubleValue() >= 0) { + notNullCount++; + } + } + return notNullCount; + } + + @RequestMapping("/new.json") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiAdd(String app, String ip, Integer port, + Double highestSystemLoad, Double highestCpuUsage, Long avgRt, + Long maxThread, Double qps) { + + Result checkResult = checkBasicParams(app, ip, port); + if (checkResult != null) { + return checkResult; + } + + int notNullCount = countNotNullAndNotNegative(highestSystemLoad, avgRt, maxThread, qps, highestCpuUsage); + if (notNullCount != 1) { + return Result.ofFail(-1, "only one of [highestSystemLoad, avgRt, maxThread, qps,highestCpuUsage] " + + "value must be set > 0, but " + notNullCount + " values get"); + } + if (null != highestCpuUsage && highestCpuUsage > 1) { + return Result.ofFail(-1, "highestCpuUsage must between [0.0, 1.0]"); + } + SystemRuleEntity entity = new SystemRuleEntity(); + entity.setApp(app.trim()); + entity.setIp(ip.trim()); + entity.setPort(port); + // -1 is a fake value + if (null != highestSystemLoad) { + entity.setHighestSystemLoad(highestSystemLoad); + } else { + entity.setHighestSystemLoad(-1D); + } + + if (null != highestCpuUsage) { + entity.setHighestCpuUsage(highestCpuUsage); + } else { + entity.setHighestCpuUsage(-1D); + } + + if (avgRt != null) { + entity.setAvgRt(avgRt); + } else { + entity.setAvgRt(-1L); + } + if (maxThread != null) { + entity.setMaxThread(maxThread); + } else { + entity.setMaxThread(-1L); + } + if (qps != null) { + entity.setQps(qps); + } else { + entity.setQps(-1D); + } + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(app); + } catch (Throwable throwable) { + logger.error("Add SystemRule error", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @GetMapping("/save.json") + @AuthAction(PrivilegeType.WRITE_RULE) + public Result apiUpdateIfNotNull(Long id, String app, Double highestSystemLoad, + Double highestCpuUsage, Long avgRt, Long maxThread, Double qps) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + SystemRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "id " + id + " dose not exist"); + } + + if (StringUtil.isNotBlank(app)) { + entity.setApp(app.trim()); + } + if (highestSystemLoad != null) { + if (highestSystemLoad < 0) { + return Result.ofFail(-1, "highestSystemLoad must >= 0"); + } + entity.setHighestSystemLoad(highestSystemLoad); + } + if (highestCpuUsage != null) { + if (highestCpuUsage < 0) { + return Result.ofFail(-1, "highestCpuUsage must >= 0"); + } + if (highestCpuUsage > 1) { + return Result.ofFail(-1, "highestCpuUsage must <= 1"); + } + entity.setHighestCpuUsage(highestCpuUsage); + } + if (avgRt != null) { + if (avgRt < 0) { + return Result.ofFail(-1, "avgRt must >= 0"); + } + entity.setAvgRt(avgRt); + } + if (maxThread != null) { + if (maxThread < 0) { + return Result.ofFail(-1, "maxThread must >= 0"); + } + entity.setMaxThread(maxThread); + } + if (qps != null) { + if (qps < 0) { + return Result.ofFail(-1, "qps must >= 0"); + } + entity.setQps(qps); + } + Date date = new Date(); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + } catch (Throwable throwable) { + logger.error("save error:", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @RequestMapping("/delete.json") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result delete(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + SystemRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + try { + repository.delete(id); + publishRules(oldEntity.getApp()); + } catch (Throwable throwable) { + logger.error("delete error:", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(id); + } + + private void publishRules(String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + //延迟加载 + delayTime(); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/base/BaseRuleController.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/base/BaseRuleController.java new file mode 100644 index 0000000..d163f65 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/base/BaseRuleController.java @@ -0,0 +1,26 @@ +package com.alibaba.csp.sentinel.dashboard.controller.base; + +import java.util.concurrent.TimeUnit; + + +/** + * Nacos持久化通用处理类 + * + * @author zyf + * @date 2022-04-13 + */ +public class BaseRuleController { + /** + * 延迟一下 + * + * 解释:列表加载数据的时候,Nacos持久化还没做完,导致加载数据不对 + */ + public static void delayTime(){ + try { + TimeUnit.MILLISECONDS.sleep(100); + System.out.println("-------------睡100毫秒-----------"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java new file mode 100644 index 0000000..df641c0 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java @@ -0,0 +1,260 @@ +package com.alibaba.csp.sentinel.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.controller.base.BaseRuleController; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiPredicateItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.AddApiReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.ApiPredicateItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.UpdateApiReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemApiDefinitionStore; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; + +/** + * 网关API规则控制器 + * + * @author zyf + * @date 2022-04-13 + */ +@RestController +@RequestMapping(value = "/gateway/api") +public class GatewayApiController extends BaseRuleController { + + private final Logger logger = LoggerFactory.getLogger(GatewayApiController.class); + + @Autowired + private InMemApiDefinitionStore repository; + + + @Autowired + @Qualifier("gateWayApiNacosProvider") + private DynamicRuleProvider> apiProvider; + + @Autowired + @Qualifier("gateWayApiNacosPublisher") + private DynamicRulePublisher> apiPublisher; + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryApis(String app, String ip, Integer port) { + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List apis = apiProvider.getRules(app); + repository.saveAll(apis); + return Result.ofSuccess(apis); + } catch (Throwable throwable) { + logger.error("queryApis error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addApi(HttpServletRequest request, @RequestBody AddApiReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API名称 + String apiName = reqVo.getApiName(); + if (StringUtil.isBlank(apiName)) { + return Result.ofFail(-1, "apiName can't be null or empty"); + } + entity.setApiName(apiName.trim()); + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + Integer matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + // 检查API名称不能重复 + List allApis = repository.findAllByMachine(MachineInfo.of(app.trim(), ip.trim(), port)); + if (allApis.stream().map(o -> o.getApiName()).anyMatch(o -> o.equals(apiName.trim()))) { + return Result.ofFail(-1, "apiName exists: " + apiName); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, ip, port)) { + logger.warn("publish gateway apis fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateApi(@RequestBody UpdateApiReqVo reqVo) { + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "api does not exist, id=" + id); + } + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + int matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "Invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("update gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway apis fail after update"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + public Result deleteApi(Long id) { + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway apis fail after delete"); + } + return Result.ofSuccess(id); + } + + private boolean publishApis(String app, String ip, Integer port) { + List apis = repository.findAllByApp(app); + try { + apiPublisher.publish(app, apis); + //延迟加载 + delayTime(); + return true; + } catch (Exception e) { + logger.error("publish api error!"); + e.printStackTrace(); + return false; + } + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java new file mode 100644 index 0000000..a0589c4 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java @@ -0,0 +1,431 @@ +package com.alibaba.csp.sentinel.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.controller.base.BaseRuleController; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayParamFlowItemEntity; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.AddFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.GatewayParamFlowItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.UpdateFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemGatewayFlowRuleStore; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.*; +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity.*; + +/** + * 网关限流规则控制器 + * + * @author zyf + * @date 2022-04-13 + */ +@RestController +@RequestMapping(value = "/gateway/flow") +public class GatewayFlowRuleController extends BaseRuleController { + + private final Logger logger = LoggerFactory.getLogger(GatewayFlowRuleController.class); + + @Autowired + private InMemGatewayFlowRuleStore repository; + + @Autowired + @Qualifier("gateWayFlowRulesNacosProvider") + private DynamicRuleProvider> ruleProvider; + + @Autowired + @Qualifier("gateWayFlowRulesNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/list.json") + @AuthAction(AuthService.PrivilegeType.READ_RULE) + public Result> queryFlowRules(String app, String ip, Integer port) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List rules = ruleProvider.getRules(app); + repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("query gateway flow rules error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result addFlowRule(@RequestBody AddFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API类型, Route ID或API分组 + Integer resourceMode = reqVo.getResourceMode(); + if (resourceMode == null) { + return Result.ofFail(-1, "resourceMode can't be null"); + } + if (!Arrays.asList(RESOURCE_MODE_ROUTE_ID, RESOURCE_MODE_CUSTOM_API_NAME).contains(resourceMode)) { + return Result.ofFail(-1, "invalid resourceMode: " + resourceMode); + } + entity.setResourceMode(resourceMode); + + // API名称 + String resource = reqVo.getResource(); + if (StringUtil.isBlank(resource)) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + entity.setResource(resource.trim()); + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER + , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + } + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 1-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, ip, port)) { + logger.warn("publish gateway flow rules fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + public Result updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) { + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id); + } + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER + , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + } + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } else { + entity.setParamItem(null); + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 2-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("update gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway flow rules fail after update"); + } + + return Result.ofSuccess(entity); + } + + + @PostMapping("/delete.json") + @AuthAction(AuthService.PrivilegeType.DELETE_RULE) + public Result deleteFlowRule(Long id) { + + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway flow rules fail after delete"); + } + + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByApp(app); + try { + rulePublisher.publish(app, rules); + //延迟加载 + delayTime(); + return true; + } catch (Exception e) { + logger.error("publish rules error!"); + e.printStackTrace(); + return false; + } + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java new file mode 100644 index 0000000..74f58d1 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java @@ -0,0 +1,230 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.controller.v2; + +import java.util.Date; +import java.util.List; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; +import com.alibaba.csp.sentinel.dashboard.controller.base.BaseRuleController; +import com.alibaba.csp.sentinel.util.StringUtil; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.dashboard.domain.Result; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 流控规则控制器 + * + * @author zyf + * @date 2022-04-13 + */ +@RestController +@RequestMapping(value = "/v2/flow") +public class FlowControllerV2 extends BaseRuleController { + + private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class); + + @Autowired + private InMemoryRuleRepositoryAdapter repository; + + @Autowired + @Qualifier("flowRuleNacosProvider") + private DynamicRuleProvider> ruleProvider; + @Autowired + @Qualifier("flowRuleNacosPublisher") + private DynamicRulePublisher> rulePublisher; + + @GetMapping("/rules") + @AuthAction(PrivilegeType.READ_RULE) + public Result> apiQueryMachineRules(@RequestParam String app) { + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + try { + List rules = ruleProvider.getRules(app); + if (rules != null && !rules.isEmpty()) { + for (FlowRuleEntity entity : rules) { + entity.setApp(app); + if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null) { + entity.setId(entity.getClusterConfig().getFlowId()); + } + } + } + rules = repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("Error when querying flow rules", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + private Result checkEntityInternal(FlowRuleEntity entity) { + if (entity == null) { + return Result.ofFail(-1, "invalid body"); + } + if (StringUtil.isBlank(entity.getApp())) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isBlank(entity.getLimitApp())) { + return Result.ofFail(-1, "limitApp can't be null or empty"); + } + if (StringUtil.isBlank(entity.getResource())) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + if (entity.getGrade() == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (entity.getGrade() != 0 && entity.getGrade() != 1) { + return Result.ofFail(-1, "grade must be 0 or 1, but " + entity.getGrade() + " got"); + } + if (entity.getCount() == null || entity.getCount() < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + if (entity.getStrategy() == null) { + return Result.ofFail(-1, "strategy can't be null"); + } + if (entity.getStrategy() != 0 && StringUtil.isBlank(entity.getRefResource())) { + return Result.ofFail(-1, "refResource can't be null or empty when strategy!=0"); + } + if (entity.getControlBehavior() == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + int controlBehavior = entity.getControlBehavior(); + if (controlBehavior == 1 && entity.getWarmUpPeriodSec() == null) { + return Result.ofFail(-1, "warmUpPeriodSec can't be null when controlBehavior==1"); + } + if (controlBehavior == 2 && entity.getMaxQueueingTimeMs() == null) { + return Result.ofFail(-1, "maxQueueingTimeMs can't be null when controlBehavior==2"); + } + if (entity.isClusterMode() && entity.getClusterConfig() == null) { + return Result.ofFail(-1, "cluster config should be valid"); + } + return null; + } + + @PostMapping("/rule") + @AuthAction(value = AuthService.PrivilegeType.WRITE_RULE) + public Result apiAddFlowRule(@RequestBody FlowRuleEntity entity) { + + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + entity.setId(null); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + entity.setLimitApp(entity.getLimitApp().trim()); + entity.setResource(entity.getResource().trim()); + try { + entity = repository.save(entity); + publishRules(entity.getApp()); + } catch (Throwable throwable) { + logger.error("Failed to add flow rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @PutMapping("/rule/{id}") + @AuthAction(AuthService.PrivilegeType.WRITE_RULE) + + public Result apiUpdateFlowRule(@PathVariable("id") Long id, + @RequestBody FlowRuleEntity entity) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + FlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofFail(-1, "id " + id + " does not exist"); + } + if (entity == null) { + return Result.ofFail(-1, "invalid body"); + } + + entity.setApp(oldEntity.getApp()); + entity.setIp(oldEntity.getIp()); + entity.setPort(oldEntity.getPort()); + Result checkResult = checkEntityInternal(entity); + if (checkResult != null) { + return checkResult; + } + + entity.setId(id); + Date date = new Date(); + entity.setGmtCreate(oldEntity.getGmtCreate()); + entity.setGmtModified(date); + try { + entity = repository.save(entity); + if (entity == null) { + return Result.ofFail(-1, "save entity fail"); + } + publishRules(oldEntity.getApp()); + } catch (Throwable throwable) { + logger.error("Failed to update flow rule", throwable); + return Result.ofThrowable(-1, throwable); + } + return Result.ofSuccess(entity); + } + + @DeleteMapping("/rule/{id}") + @AuthAction(PrivilegeType.DELETE_RULE) + public Result apiDeleteRule(@PathVariable("id") Long id) { + if (id == null || id <= 0) { + return Result.ofFail(-1, "Invalid id"); + } + FlowRuleEntity oldEntity = repository.findById(id); + if (ObjectUtils.isEmpty(oldEntity)) { + return Result.ofSuccess(null); + } + + try { + repository.delete(id); + publishRules(oldEntity.getApp()); + } catch (Exception e) { + return Result.ofFail(-1, e.getMessage()); + } + return Result.ofSuccess(id); + } + + private void publishRules(/*@NonNull*/ String app) throws Exception { + List rules = repository.findAllByApp(app); + rulePublisher.publish(app, rules); + //延迟加载 + delayTime(); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigProperties.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigProperties.java new file mode 100644 index 0000000..c6f8a5d --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigProperties.java @@ -0,0 +1,32 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +/** + * @Description: nacos配置 + * @author: zyf + * @date: 2022/03/01$ + * @version: V1.0 + */ +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "nacos.server") +@Data +public class NacosConfigProperties { + + private String ip; + + private String namespace; + + private String username; + + private String password; + + private String groupId; + + public String getServerAddr() { + return this.getIp(); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/SentinelConfig.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/SentinelConfig.java new file mode 100644 index 0000000..de9450e --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/SentinelConfig.java @@ -0,0 +1,165 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos; + +import java.util.List; +import java.util.Properties; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.*; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.entity.AuthorityRuleCorrectEntity; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.entity.ParamFlowRuleCorrectEntity; +import com.alibaba.nacos.api.PropertyKeyConst; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.api.config.ConfigFactory; +import com.alibaba.nacos.api.config.ConfigService; + +/** + * sentinel配置类 + * + * @author zyf + * @date 2022-04-13 + */ +@Configuration +public class SentinelConfig { + + @Autowired + private NacosConfigProperties nacosConfigProperties; + + + /** + * 流控规则 + * @return + */ + @Bean + public Converter, String> flowRuleEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> flowRuleEntityDecoder() { + return s -> JSON.parseArray(s, FlowRuleEntity.class); + } + /** + * 降级规则 + * @return + */ + @Bean + public Converter, String> degradeRuleEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> degradeRuleEntityDecoder() { + return s -> JSON.parseArray(s, DegradeRuleEntity.class); + } + + /** + * 热点参数 规则 + * @return + */ + @Bean + public Converter, String> paramFlowRuleEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> paramFlowRuleEntityDecoder() { + return s -> JSON.parseArray(s, ParamFlowRuleCorrectEntity.class); + } + + /** + * 系统规则 + * @return + */ + @Bean + public Converter, String> systemRuleRuleEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> systemRuleRuleEntityDecoder() { + return s -> JSON.parseArray(s, SystemRuleEntity.class); + } + /** + * 授权规则 + * @return + */ + @Bean + public Converter, String> authorityRuleRuleEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> authorityRuleRuleEntityDecoder() { + return s -> JSON.parseArray(s, AuthorityRuleCorrectEntity.class); + } + + /** + * 网关API + * + * @return + * @throws Exception + */ + @Bean + public Converter, String> apiDefinitionEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> apiDefinitionEntityDecoder() { + return s -> JSON.parseArray(s, ApiDefinitionEntity.class); + } + + /** + * 网关flowRule + * + * @return + * @throws Exception + */ + @Bean + public Converter, String> gatewayFlowRuleEntityEncoder() { + return JSON::toJSONString; + } + + @Bean + public Converter> gatewayFlowRuleEntityDecoder() { + return s -> JSON.parseArray(s, GatewayFlowRuleEntity.class); + } + + @Bean + public ConfigService nacosConfigService() throws Exception { + Properties properties=new Properties(); + properties.put(PropertyKeyConst.SERVER_ADDR,nacosConfigProperties.getServerAddr()); + if(StringUtils.isNotBlank(nacosConfigProperties.getUsername())){ + properties.put(PropertyKeyConst.USERNAME,nacosConfigProperties.getUsername()); + } + if(StringUtils.isNotBlank(nacosConfigProperties.getPassword())){ + properties.put(PropertyKeyConst.PASSWORD,nacosConfigProperties.getPassword()); + } + if(StringUtils.isNotBlank(nacosConfigProperties.getNamespace())){ + properties.put(PropertyKeyConst.NAMESPACE,nacosConfigProperties.getNamespace()); + } + return ConfigFactory.createConfigService(properties); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/authority/AuthorityRuleNacosProvider.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/authority/AuthorityRuleNacosProvider.java new file mode 100644 index 0000000..a37c0b4 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/authority/AuthorityRuleNacosProvider.java @@ -0,0 +1,50 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.authority; + + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.entity.AuthorityRuleCorrectEntity; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 授权规则拉取(黑名单白名单) + * + * @author zyf + * @date 2022-04-13 + */ +@Component("authorityRuleNacosProvider") +public class AuthorityRuleNacosProvider implements DynamicRuleProvider> { + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + SentinelConStants.AUTHORITY_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + List entityList = converter.convert(rules); + return entityList.stream().map(rule -> { + AuthorityRule authorityRule = new AuthorityRule(); + BeanUtils.copyProperties(rule, authorityRule); + AuthorityRuleEntity entity = AuthorityRuleEntity.fromAuthorityRule(rule.getApp(), rule.getIp(), rule.getPort(), authorityRule); + entity.setId(rule.getId()); + entity.setGmtCreate(rule.getGmtCreate()); + return entity; + }).collect(Collectors.toList()); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/authority/AuthorityRuleNacosPublisher.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/authority/AuthorityRuleNacosPublisher.java new file mode 100644 index 0000000..0c673b2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/authority/AuthorityRuleNacosPublisher.java @@ -0,0 +1,47 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.authority; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.AuthorityRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.entity.AuthorityRuleCorrectEntity; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 授权规则持久化(黑名单白名单) + * + * @author zyf + * @date 2022-04-13 + */ +@Component("authorityRuleNacosPublisher") +public class AuthorityRuleNacosPublisher implements DynamicRulePublisher> { + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String app, List rules) throws Exception { + AssertUtil.notEmpty(app, "app name cannot be empty"); + if (rules == null) { + return; + } + // 转换 + List list = rules.stream().map(rule -> { + AuthorityRuleCorrectEntity entity = new AuthorityRuleCorrectEntity(); + BeanUtils.copyProperties(rule, entity); + return entity; + }).collect(Collectors.toList()); + + configService.publishConfig(app + SentinelConStants.AUTHORITY_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, converter.convert(list)); + } +} + diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/degrade/DegradeRuleNacosProvider.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/degrade/DegradeRuleNacosProvider.java new file mode 100644 index 0000000..6f425a2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/degrade/DegradeRuleNacosProvider.java @@ -0,0 +1,39 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.degrade; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 降级规则拉取 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("degradeRuleNacosProvider") +public class DegradeRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + SentinelConStants.DEGRADE_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/degrade/DegradeRuleNacosPublisher.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/degrade/DegradeRuleNacosPublisher.java new file mode 100644 index 0000000..a0e844b --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/degrade/DegradeRuleNacosPublisher.java @@ -0,0 +1,38 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.degrade; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 降级规则推送 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("degradeRuleNacosPublisher") +public class DegradeRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String app, List rules) throws Exception { + AssertUtil.notEmpty(app, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(app + SentinelConStants.DEGRADE_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, converter.convert(rules)); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/entity/AuthorityRuleCorrectEntity.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/entity/AuthorityRuleCorrectEntity.java new file mode 100644 index 0000000..92bcaca --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/entity/AuthorityRuleCorrectEntity.java @@ -0,0 +1,110 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.entity; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; + +import java.util.Date; + +/** + * @author zyf + * @description 重写授权规则实体类,原因同热点规则 + * @date 2022-04-13 + */ +public class AuthorityRuleCorrectEntity implements RuleEntity { + + private Long id; + private String app; + private String ip; + private Integer port; + private String limitApp; + private String resource; + private Date gmtCreate; + private Date gmtModified; + + private int strategy; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getLimitApp() { + return limitApp; + } + + public void setLimitApp(String limitApp) { + this.limitApp = limitApp; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public int getStrategy() { + return strategy; + } + + public void setStrategy(int strategy) { + this.strategy = strategy; + } + + @Override + public Rule toRule(){ + AuthorityRule rule=new AuthorityRule(); + return rule; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/entity/ParamFlowRuleCorrectEntity.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/entity/ParamFlowRuleCorrectEntity.java new file mode 100644 index 0000000..f7bb4b8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/entity/ParamFlowRuleCorrectEntity.java @@ -0,0 +1,194 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.entity; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowClusterConfig; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowItem; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; + +import java.util.*; + +/** + * @author zyf + * @description 重写热点规则实体类,。查看sentinel-dashboard在自定义ParamFlowRuleNacosPublisher时候 推送的数据是ParamFlowRuleEntity。 客户端接收的ParamFlowRule类 + * @date 2022-04-13 + */ +public class ParamFlowRuleCorrectEntity implements RuleEntity { + + private Long id; + private String app; + private String ip; + private Integer port; + private String limitApp; + private String resource; + private Date gmtCreate; + + private int grade = 1; + private Integer paramIdx; + private double count; + private int controlBehavior = 0; + private int maxQueueingTimeMs = 0; + private int burstCount = 0; + private long durationInSec = 1L; + private List paramFlowItemList = new ArrayList(); + private Map hotItems = new HashMap(); + private boolean clusterMode = false; + private ParamFlowClusterConfig clusterConfig; + + public int getGrade() { + return grade; + } + + public void setGrade(int grade) { + this.grade = grade; + } + + public Integer getParamIdx() { + return paramIdx; + } + + public void setParamIdx(Integer paramIdx) { + this.paramIdx = paramIdx; + } + + public double getCount() { + return count; + } + + public void setCount(double count) { + this.count = count; + } + + public int getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(int controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public int getMaxQueueingTimeMs() { + return maxQueueingTimeMs; + } + + public void setMaxQueueingTimeMs(int maxQueueingTimeMs) { + this.maxQueueingTimeMs = maxQueueingTimeMs; + } + + public int getBurstCount() { + return burstCount; + } + + public void setBurstCount(int burstCount) { + this.burstCount = burstCount; + } + + public long getDurationInSec() { + return durationInSec; + } + + public void setDurationInSec(long durationInSec) { + this.durationInSec = durationInSec; + } + + public List getParamFlowItemList() { + return paramFlowItemList; + } + + public void setParamFlowItemList(List paramFlowItemList) { + this.paramFlowItemList = paramFlowItemList; + } + + public Map getHotItems() { + return hotItems; + } + + public void setHotItems(Map hotItems) { + this.hotItems = hotItems; + } + + public boolean isClusterMode() { + return clusterMode; + } + + public void setClusterMode(boolean clusterMode) { + this.clusterMode = clusterMode; + } + + public ParamFlowClusterConfig getClusterConfig() { + return clusterConfig; + } + + public void setClusterConfig(ParamFlowClusterConfig clusterConfig) { + this.clusterConfig = clusterConfig; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getLimitApp() { + return limitApp; + } + + public void setLimitApp(String limitApp) { + this.limitApp = limitApp; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + @Override + public Rule toRule() { + ParamFlowRule rule = new ParamFlowRule(); + return rule; + } +} + diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/flow/FlowRuleNacosProvider.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/flow/FlowRuleNacosProvider.java new file mode 100644 index 0000000..5287673 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/flow/FlowRuleNacosProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.flow; + +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; + +/** + * 流控规则拉取 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("flowRuleNacosProvider") +public class FlowRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + SentinelConStants.FLOW_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/flow/FlowRuleNacosPublisher.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/flow/FlowRuleNacosPublisher.java new file mode 100644 index 0000000..87c2335 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/flow/FlowRuleNacosPublisher.java @@ -0,0 +1,54 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.flow; + +import java.util.List; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; + +/** + * 流控规则推送 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("flowRuleNacosPublisher") +public class FlowRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String app, List rules) throws Exception { + AssertUtil.notEmpty(app, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(app + SentinelConStants.FLOW_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, converter.convert(rules)); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayApiNacosProvider.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayApiNacosProvider.java new file mode 100644 index 0000000..319c061 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayApiNacosProvider.java @@ -0,0 +1,35 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.gateway; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +/** + * 网关API规则拉取 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("gateWayApiNacosProvider") +public class GateWayApiNacosProvider implements DynamicRuleProvider> { + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName+ SentinelConStants.GETEWAY_API_DATA_ID_POSTFIX + , SentinelConStants.GROUP_ID,3000); + if(StringUtil.isEmpty(rules)){ + return new ArrayList<>(); + } + return converter.convert(rules); + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayApiNacosPublisher.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayApiNacosPublisher.java new file mode 100644 index 0000000..955ae3b --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayApiNacosPublisher.java @@ -0,0 +1,35 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.gateway; + + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +/** + * 网关API规则推送 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("gateWayApiNacosPublisher") +public class GateWayApiNacosPublisher implements DynamicRulePublisher> { + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + @Override + public void publish(String app, List rules) throws Exception { + AssertUtil.notEmpty(app, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(app+ SentinelConStants.GETEWAY_API_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID,converter.convert(rules)); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayFlowRulesNacosProvider.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayFlowRulesNacosProvider.java new file mode 100644 index 0000000..5bbe785 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayFlowRulesNacosProvider.java @@ -0,0 +1,40 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.gateway; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 网关流控规则拉取 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("gateWayFlowRulesNacosProvider") +public class GateWayFlowRulesNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + SentinelConStants.GETEWAY_FLOW_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayFlowRulesNacosPublisher.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayFlowRulesNacosPublisher.java new file mode 100644 index 0000000..8b7defd --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/gateway/GateWayFlowRulesNacosPublisher.java @@ -0,0 +1,41 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.gateway; + + +import java.util.List; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; + +/** + * 网关流控规则推送 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("gateWayFlowRulesNacosPublisher") +public class GateWayFlowRulesNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + + @Override + public void publish(String app, List rules) throws Exception { + AssertUtil.notEmpty(app, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(app + SentinelConStants.GETEWAY_FLOW_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, converter.convert(rules)); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/paramflow/ParamFlowRuleNacosProvider.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/paramflow/ParamFlowRuleNacosProvider.java new file mode 100644 index 0000000..140d636 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/paramflow/ParamFlowRuleNacosProvider.java @@ -0,0 +1,52 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.paramflow; + + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.entity.ParamFlowRuleCorrectEntity; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 加载热点参数规则 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("paramFlowRuleNacosProvider") +public class ParamFlowRuleNacosProvider implements DynamicRuleProvider> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + SentinelConStants.PARAM_FLOW_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + List entityList = converter.convert(rules); + return entityList.stream().map(rule -> { + ParamFlowRule paramFlowRule = new ParamFlowRule(); + BeanUtils.copyProperties(rule, paramFlowRule); + ParamFlowRuleEntity entity = ParamFlowRuleEntity.fromParamFlowRule(rule.getApp(), rule.getIp(), rule.getPort(), paramFlowRule); + entity.setId(rule.getId()); + entity.setGmtCreate(rule.getGmtCreate()); + return entity; + }).collect(Collectors.toList()); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/paramflow/ParamFlowRuleNacosPublisher.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/paramflow/ParamFlowRuleNacosPublisher.java new file mode 100644 index 0000000..b00319b --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/paramflow/ParamFlowRuleNacosPublisher.java @@ -0,0 +1,51 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.paramflow; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.ParamFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.SentinelConfig; +import com.alibaba.csp.sentinel.dashboard.rule.nacos.entity.ParamFlowRuleCorrectEntity; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 持久化热点参数规则 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("paramFlowRuleNacosPublisher") +public class ParamFlowRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String app, List rules) throws Exception { + AssertUtil.notEmpty(app, "app name cannot be empty"); + if (rules == null) { + return; + } + rules.forEach(e -> e.setApp(app)); + + // 转换 + List list = rules.stream().map(rule -> { + ParamFlowRuleCorrectEntity entity = new ParamFlowRuleCorrectEntity(); + BeanUtils.copyProperties(rule, entity); + return entity; + }).collect(Collectors.toList()); + + configService.publishConfig(app + SentinelConStants.PARAM_FLOW_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, converter.convert(list)); + + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/system/SystemRuleNacosProvider.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/system/SystemRuleNacosProvider.java new file mode 100644 index 0000000..c242426 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/system/SystemRuleNacosProvider.java @@ -0,0 +1,37 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.system; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.SystemRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.StringUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 加载系统规则 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("systemRuleNacosProvider") +public class SystemRuleNacosProvider implements DynamicRuleProvider> { + @Autowired + private ConfigService configService; + @Autowired + private Converter> converter; + + @Override + public List getRules(String appName) throws Exception { + String rules = configService.getConfig(appName + SentinelConStants.SYSTEM_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, 3000); + if (StringUtil.isEmpty(rules)) { + return new ArrayList<>(); + } + return converter.convert(rules); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/system/SystemRuleNacosPublisher.java b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/system/SystemRuleNacosPublisher.java new file mode 100644 index 0000000..1617ebc --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/system/SystemRuleNacosPublisher.java @@ -0,0 +1,37 @@ +package com.alibaba.csp.sentinel.dashboard.rule.nacos.system; + +import com.alibaba.csp.sentinel.dashboard.constants.SentinelConStants; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.SystemRuleEntity; +import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.util.AssertUtil; +import com.alibaba.nacos.api.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 持久化系统规则 + * + * @author zyf + * @date 2022-04-13 + */ +@Component("systemRuleNacosPublisher") +public class SystemRuleNacosPublisher implements DynamicRulePublisher> { + + @Autowired + private ConfigService configService; + @Autowired + private Converter, String> converter; + + @Override + public void publish(String app, List rules) throws Exception { + AssertUtil.notEmpty(app, "app name cannot be empty"); + if (rules == null) { + return; + } + configService.publishConfig(app + SentinelConStants.SYSTEM_DATA_ID_POSTFIX, + SentinelConStants.GROUP_ID, converter.convert(rules)); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-sentinel/src/main/resources/application.yml b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/resources/application.yml new file mode 100644 index 0000000..190bcb2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-sentinel/src/main/resources/application.yml @@ -0,0 +1,38 @@ +server: + port: 9000 + servlet: + session: + cookie: + name: sentinel_dashboard_cookie + encoding: + charset: UTF-8 + enabled: true + force: true +spring: + mvc: + #Spring Boot 2.6+\u540E\u6620\u5C04\u5339\u914D\u7684\u9ED8\u8BA4\u7B56\u7565\u5DF2\u4ECEAntPathMatcher\u66F4\u6539\u4E3APathPatternParser,\u9700\u8981\u624B\u52A8\u6307\u5B9A\u4E3Aant-path-matcher + pathmatch: + matching-strategy: ant_path_matcher +#auth settings +auth: + filter: + exclude-url-suffixes: htm,html,js,css,map,ico,ttf,woff,png + exclude-urls: /,/auth/login,/auth/logout,/registry/machine,/version +logging: + level: + org: + springframework: + web: INFO + pattern: + file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n' + file: + name: ${user.home}/logs/csp/sentinel-dashboard.log +nacos: + server: + ip: @config.server-addr@ +sentinel: + dashboard: + version: 1.8.2 + auth: + username: sentinel + password: sentinel \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/pom.xml b/test-server-cloud/test-visual/test-cloud-test/pom.xml new file mode 100644 index 0000000..82d48eb --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/pom.xml @@ -0,0 +1,29 @@ + + + + test-visual + com.ghb + 3.9.2 + + + 4.0.0 + pom + test-cloud-test + + + + com.ghb + test-base-core + + + + + test-cloud-test-shardingsphere + test-cloud-test-more + test-cloud-test-rabbitmq + test-cloud-test-seata + test-cloud-test-rocketmq + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/pom.xml new file mode 100644 index 0000000..7f50a30 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/pom.xml @@ -0,0 +1,44 @@ + + + + test-cloud-test + com.ghb + 3.9.2 + + 4.0.0 + 公共测试模块 + test-cloud-test-more + + + + + org.jeecgframework.boot3 + jeecg-boot-starter-cloud + + + + org.jeecgframework.boot3 + test-system-cloud-api + + + + + + org.jeecgframework.boot3 + jeecg-boot-starter-job + + + + org.jeecgframework.boot3 + jeecg-boot-starter-rabbitmq + + + + org.jeecgframework.boot3 + jeecg-boot-starter-lock + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/constant/CloudConstant.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/constant/CloudConstant.java new file mode 100644 index 0000000..ed0af04 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/constant/CloudConstant.java @@ -0,0 +1,26 @@ +package com.ghb.base.modules.test.constant; + +/** + * 微服务单元测试常量定义 + * @author: zyf + * @date: 2022/04/21 + */ +public interface CloudConstant { + + /** + * MQ测试队列名字 + */ + public final static String MQ_Ghb_PLACE_ORDER = "Ghb_place_order"; + + /** + * MQ测试消息总线 + */ + public final static String MQ_DEMO_BUS_EVENT = "demoBusEvent"; + + /** + * 分布式锁lock key + */ + public final static String REDISSON_DEMO_LOCK_KEY1 = "demoLockKey1"; + public final static String REDISSON_DEMO_LOCK_KEY2 = "demoLockKey2"; + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/client/GhbTestClient.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/client/GhbTestClient.java new file mode 100644 index 0000000..836a2b8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/client/GhbTestClient.java @@ -0,0 +1,30 @@ +package com.ghb.base.modules.test.feign.client; + +import com.ghb.base.common.api.vo.Result; + +import com.ghb.base.common.constant.ServiceNameConstants; +import org.jeecg.config.FeignConfig; +import com.ghb.base.modules.test.constant.CloudConstant; +import com.ghb.base.modules.test.feign.factory.GhbTestClientFactory; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 常规feign接口定义 + * @author: zyf + * @date: 2022/04/21 + */ +@FeignClient(value = ServiceNameConstants.SERVICE_DEMO, configuration = FeignConfig.class,fallbackFactory = GhbTestClientFactory.class) +@Component +public interface GhbTestClient { + + /** + * feign测试方法 + * @param name + * @return + */ + @GetMapping(value = "/test/getMessage") + String getMessage(@RequestParam(value = "name",required = false) String name); +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/client/GhbTestClientDyn.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/client/GhbTestClientDyn.java new file mode 100644 index 0000000..d1c49b9 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/client/GhbTestClientDyn.java @@ -0,0 +1,15 @@ +//package com.ghb.base.modules.test.feign.client; +// +//import com.ghb.base.common.api.vo.Result; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.PostMapping; +//import org.springframework.web.bind.annotation.RequestParam; +// +///** +// * 动态feign接口定义 +// */ +//public interface GhbTestClientDyn { +// +// @GetMapping(value = "/test/getMessage") +// Result getMessage(@RequestParam(value = "name",required = false) String name); +//} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/controller/GhbTestFeignController.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/controller/GhbTestFeignController.java new file mode 100644 index 0000000..ffd94cc --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/controller/GhbTestFeignController.java @@ -0,0 +1,78 @@ +package com.ghb.base.modules.test.feign.controller; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.test.feign.client.GhbTestClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.alibaba.csp.sentinel.annotation.SentinelResource; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; + +/** + * 微服务单元测试 + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@RestController +@RequestMapping("/sys/test") +@Tag(name = "【微服务】单元测试") +public class GhbTestFeignController { + + @Autowired + private GhbTestClient GhbTestClient; + + /** + * 熔断: fallbackFactory优先于 @SentinelResource + * + * @param name + * @return + */ + @GetMapping("/getMessage") + @Operation(summary = "测试feign调用demo服务1") + @SentinelResource(value = "test_more_getMessage", fallback = "getDefaultUser") + public Result getMessage(@RequestParam(value = "name", required = false) String name) { + log.info("---------Feign fallbackFactory优先级高于@SentinelResource-----------------"); + String resultMsg = GhbTestClient.getMessage(" I am Ghb-system 服务节点,呼叫 Ghb-demo!"); + return Result.OK(null, resultMsg); + } + + /** + * 测试方法:关闭demo服务,访问请求 http://127.0.0.1:9999/sys/test/getMessage + * + * @param name + * @return + */ + @GetMapping("/getMessage2") + @Operation(summary = "测试feign调用demo服务2") + public Result getMessage2(@RequestParam(value = "name", required = false) String name) { + log.info("---------测试 Feign fallbackFactory-----------------"); + String resultMsg = GhbTestClient.getMessage(" I am Ghb-system 服务节点,呼叫 Ghb-demo!"); + return Result.OK(null, resultMsg); + } + + + @GetMapping("/fallback") + @Operation(summary = "测试熔断") + @SentinelResource(value = "test_more_fallback", fallback = "getDefaultUser") + public Result test(@RequestParam(value = "name", required = false) String name) { + if (StringUtils.isEmpty(name)) { + throw new IllegalArgumentException("name param is empty"); + } + return Result.OK(); + } + + /** + * 熔断,默认回调函数 + * + * @param name + * @return + */ + public Result getDefaultUser(String name) { + log.info("熔断,默认回调函数"); + return Result.error(null, "访问超时, 自定义 @SentinelResource Fallback"); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/factory/GhbTestClientFactory.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/factory/GhbTestClientFactory.java new file mode 100644 index 0000000..2c00994 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/factory/GhbTestClientFactory.java @@ -0,0 +1,23 @@ +package com.ghb.base.modules.test.feign.factory; + + + + +import org.springframework.cloud.openfeign.FallbackFactory; +import com.ghb.base.modules.test.feign.client.GhbTestClient; +import com.ghb.base.modules.test.feign.fallback.GhbTestFallback; +import org.springframework.stereotype.Component; + +/** + * @author qinfeng + */ +@Component +public class GhbTestClientFactory implements FallbackFactory { + + @Override + public GhbTestClient create(Throwable throwable) { + GhbTestFallback fallback = new GhbTestFallback(); + fallback.setCause(throwable); + return fallback; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/fallback/GhbTestFallback.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/fallback/GhbTestFallback.java new file mode 100644 index 0000000..462aadd --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/feign/fallback/GhbTestFallback.java @@ -0,0 +1,25 @@ +package com.ghb.base.modules.test.feign.fallback; + +import com.ghb.base.common.api.vo.Result; + +import lombok.Setter; +import com.ghb.base.modules.test.feign.client.GhbTestClient; + + +/** +* 接口fallback实现 +* +* @author: scott +* @date: 2022/4/11 19:41 +*/ +public class GhbTestFallback implements GhbTestClient { + + @Setter + private Throwable cause; + + + @Override + public String getMessage(String name) { + return "访问超时, 自定义FallbackFactory"; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/lock/DemoLockTest.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/lock/DemoLockTest.java new file mode 100644 index 0000000..06d1815 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/lock/DemoLockTest.java @@ -0,0 +1,69 @@ +package com.ghb.base.modules.test.lock; + +import lombok.extern.slf4j.Slf4j; +import org.jeecg.boot.starter.lock.annotation.JLock; +import org.jeecg.boot.starter.lock.client.RedissonLockClient; +import com.ghb.base.modules.test.constant.CloudConstant; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 分布式锁测试demo + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@Component +public class DemoLockTest { + @Autowired + RedissonLockClient redissonLock; +// @Autowired +// RabbitMqClient rabbitMqClient; + + /** + * 测试方法: + * @Scheduled(cron = "0/5 * * * * ?") 表示每5秒执行一次 + * @JLock(lockKey = CloudConstant.REDISSON_DEMO_LOCK_KEY1)分布式锁,10秒钟才释放 + * 结果:每10秒钟输出一次 “执行 分布式锁 业务逻辑1” 就说明锁成功了 + * + * 测试分布式锁【注解方式】 + */ + @Scheduled(cron = "0/5 * * * * ?") + @JLock(lockKey = CloudConstant.REDISSON_DEMO_LOCK_KEY1) + public void execute() throws InterruptedException { + log.info("执行execute任务开始,休眠十秒开始,当前系统时间戳(秒):"+ System.currentTimeMillis()/1000); + Thread.sleep(10000); + log.info("========执行 分布式锁 业务逻辑1============="); +// Map map = new BaseMap(); +// map.put("orderId", "BJ0001"); +// rabbitMqClient.sendMessage(CloudConstant.MQ_Ghb_PLACE_ORDER, map); +// //延迟10秒发送 +// map.put("orderId", "NJ0002"); +// rabbitMqClient.sendMessage(CloudConstant.MQ_Ghb_PLACE_ORDER, map, 10000); + + log.info("execute任务结束,休眠十秒完成,当前系统时间戳(秒):"+ System.currentTimeMillis()/1000); + } + + + /** + * 测试分布式锁【编码方式】 + * @Scheduled(cron = "0/5 * * * * ?") + */ + public void execute2() throws InterruptedException { + int expireSeconds=6000; + if (redissonLock.tryLock(CloudConstant.REDISSON_DEMO_LOCK_KEY2, -1, expireSeconds)) { + log.info("执行任务execute2开始,休眠十秒"); + Thread.sleep(10000); + log.info("=============业务逻辑2==================="); + log.info("定时execute2结束,休眠十秒"); + + redissonLock.unlock(CloudConstant.REDISSON_DEMO_LOCK_KEY2); + } else { + log.info("execute2获取锁失败"); + } + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/xxljob/DemoJobHandler.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/xxljob/DemoJobHandler.java new file mode 100644 index 0000000..a01f271 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/xxljob/DemoJobHandler.java @@ -0,0 +1,235 @@ + +package com.ghb.base.modules.test.xxljob; + + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import com.xxl.job.core.context.XxlJobHelper; + +/** + * xxl-job定时任务测试 + * @author: zyf + * @date: 2022/04/21 + */ +@Component +@Slf4j +public class DemoJobHandler { + + + /** + * 简单任务 + * + * @param params + * @return + */ + @XxlJob(value = "demoJob") + public ReturnT demoJobHandler(String params) { + log.info("我是 Ghb-system 服务里的定时任务 demoJob,我执行了..............................."); + return ReturnT.SUCCESS; + } + + /** + * 2、分片广播任务 + */ + @XxlJob("shardingJobHandler") + public ReturnT shardingJobHandler(String param) throws Exception { + + // 获取分片序号和总分片数 + int shardIndex = XxlJobHelper.getShardIndex(); + int shardTotal = XxlJobHelper.getShardTotal(); + log.info("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal); + + // 业务逻辑 + for (int i = 0; i < shardTotal; i++) { + if (i == shardIndex) { + log.info("第 {} 片, 命中分片开始处理", i); + } else { + log.info("第 {} 片, 忽略", i); + } + } + + return ReturnT.SUCCESS; + } + + + /** + * 3、命令行任务 + * + * 输入参数:ipconfig /all + */ + @XxlJob("commandJobHandler") + public ReturnT commandJobHandler(String param) throws Exception { + String command = param; + int exitValue = -1; + + BufferedReader bufferedReader = null; + try { + // command process + Process process = Runtime.getRuntime().exec(command); + BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream()); + bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream)); + + // command log + String line; + while ((line = bufferedReader.readLine()) != null) { + log.info(line); + } + + // command exit + process.waitFor(); + exitValue = process.exitValue(); + } catch (Exception e) { + log.info(e.getMessage(),e); + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + } + + if (exitValue == 0) { + return ReturnT.SUCCESS; + } else { + return new ReturnT(ReturnT.FAIL_CODE, "command exit value(" + exitValue + ") is failed"); + } + } + + + /** + * 4、跨平台Http任务 + * + * 输入参数: + * url: https://www.baidu.com + * method: get + * data: content + */ + @XxlJob("httpJobHandler") + public ReturnT httpJobHandler(String param) throws Exception { + String[] methodArray=new String[]{"GET","POST"}; + int okState=200; + // param parse + if (param == null || param.trim().length() == 0) { + log.info("param[" + param + "] invalid."); + return ReturnT.FAIL; + } + String[] httpParams = param.split("\n"); + String url = null; + String method = null; + String data = null; + for (String httpParam : httpParams) { + if (httpParam.startsWith("url:")) { + url = httpParam.substring(httpParam.indexOf("url:") + 4).trim(); + } + if (httpParam.startsWith("method:")) { + method = httpParam.substring(httpParam.indexOf("method:") + 7).trim().toUpperCase(); + } + if (httpParam.startsWith("data:")) { + data = httpParam.substring(httpParam.indexOf("data:") + 5).trim(); + } + } + + // param valid + if (url == null || url.trim().length() == 0) { + log.info("url[" + url + "] invalid."); + return ReturnT.FAIL; + } + if (method == null || !Arrays.asList(methodArray).contains(method)) { + log.info("method[" + method + "] invalid."); + return ReturnT.FAIL; + } + + // request + HttpURLConnection connection = null; + BufferedReader bufferedReader = null; + try { + // connection + URL realUrl = new URL(url); + connection = (HttpURLConnection) realUrl.openConnection(); + + // connection setting + connection.setRequestMethod(method); + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setUseCaches(false); + connection.setReadTimeout(5 * 1000); + connection.setConnectTimeout(3 * 1000); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); + connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); + + // do connection + connection.connect(); + + // data + if (data != null && data.trim().length() > 0) { + DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream()); + dataOutputStream.write(data.getBytes("UTF-8")); + dataOutputStream.flush(); + dataOutputStream.close(); + } + + // valid StatusCode + int statusCode = connection.getResponseCode(); + if (statusCode != okState) { + throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid."); + } + + // result + bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + StringBuilder result = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + String responseMsg = result.toString(); + + log.info(responseMsg); + return ReturnT.SUCCESS; + } catch (Exception e) { + log.info(e.getMessage(),e); + return ReturnT.FAIL; + } finally { + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + if (connection != null) { + connection.disconnect(); + } + } catch (Exception e2) { + log.info(e2.getMessage(),e2); + } + } + + } + + + /** + * 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑; + */ + @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy") + public ReturnT demoJobHandler2(String param) throws Exception { + log.info("XXL-JOB, Hello World."); + return ReturnT.SUCCESS; + } + + public void init() { + log.info("init"); + } + + public void destroy() { + log.info("destory"); + } + +} + diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/xxljob/XxclJobTest.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/xxljob/XxclJobTest.java new file mode 100644 index 0000000..66bc4b1 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-more/src/main/java/com/ghb/base/modules/test/xxljob/XxclJobTest.java @@ -0,0 +1,41 @@ + +package com.ghb.base.modules.test.xxljob; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * xxl-job定时任务测试 + * @author: zyf + * @date: 2022/04/21 + */ +@Component +@Slf4j +public class XxclJobTest { + + + /** + * 简单任务 + * + * @param params + * @return + */ + + @XxlJob(value = "xxclJobTest") + public ReturnT demoJobHandler(String params) { + log.info("我是 Ghb-system 服务里的定时任务 xxclJobTest , 我执行了..............................."); + return ReturnT.SUCCESS; + } + + public void init() { + log.info("init"); + } + + public void destroy() { + log.info("destory"); + } + +} + diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/pom.xml new file mode 100644 index 0000000..9a85f31 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/pom.xml @@ -0,0 +1,22 @@ + + + + test-cloud-test + com.ghb + 3.9.2 + + 4.0.0 + 消息队列测试模块 + test-cloud-test-rabbitmq + + + + + org.jeecgframework.boot3 + jeecg-boot-starter-rabbitmq + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/constant/CloudConstant.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/constant/CloudConstant.java new file mode 100644 index 0000000..fa285d5 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/constant/CloudConstant.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.test.rabbitmq.constant; + +/** + * 微服务单元测试常量定义 + * @author: zyf + * @date: 2022/04/21 + */ +public interface CloudConstant { + + + /** + * MQ测试队列名字 + */ + public final static String MQ_Ghb_PLACE_ORDER = "Ghb_place_order"; + public final static String MQ_Ghb_PLACE_ORDER_TIME = "Ghb_place_order_time"; + + /** + * MQ测试消息总线 + */ + public final static String MQ_DEMO_BUS_EVENT = "demoBusEvent"; + + /** + * 分布式锁lock key + */ + public final static String REDISSON_DEMO_LOCK_KEY1 = "demoLockKey1"; + public final static String REDISSON_DEMO_LOCK_KEY2 = "demoLockKey2"; + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/controller/GhbMqTestController.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/controller/GhbMqTestController.java new file mode 100644 index 0000000..165a135 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/controller/GhbMqTestController.java @@ -0,0 +1,64 @@ +package com.ghb.base.modules.test.rabbitmq.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; + +import org.jeecg.boot.starter.rabbitmq.client.RabbitMqClient; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rabbitmq.constant.CloudConstant; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import cn.hutool.core.util.RandomUtil; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; + + +/** + * RabbitMqClient发送消息 + * @author: zyf + * @date: 2022/04/21 + */ +@RestController +@RequestMapping("/sys/test") +@Tag(name = "【微服务】MQ单元测试") +public class GhbMqTestController { + + @Autowired + private RabbitMqClient rabbitMqClient; + + + /** + * 测试方法:快速点击发送MQ消息 + * 观察三个接受者如何分配处理消息:HelloReceiver1、HelloReceiver2、HelloReceiver3,会均衡分配 + * + * @param req + * @return + */ + @GetMapping(value = "/rabbitmq") + @Operation(summary = "测试rabbitmq") + public Result rabbitMqClientTest(HttpServletRequest req) { + //rabbitmq消息队列测试 + BaseMap map = new BaseMap(); + map.put("orderId", RandomUtil.randomNumbers(10)); + rabbitMqClient.sendMessage(CloudConstant.MQ_Ghb_PLACE_ORDER, map); + rabbitMqClient.sendMessage(CloudConstant.MQ_Ghb_PLACE_ORDER_TIME, map,10); + return Result.OK("MQ发送消息成功"); + } + + @GetMapping(value = "/rabbitmq2") + @Operation(summary = "rabbitmq消息总线测试") + public Result rabbitmq2(HttpServletRequest req) { + + //rabbitmq消息总线测试 + BaseMap params = new BaseMap(); + params.put("orderId", "123456"); + rabbitMqClient.publishEvent(CloudConstant.MQ_DEMO_BUS_EVENT, params); + return Result.OK("MQ发送消息成功"); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/event/DemoBusEvent.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/event/DemoBusEvent.java new file mode 100644 index 0000000..ea7e2bd --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/event/DemoBusEvent.java @@ -0,0 +1,30 @@ +package com.ghb.base.modules.test.rabbitmq.event; + +import org.jeecg.boot.starter.rabbitmq.event.EventObj; +import org.jeecg.boot.starter.rabbitmq.event.JeecgBusEventHandler; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rabbitmq.constant.CloudConstant; +import org.springframework.stereotype.Component; + +import cn.hutool.core.util.ObjectUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * 消息处理器【发布订阅】 + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@Component(CloudConstant.MQ_DEMO_BUS_EVENT) +public class DemoBusEvent implements JeecgBusEventHandler{ + + + @Override + public void onMessage(EventObj obj) { + if (ObjectUtil.isNotEmpty(obj)) { + BaseMap baseMap = obj.getBaseMap(); + String orderId = baseMap.get("orderId"); + log.info("业务处理----订单ID:" + orderId); + } + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver1.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver1.java new file mode 100644 index 0000000..9d50246 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver1.java @@ -0,0 +1,62 @@ +package com.ghb.base.modules.test.rabbitmq.listener; + +import org.jeecg.boot.starter.rabbitmq.core.BaseRabbiMqHandler; +import org.jeecg.boot.starter.rabbitmq.listenter.MqListener; +import org.jeecg.common.annotation.RabbitComponent; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rabbitmq.constant.CloudConstant; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.web.client.RestTemplate; + +import com.rabbitmq.client.Channel; + +import lombok.extern.slf4j.Slf4j; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * + * RabbitMq接受者1 + * (@RabbitListener声明类上,一个类只能监听一个队列) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@RabbitListener(queues = CloudConstant.MQ_Ghb_PLACE_ORDER) +@RabbitComponent(value = "helloReceiver1") +public class HelloReceiver1 extends BaseRabbiMqHandler { + + @Autowired + private RestTemplate restTemplate; + + @RabbitHandler + public void onMessage(BaseMap baseMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { + super.onMessage(baseMap, deliveryTag, channel, new MqListener() { + @Override + public void handler(BaseMap map, Channel channel) { + //业务处理 + String orderId = map.get("orderId").toString(); + log.info("【我是处理人1】 MQ Receiver1,orderId : " + orderId); + // GhbTestClient.getMessage("Ghb"); + try{ +// HttpHeaders requestHeaders = new HttpHeaders(); +// requestHeaders.add("X-Access-Token", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzExOTcyOTEsInVzZXJuYW1lIjoiYWRtaW4ifQ.N8mJvwzb4G0i3vYF9A2Bmf5cDKb1LDnOp1RwtpYEu1E"); +// requestHeaders.add("content-type", MediaType.APPLICATION_JSON_UTF8.toString()); +// MultiValueMap requestBody = new LinkedMultiValueMap<>(); +// requestBody.add("name", "test"); +// HttpEntity< MultiValueMap > requestEntity = new HttpEntity(requestBody, requestHeaders); +// //post +// ResponseEntity responseEntity = restTemplate.postForEntity("http://localhost:7002/test/getMessage", requestEntity, String.class); +// System.out.println(" responseEntity :"+responseEntity.getBody()); + }catch (Exception e){ + e.printStackTrace(); + } + + } + }); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver2.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver2.java new file mode 100644 index 0000000..97b4ebe --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver2.java @@ -0,0 +1,40 @@ +package com.ghb.base.modules.test.rabbitmq.listener;//package com.ghb.base.modules.cloud.rabbitmq; + +import com.rabbitmq.client.Channel; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.boot.starter.rabbitmq.core.BaseRabbiMqHandler; +import org.jeecg.boot.starter.rabbitmq.listenter.MqListener; +import org.jeecg.common.annotation.RabbitComponent; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rabbitmq.constant.CloudConstant; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.messaging.handler.annotation.Header; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * + * RabbitMq接受者2 + * (@RabbitListener声明类上,一个类只能监听一个队列) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@RabbitListener(queues = CloudConstant.MQ_Ghb_PLACE_ORDER) +@RabbitComponent(value = "helloReceiver2") +public class HelloReceiver2 extends BaseRabbiMqHandler { + + @RabbitHandler + public void onMessage(BaseMap baseMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { + super.onMessage(baseMap, deliveryTag, channel, new MqListener() { + @Override + public void handler(BaseMap map, Channel channel) { + //业务处理 + String orderId = map.get("orderId").toString(); + log.info("【我是处理人2】 MQ Receiver2,orderId : " + orderId); + } + }); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver3.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver3.java new file mode 100644 index 0000000..df11d02 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloReceiver3.java @@ -0,0 +1,38 @@ +package com.ghb.base.modules.test.rabbitmq.listener;//package com.ghb.base.modules.cloud.rabbitmq; + +import com.rabbitmq.client.Channel; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.boot.starter.rabbitmq.core.BaseRabbiMqHandler; +import org.jeecg.boot.starter.rabbitmq.listenter.MqListener; +import org.jeecg.common.annotation.RabbitComponent; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rabbitmq.constant.CloudConstant; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.messaging.handler.annotation.Header; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * + * RabbitMq接受者3【我是处理人3】 + * (@RabbitListener声明类方法上,一个类可以多监听多个队列) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@RabbitComponent(value = "helloReceiver3") +public class HelloReceiver3 extends BaseRabbiMqHandler { + + @RabbitListener(queues = CloudConstant.MQ_Ghb_PLACE_ORDER) + public void onMessage(BaseMap baseMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { + super.onMessage(baseMap, deliveryTag, channel, new MqListener() { + @Override + public void handler(BaseMap map, Channel channel) { + //业务处理 + String orderId = map.get("orderId").toString(); + log.info("【我是处理人3】MQ Receiver3,orderId : " + orderId); + } + }); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloTimeReceiver.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloTimeReceiver.java new file mode 100644 index 0000000..674ec90 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rabbitmq/src/main/java/com/ghb/base/modules/test/rabbitmq/listener/HelloTimeReceiver.java @@ -0,0 +1,39 @@ +package com.ghb.base.modules.test.rabbitmq.listener; + +import org.jeecg.boot.starter.rabbitmq.core.BaseRabbiMqHandler; +import org.jeecg.boot.starter.rabbitmq.listenter.MqListener; +import org.jeecg.common.annotation.RabbitComponent; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rabbitmq.constant.CloudConstant; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.messaging.handler.annotation.Header; + +import com.rabbitmq.client.Channel; + +import lombok.extern.slf4j.Slf4j; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@RabbitListener(queues = CloudConstant.MQ_Ghb_PLACE_ORDER_TIME) +@RabbitComponent(value = "helloTimeReceiver") +public class HelloTimeReceiver extends BaseRabbiMqHandler { + + @RabbitHandler + public void onMessage(BaseMap baseMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { + super.onMessage(baseMap, deliveryTag, channel, new MqListener() { + @Override + public void handler(BaseMap map, Channel channel) { + //业务处理 + String orderId = map.get("orderId").toString(); + log.info("Time Receiver1,orderId : " + orderId); + } + }); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/pom.xml new file mode 100644 index 0000000..ca7711a --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/pom.xml @@ -0,0 +1,22 @@ + + + + com.ghb + test-cloud-test + 3.9.2 + + 4.0.0 + 消息队列测试模块 + test-cloud-test-rocketmq + + + + + org.jeecgframework.boot3 + jeecg-boot-starter-rocketmq + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/constant/CloudConstant.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/constant/CloudConstant.java new file mode 100644 index 0000000..a5a5e2b --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/constant/CloudConstant.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.test.rocketmq.constant; + +/** + * 微服务单元测试常量定义 + * @author: zyf + * @date: 2022/04/21 + */ +public interface CloudConstant { + + + /** + * MQ测试队列名字 + */ + public final static String MQ_Ghb_PLACE_ORDER = "Ghb_place_order"; + public final static String MQ_Ghb_PLACE_ORDER_TIME = "Ghb_place_order_time"; + + /** + * MQ测试消息总线 + */ + public final static String MQ_DEMO_BUS_EVENT = "demoBusEvent"; + + /** + * 分布式锁lock key + */ + public final static String REDISSON_DEMO_LOCK_KEY1 = "demoLockKey1"; + public final static String REDISSON_DEMO_LOCK_KEY2 = "demoLockKey2"; + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/controller/GhbMqTestController.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/controller/GhbMqTestController.java new file mode 100644 index 0000000..25e1821 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/controller/GhbMqTestController.java @@ -0,0 +1,61 @@ +package com.ghb.base.modules.test.rocketmq.controller; + + +import cn.hutool.core.util.RandomUtil; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.jeecg.boot.starter.rabbitmq.client.RabbitMqClient; +import com.ghb.base.common.api.vo.Result; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rocketmq.constant.CloudConstant; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; + + +/** + * RocketMqClient发送消息 + * @author: zyf + * @date: 2022/04/21 + */ +@RestController +@RequestMapping("/sys/test") +@Tag(name = "【微服务】MQ单元测试") +public class GhbMqTestController { + + @Autowired + private RabbitMqClient rabbitMqClient; + + + /** + * 测试方法:快速点击发送MQ消息 + * 观察三个接受者如何分配处理消息:HelloReceiver1、HelloReceiver2、HelloReceiver3,会均衡分配 + * + * @param req + * @return + */ + @GetMapping(value = "/rocketmq") + @Operation(summary = "测试rocketmq") + public Result rabbitMqClientTest(HttpServletRequest req) { + //rabbitmq消息队列测试 + BaseMap map = new BaseMap(); + map.put("orderId", RandomUtil.randomNumbers(10)); + rabbitMqClient.sendMessage(CloudConstant.MQ_Ghb_PLACE_ORDER, map); + rabbitMqClient.sendMessage(CloudConstant.MQ_Ghb_PLACE_ORDER_TIME, map,2); + return Result.OK("MQ发送消息成功"); + } + + @GetMapping(value = "/rocketmq2") + @Operation(summary = "rocketmq消息总线测试") + public Result rabbitmq2(HttpServletRequest req) { + + //rabbitmq消息总线测试 + BaseMap params = new BaseMap(); + params.put("orderId", "123456"); + rabbitMqClient.publishEvent(CloudConstant.MQ_DEMO_BUS_EVENT, params); + return Result.OK("MQ发送消息成功"); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/event/DemoBusEvent.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/event/DemoBusEvent.java new file mode 100644 index 0000000..4a31ec3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/event/DemoBusEvent.java @@ -0,0 +1,29 @@ +package com.ghb.base.modules.test.rocketmq.event; + +import cn.hutool.core.util.ObjectUtil; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.boot.starter.rabbitmq.event.EventObj; +import org.jeecg.boot.starter.rabbitmq.event.JeecgBusEventHandler; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rocketmq.constant.CloudConstant; +import org.springframework.stereotype.Component; + +/** + * 消息处理器【发布订阅】 + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@Component(CloudConstant.MQ_DEMO_BUS_EVENT) +public class DemoBusEvent implements JeecgBusEventHandler { + + + @Override + public void onMessage(EventObj obj) { + if (ObjectUtil.isNotEmpty(obj)) { + BaseMap baseMap = obj.getBaseMap(); + String orderId = baseMap.get("orderId"); + log.info("业务处理----订单ID:" + orderId); + } + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver1.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver1.java new file mode 100644 index 0000000..f4640ff --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver1.java @@ -0,0 +1,27 @@ +package com.ghb.base.modules.test.rocketmq.listener; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rocketmq.constant.CloudConstant; +import org.springframework.stereotype.Component; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * + * RabbitMq接受者1 + * (@RabbitListener声明类上,一个类只能监听一个队列) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@Component +@RocketMQMessageListener(topic = CloudConstant.MQ_Ghb_PLACE_ORDER, consumerGroup = "helloReceiver1") +public class HelloReceiver1 implements RocketMQListener { + + public void onMessage(BaseMap baseMap) { + log.info("helloReceiver1接收消息:" + baseMap); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver2.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver2.java new file mode 100644 index 0000000..3eb65f2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver2.java @@ -0,0 +1,27 @@ +package com.ghb.base.modules.test.rocketmq.listener;//package com.ghb.base.modules.cloud.rabbitmq; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rocketmq.constant.CloudConstant; +import org.springframework.stereotype.Component; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * + * RabbitMq接受者2 + * (@RabbitListener声明类上,一个类只能监听一个队列) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@Component +@RocketMQMessageListener(topic = CloudConstant.MQ_Ghb_PLACE_ORDER, consumerGroup = "helloReceiver2") +public class HelloReceiver2 implements RocketMQListener { + + public void onMessage(BaseMap baseMap) { + log.info("helloReceiver2接收消息:" + baseMap); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver3.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver3.java new file mode 100644 index 0000000..4f1b884 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloReceiver3.java @@ -0,0 +1,27 @@ +package com.ghb.base.modules.test.rocketmq.listener;//package com.ghb.base.modules.cloud.rabbitmq; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rocketmq.constant.CloudConstant; +import org.springframework.stereotype.Component; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * + * RabbitMq接受者3【我是处理人3】 + * (@RabbitListener声明类方法上,一个类可以多监听多个队列) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@Component +@RocketMQMessageListener(topic = CloudConstant.MQ_Ghb_PLACE_ORDER, consumerGroup = "helloReceiver3") +public class HelloReceiver3 implements RocketMQListener { + + public void onMessage(BaseMap baseMap) { + log.info("helloReceiver3接收消息:" + baseMap); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloTimeReceiver.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloTimeReceiver.java new file mode 100644 index 0000000..61e988f --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-rocketmq/src/main/java/com/ghb/base/modules/test/rocketmq/listener/HelloTimeReceiver.java @@ -0,0 +1,24 @@ +package com.ghb.base.modules.test.rocketmq.listener; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.jeecg.common.base.BaseMap; +import com.ghb.base.modules.test.rocketmq.constant.CloudConstant; +import org.springframework.stereotype.Component; + +/** + * 定义接收者(可以定义N个接受者,消息会均匀的发送到N个接收者中) + * @author: zyf + * @date: 2022/04/21 + */ +@Slf4j +@Component +@RocketMQMessageListener(topic = CloudConstant.MQ_Ghb_PLACE_ORDER_TIME, consumerGroup = "helloTimeReceiver") +public class HelloTimeReceiver implements RocketMQListener { + + public void onMessage(BaseMap baseMap) { + log.info("helloTimeReceiver接收消息:" + baseMap); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/db/seata.sql b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/db/seata.sql new file mode 100644 index 0000000..eab85b2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/db/seata.sql @@ -0,0 +1,77 @@ +-- -------------------------------- The script used when storeMode is 'db' -------------------------------- +-- the table to store GlobalSession data +DROP TABLE IF EXISTS `global_table`; +CREATE TABLE IF NOT EXISTS `global_table` +( + `xid` VARCHAR(128) NOT NULL, + `transaction_id` BIGINT, + `status` TINYINT NOT NULL, + `application_id` VARCHAR(32), + `transaction_service_group` VARCHAR(32), + `transaction_name` VARCHAR(128), + `timeout` INT, + `begin_time` BIGINT, + `application_data` VARCHAR(2000), + `gmt_create` DATETIME, + `gmt_modified` DATETIME, + PRIMARY KEY (`xid`), + KEY `idx_status_gmt_modified` (`status` , `gmt_modified`), + KEY `idx_transaction_id` (`transaction_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +-- the table to store BranchSession data +DROP TABLE IF EXISTS `branch_table`; +CREATE TABLE IF NOT EXISTS `branch_table` +( + `branch_id` BIGINT NOT NULL, + `xid` VARCHAR(128) NOT NULL, + `transaction_id` BIGINT, + `resource_group_id` VARCHAR(32), + `resource_id` VARCHAR(256), + `branch_type` VARCHAR(8), + `status` TINYINT, + `client_id` VARCHAR(64), + `application_data` VARCHAR(2000), + `gmt_create` DATETIME(6), + `gmt_modified` DATETIME(6), + PRIMARY KEY (`branch_id`), + KEY `idx_xid` (`xid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +-- the table to store lock data +DROP TABLE IF EXISTS `lock_table`; +CREATE TABLE IF NOT EXISTS `lock_table` +( + `row_key` VARCHAR(128) NOT NULL, + `xid` VARCHAR(128), + `transaction_id` BIGINT, + `branch_id` BIGINT NOT NULL, + `resource_id` VARCHAR(256), + `table_name` VARCHAR(32), + `pk` VARCHAR(36), + `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', + `gmt_create` DATETIME, + `gmt_modified` DATETIME, + PRIMARY KEY (`row_key`), + KEY `idx_status` (`status`), + KEY `idx_branch_id` (`branch_id`), + KEY `idx_xid` (`xid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +DROP TABLE IF EXISTS `distributed_lock`; +CREATE TABLE IF NOT EXISTS `distributed_lock` +( + `lock_key` CHAR(20) NOT NULL, + `lock_value` VARCHAR(20) NOT NULL, + `expire` BIGINT, + primary key (`lock_key`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); +INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); +INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); +INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0); \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/pom.xml new file mode 100644 index 0000000..776a6a0 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/pom.xml @@ -0,0 +1,30 @@ + + + + test-cloud-test + com.ghb + 3.9.2 + + 4.0.0 + test-cloud-test-seata + pom + + test-cloud-test-seata-account + test-cloud-test-seata-product + test-cloud-test-seata-order + + + + org.jeecgframework.boot3 + jeecg-boot-starter-cloud + ${jeecgboot.version} + + + org.jeecgframework.boot3 + jeecg-boot-starter-seata + ${jeecgboot.version} + + + diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/pom.xml new file mode 100644 index 0000000..68016cc --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/pom.xml @@ -0,0 +1,14 @@ + + + + test-cloud-test-seata + com.ghb + 3.9.2 + + 4.0.0 + 分布式事务测试模块 + test-cloud-test-seata-account + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/SeataAccountApplication.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/SeataAccountApplication.java new file mode 100644 index 0000000..40c13e1 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/SeataAccountApplication.java @@ -0,0 +1,17 @@ +package com.ghb.base; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 分布式事务-账户服务 + * @author zyf + */ +@SpringBootApplication +public class SeataAccountApplication { + + public static void main(String[] args) { + SpringApplication.run(SeataAccountApplication.class, args); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/controller/SeataAccountController.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/controller/SeataAccountController.java new file mode 100644 index 0000000..cba1dd7 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/controller/SeataAccountController.java @@ -0,0 +1,27 @@ +package com.ghb.base.modules.test.seata.account.controller; + +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.test.seata.account.service.SeataAccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; + +/** + * @author zyf + */ +@RestController +@RequestMapping("/test/seata/account") +public class SeataAccountController { + + @Autowired + private SeataAccountService accountService; + + @PostMapping("/reduceBalance") + public Result reduceBalance(Long userId, BigDecimal amount) { + return accountService.reduceBalance(userId, amount); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/entity/SeataAccount.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/entity/SeataAccount.java new file mode 100644 index 0000000..aebebde --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/entity/SeataAccount.java @@ -0,0 +1,31 @@ +package com.ghb.base.modules.test.seata.account.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @Description: 账户 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Data +@Builder +@TableName("account") +public class SeataAccount { + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 余额 + */ + private BigDecimal balance; + + private Date lastUpdateTime; +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/mapper/SeataAccountMapper.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/mapper/SeataAccountMapper.java new file mode 100644 index 0000000..49d27db --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/mapper/SeataAccountMapper.java @@ -0,0 +1,17 @@ +package com.ghb.base.modules.test.seata.account.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import com.ghb.base.modules.test.seata.account.entity.SeataAccount; + + +/** + * @Description: TODO + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Mapper +public interface SeataAccountMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/service/SeataAccountService.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/service/SeataAccountService.java new file mode 100644 index 0000000..d77da4c --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/service/SeataAccountService.java @@ -0,0 +1,20 @@ +package com.ghb.base.modules.test.seata.account.service; + +import com.ghb.base.common.api.vo.Result; + +import java.math.BigDecimal; + +/** + * @Description: 账户接口 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +public interface SeataAccountService { + /** + * 扣减金额 + * @param userId 用户 ID + * @param amount 扣减金额 + */ + Result reduceBalance(Long userId, BigDecimal amount); +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/service/impl/SeataAccountServiceImpl.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/service/impl/SeataAccountServiceImpl.java new file mode 100644 index 0000000..456b38e --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/java/com/ghb/base/modules/test/seata/account/service/impl/SeataAccountServiceImpl.java @@ -0,0 +1,58 @@ +package com.ghb.base.modules.test.seata.account.service.impl; + + +import com.baomidou.dynamic.datasource.annotation.DS; +import io.seata.core.context.RootContext; +import lombok.extern.slf4j.Slf4j; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.test.seata.account.entity.SeataAccount; +import com.ghb.base.modules.test.seata.account.mapper.SeataAccountMapper; +import com.ghb.base.modules.test.seata.account.service.SeataAccountService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import jakarta.annotation.Resource; +import java.math.BigDecimal; + +/** + * @Description: TODO + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Slf4j +@Service +public class SeataAccountServiceImpl implements SeataAccountService { + @Resource + private SeataAccountMapper accountMapper; + + /** + * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 + */ + @DS("account") + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class) + public Result reduceBalance(Long userId, BigDecimal amount) { + log.info("xid:"+ RootContext.getXID()); + log.info("=============ACCOUNT START================="); + SeataAccount account = accountMapper.selectById(userId); + Assert.notNull(account, "用户不存在"); + BigDecimal balance = account.getBalance(); + log.info("下单用户{}余额为 {},商品总价为{}", userId, balance, amount); + + if (balance.compareTo(amount)==-1) { + log.warn("用户 {} 余额不足,当前余额:{}", userId, balance); + return Result.error("余额不足"); + } + log.info("开始扣减用户 {} 余额", userId); + BigDecimal currentBalance = account.getBalance().subtract(amount); + account.setBalance(currentBalance); + accountMapper.updateById(account); + log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance); + log.info("=============ACCOUNT END================="); + return Result.OK(); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/resources/application.yml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/resources/application.yml new file mode 100644 index 0000000..d6a1a46 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/resources/application.yml @@ -0,0 +1,44 @@ +server: + port: 5002 +spring: + data: + redis: + ##redis 单机环境配置 + host: jeecg-boot-redis + port: 6379 + database: 0 + password: + ssl: + enabled: false + application: + name: seata-account + cloud: + nacos: + config: + import-check: + enabled: false + main: + allow-bean-definition-overriding: true + autoconfigure: + exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration + datasource: + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg_account?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + sql: + init: + schema-locations: classpath:sql/schema-account.sql +seata: + enable-auto-data-source-proxy: true + service: + grouplist: + default: 127.0.0.1:8091 + vgroup-mapping: + springboot-seata-group: default + # seata 事务组编号 用于TC集群名 + tx-service-group: springboot-seata-group + +# 无用配置,为了避免扫码全代码导致启动慢 +minidao: + base-package: com.ghb.base.modules.jmreport.* \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/resources/sql/schema-account.sql b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/resources/sql/schema-account.sql new file mode 100644 index 0000000..152963a --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-account/src/main/resources/sql/schema-account.sql @@ -0,0 +1,37 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for account +-- ---------------------------- +DROP TABLE IF EXISTS `account`; +CREATE TABLE `account` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `balance` decimal(10, 2) NULL DEFAULT NULL, + `last_update_time` timestamp NULL DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of account +-- ---------------------------- +INSERT INTO `account` VALUES (1, 50.00, '2022-03-16 17:02:53'); + +-- ---------------------------- +-- Table structure for undo_log +-- ---------------------------- +DROP TABLE IF EXISTS `undo_log`; +CREATE TABLE `undo_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `branch_id` bigint(20) NOT NULL, + `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `rollback_info` longblob NOT NULL, + `log_status` int(11) NOT NULL, + `log_created` datetime(0) NOT NULL, + `log_modified` datetime(0) NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/pom.xml new file mode 100644 index 0000000..5f184e4 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/pom.xml @@ -0,0 +1,14 @@ + + + + test-cloud-test-seata + com.ghb + 3.9.2 + + 4.0.0 + 分布式事务测试模块 + test-cloud-test-seata-order + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/SeataOrderApplication.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/SeataOrderApplication.java new file mode 100644 index 0000000..5ed09cf --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/SeataOrderApplication.java @@ -0,0 +1,18 @@ +package com.ghb.base; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; + +/** + * @author zyf + */ +@SpringBootApplication +@EnableFeignClients +public class SeataOrderApplication { + + public static void main(String[] args) { + SpringApplication.run(SeataOrderApplication.class, args); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/controller/SeataOrderController.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/controller/SeataOrderController.java new file mode 100644 index 0000000..a847aa1 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/controller/SeataOrderController.java @@ -0,0 +1,60 @@ +package com.ghb.base.modules.test.seata.order.controller; + +/** + * @Description: TODO + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; + +import com.ghb.base.modules.test.seata.order.dto.PlaceOrderRequest; +import com.ghb.base.modules.test.seata.order.service.SeataOrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test/seata/order") +@Tag(name = "seata测试") +public class SeataOrderController { + + @Autowired + private SeataOrderService orderService; + + /** + * 自由下单 + */ + @PostMapping("/placeOrder") + @Operation(summary = "自由下单") + public String placeOrder(@Validated @RequestBody PlaceOrderRequest request) { + orderService.placeOrder(request); + return "下单成功"; + } + + /** + * 测试商品库存不足-异常回滚 + */ + @PostMapping("/test1") + @Operation(summary = "测试商品库存不足") + public String test1() { + //商品单价10元,库存20个,用户余额50元,模拟一次性购买22个。 期望异常回滚 + orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 22)); + return "下单成功"; + } + + /** + * 测试用户账户余额不足-异常回滚 + */ + @PostMapping("/test2") + @Operation(summary = "测试用户账户余额不足") + public String test2() { + //商品单价10元,库存20个,用户余额50元,模拟一次性购买6个。 期望异常回滚 + orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 6)); + return "下单成功"; + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/PlaceOrderRequest.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/PlaceOrderRequest.java new file mode 100644 index 0000000..28f1816 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/PlaceOrderRequest.java @@ -0,0 +1,28 @@ +package com.ghb.base.modules.test.seata.order.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotNull; +/** + * @Description: 订单请求对象 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PlaceOrderRequest { + + @NotNull + private Long userId; + + @NotNull + private Long productId; + + @NotNull + private Integer count; +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/ReduceBalanceRequest.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/ReduceBalanceRequest.java new file mode 100644 index 0000000..3b6e440 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/ReduceBalanceRequest.java @@ -0,0 +1,21 @@ +package com.ghb.base.modules.test.seata.order.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Description: 余额请求对象 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ReduceBalanceRequest { + + private Long userId; + private Integer price; +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/ReduceStockRequest.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/ReduceStockRequest.java new file mode 100644 index 0000000..de0fe03 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/dto/ReduceStockRequest.java @@ -0,0 +1,21 @@ +package com.ghb.base.modules.test.seata.order.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +/** + * @Description: 库存请求对象 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ReduceStockRequest { + + private Long productId; + private Integer amount; +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/entity/SeataOrder.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/entity/SeataOrder.java new file mode 100644 index 0000000..65b46e8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/entity/SeataOrder.java @@ -0,0 +1,46 @@ +package com.ghb.base.modules.test.seata.order.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Builder; +import lombok.Data; +import com.ghb.base.modules.test.seata.order.enums.OrderStatus; + +import java.math.BigDecimal; + +/** + * @Description: 订单 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Builder +@Data +@TableName("p_order") +public class SeataOrder { + + @TableId(type = IdType.AUTO) + private Integer id; + + /** + * 用户ID + */ + private Long userId; + /** + * 商品ID + */ + private Long productId; + /** + * 订单状态 + */ + private OrderStatus status; + /** + * 数量 + */ + private Integer count; + /** + * 总金额 + */ + private BigDecimal totalPrice; +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/enums/OrderStatus.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/enums/OrderStatus.java new file mode 100644 index 0000000..4224cb2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/enums/OrderStatus.java @@ -0,0 +1,22 @@ +package com.ghb.base.modules.test.seata.order.enums; + +/** + * @Description: 订单状态 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +public enum OrderStatus { + /** + * INIT + */ + INIT, + /** + * SUCCESS + */ + SUCCESS, + /** + * FAIL + */ + FAIL +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/feign/AccountClient.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/feign/AccountClient.java new file mode 100644 index 0000000..9a3414e --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/feign/AccountClient.java @@ -0,0 +1,24 @@ +package com.ghb.base.modules.test.seata.order.feign; + +import com.ghb.base.common.api.vo.Result; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; + +/** + * @author zyf + */ +@FeignClient(value ="seata-account") +public interface AccountClient { + + /** + * 扣减余额 + * @param userId + * @param amount + * @return + */ + @PostMapping("/test/seata/account/reduceBalance") + Result reduceBalance(@RequestParam("userId") Long userId, @RequestParam("amount") BigDecimal amount); +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/feign/ProductClient.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/feign/ProductClient.java new file mode 100644 index 0000000..53b4942 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/feign/ProductClient.java @@ -0,0 +1,26 @@ +package com.ghb.base.modules.test.seata.order.feign; + +import com.ghb.base.common.api.vo.Result; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; + +/** + * 分布式事务产品feign客户端 + * @author: zyf + * @date: 2022/04/21 + */ +@FeignClient(value ="seata-product") +public interface ProductClient { + /** + * 扣减库存 + * + * @param productId + * @param count + * @return + */ + @PostMapping("/test/seata/product/reduceStock") + Result reduceStock(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/mapper/SeataOrderMapper.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/mapper/SeataOrderMapper.java new file mode 100644 index 0000000..f956a9a --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/mapper/SeataOrderMapper.java @@ -0,0 +1,17 @@ +package com.ghb.base.modules.test.seata.order.mapper; + +/** + * @Description: TODO + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import com.ghb.base.modules.test.seata.order.entity.SeataOrder; + +@Mapper +public interface SeataOrderMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/service/SeataOrderService.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/service/SeataOrderService.java new file mode 100644 index 0000000..2684e19 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/service/SeataOrderService.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.test.seata.order.service; + + +import com.ghb.base.modules.test.seata.order.dto.PlaceOrderRequest; + +/** + * @Description: 订单接口 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +public interface SeataOrderService { + /** + * 下单 + * + * @param placeOrderRequest 订单请求参数 + */ + void placeOrder(PlaceOrderRequest placeOrderRequest); +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/service/impl/SeataOrderServiceImpl.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/service/impl/SeataOrderServiceImpl.java new file mode 100644 index 0000000..a9b1929 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/java/com/ghb/base/modules/test/seata/order/service/impl/SeataOrderServiceImpl.java @@ -0,0 +1,87 @@ +package com.ghb.base.modules.test.seata.order.service.impl; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.dynamic.datasource.annotation.DS; + +import io.seata.core.context.RootContext; +import io.seata.spring.annotation.GlobalTransactional; +import lombok.extern.slf4j.Slf4j; +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.exception.GhbBootBizTipException; +import com.ghb.base.common.util.oConvertUtils; +import com.ghb.base.modules.test.seata.order.dto.PlaceOrderRequest; +import com.ghb.base.modules.test.seata.order.entity.SeataOrder; +import com.ghb.base.modules.test.seata.order.enums.OrderStatus; +import com.ghb.base.modules.test.seata.order.feign.AccountClient; +import com.ghb.base.modules.test.seata.order.feign.ProductClient; +import com.ghb.base.modules.test.seata.order.mapper.SeataOrderMapper; +import com.ghb.base.modules.test.seata.order.service.SeataOrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.math.BigDecimal; + +/** + * @Description: 订单服务类 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Slf4j +@Service +public class SeataOrderServiceImpl implements SeataOrderService { + + @Resource + private SeataOrderMapper orderMapper; + @Resource + private AccountClient accountClient; + @Resource + private ProductClient productClient; + + @DS("order") + @Override + @Transactional(rollbackFor = Exception.class) + @GlobalTransactional + public void placeOrder(PlaceOrderRequest request) { + log.info("xid:"+RootContext.getXID()); + log.info("=============ORDER START================="); + Long userId = request.getUserId(); + Long productId = request.getProductId(); + Integer count = request.getCount(); + log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, count); + + + SeataOrder order = SeataOrder.builder() + .userId(userId) + .productId(productId) + .status(OrderStatus.INIT) + .count(count) + .build(); + + orderMapper.insert(order); + log.info("订单一阶段生成,等待扣库存付款中"); + // 扣减库存并计算总价 + Result productRes = productClient.reduceStock(productId, count); + if (!productRes.isSuccess()) { + String message = productRes.getMessage(); + message = oConvertUtils.isEmpty(message) ? "操作失败" : message; + throw new GhbBootBizTipException(message); + } + BigDecimal amount = productRes.getResult(); + // 扣减余额 + Result accountRes = accountClient.reduceBalance(userId, amount); + // feign响应被二次封装,判断使主事务回滚 + if (!accountRes.isSuccess()) { + String message = accountRes.getMessage(); + message = oConvertUtils.isEmpty(message) ? "操作失败" : message; + throw new GhbBootBizTipException(message); + } + + order.setStatus(OrderStatus.SUCCESS); + order.setTotalPrice(amount); + orderMapper.updateById(order); + log.info("订单已成功下单"); + log.info("=============ORDER END================="); + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/resources/application.yml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/resources/application.yml new file mode 100644 index 0000000..3cb234e --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/resources/application.yml @@ -0,0 +1,44 @@ +server: + port: 5001 +spring: + data: + redis: + ##redis 单机环境配置 + host: jeecg-boot-redis + port: 6379 + database: 0 + password: + ssl: + enabled: false + application: + name: seata-order + cloud: + nacos: + config: + import-check: + enabled: false + main: + allow-bean-definition-overriding: true + autoconfigure: + exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg_order?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + sql: + init: + schema-locations: classpath:sql/schema-order.sql +seata: + enable-auto-data-source-proxy: true + service: + grouplist: + default: 127.0.0.1:8091 + vgroup-mapping: + springboot-seata-group: default + # seata 事务组编号 用于TC集群名 + tx-service-group: springboot-seata-group + +# 无用配置,为了避免扫码全代码导致启动慢 +minidao: + base-package: com.ghb.base.modules.jmreport.* \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/resources/sql/schema-order.sql b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/resources/sql/schema-order.sql new file mode 100644 index 0000000..4f16e3c --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-order/src/main/resources/sql/schema-order.sql @@ -0,0 +1,37 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for p_order +-- ---------------------------- +DROP TABLE IF EXISTS `p_order`; +CREATE TABLE `p_order` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NULL DEFAULT NULL, + `product_id` int(11) NULL DEFAULT NULL, + `count` int(11) NULL DEFAULT NULL, + `total_price` decimal(10, 2) NULL DEFAULT NULL, + `status` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, + `add_time` timestamp NULL DEFAULT current_timestamp(), + `last_update_time` timestamp NULL DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for undo_log +-- ---------------------------- +DROP TABLE IF EXISTS `undo_log`; +CREATE TABLE `undo_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `branch_id` bigint(20) NOT NULL, + `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `rollback_info` longblob NOT NULL, + `log_status` int(11) NOT NULL, + `log_created` datetime(0) NOT NULL, + `log_modified` datetime(0) NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/pom.xml new file mode 100644 index 0000000..48b1435 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/pom.xml @@ -0,0 +1,14 @@ + + + + test-cloud-test-seata + com.ghb + 3.9.2 + + 4.0.0 + 分布式事务测试模块 + test-cloud-test-seata-product + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/SeataProductApplication.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/SeataProductApplication.java new file mode 100644 index 0000000..ccec6b2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/SeataProductApplication.java @@ -0,0 +1,16 @@ +package com.ghb.base; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author zyf + */ +@SpringBootApplication +public class SeataProductApplication { + + public static void main(String[] args) { + SpringApplication.run(SeataProductApplication.class, args); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/controller/SeataProductController.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/controller/SeataProductController.java new file mode 100644 index 0000000..7f9b742 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/controller/SeataProductController.java @@ -0,0 +1,27 @@ +package com.ghb.base.modules.test.seata.product.controller; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.test.seata.product.service.SeataProductService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; +import java.math.BigDecimal; + +/** + * @author zyf + */ +@RestController +@RequestMapping("/test/seata/product") +public class SeataProductController { + + @Autowired + private SeataProductService seataProductService; + + @PostMapping("/reduceStock") + public Result reduceStock(Long productId, Integer count, HttpServletRequest request) { + return seataProductService.reduceStock(productId, count); + } +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/entity/SeataProduct.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/entity/SeataProduct.java new file mode 100644 index 0000000..3f50eee --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/entity/SeataProduct.java @@ -0,0 +1,34 @@ +package com.ghb.base.modules.test.seata.product.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +/** + * @Description: 产品 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Data +@Builder +@TableName("product") +public class SeataProduct { + + @TableId(type = IdType.AUTO) + private Integer id; + /** + * 价格 + */ + private BigDecimal price; + /** + * 库存 + */ + private Integer stock; + + private Date lastUpdateTime; +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/mapper/SeataProductMapper.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/mapper/SeataProductMapper.java new file mode 100644 index 0000000..ed7ceee --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/mapper/SeataProductMapper.java @@ -0,0 +1,16 @@ +package com.ghb.base.modules.test.seata.product.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import com.ghb.base.modules.test.seata.product.entity.SeataProduct; + + +/** + * @Description: TODO + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Mapper +public interface SeataProductMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/service/SeataProductService.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/service/SeataProductService.java new file mode 100644 index 0000000..5298b48 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/service/SeataProductService.java @@ -0,0 +1,22 @@ +package com.ghb.base.modules.test.seata.product.service; + +import com.ghb.base.common.api.vo.Result; + +import java.math.BigDecimal; + +/** + * @Description: 产品接口 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +public interface SeataProductService { + /** + * 扣减库存 + * + * @param productId 商品 ID + * @param count 扣减数量 + * @return 商品总价 + */ + Result reduceStock(Long productId, Integer count); +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/service/impl/SeataProductServiceImpl.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/service/impl/SeataProductServiceImpl.java new file mode 100644 index 0000000..4a35a15 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/java/com/ghb/base/modules/test/seata/product/service/impl/SeataProductServiceImpl.java @@ -0,0 +1,62 @@ +package com.ghb.base.modules.test.seata.product.service.impl; + +import com.baomidou.dynamic.datasource.annotation.DS; +import io.seata.core.context.RootContext; +import lombok.extern.slf4j.Slf4j; + + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.modules.test.seata.product.entity.SeataProduct; +import com.ghb.base.modules.test.seata.product.mapper.SeataProductMapper; +import com.ghb.base.modules.test.seata.product.service.SeataProductService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import jakarta.annotation.Resource; +import java.math.BigDecimal; + +/** + * @Description: 产品服务类 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Slf4j +@Service +public class SeataProductServiceImpl implements SeataProductService { + + @Resource + private SeataProductMapper productMapper; + + /** + * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 + */ + @DS("product") + @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class) + @Override + public Result reduceStock(Long productId, Integer count) { + log.info("xid:"+ RootContext.getXID()); + log.info("=============PRODUCT START================="); + // 检查库存 + SeataProduct product = productMapper.selectById(productId); + Assert.notNull(product, "商品不存在"); + Integer stock = product.getStock(); + log.info("商品编号为 {} 的库存为{},订单商品数量为{}", productId, stock, count); + + if (stock < count) { + log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock); + return Result.error("库存不足"); + } + log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice()); + // 扣减库存 + int currentStock = stock - count; + product.setStock(currentStock); + productMapper.updateById(product); + BigDecimal totalPrice = product.getPrice().multiply(new BigDecimal(count)); + log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, count, totalPrice); + log.info("=============PRODUCT END================="); + return Result.OK(totalPrice); + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/resources/application.yml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/resources/application.yml new file mode 100644 index 0000000..b6ffbc5 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/resources/application.yml @@ -0,0 +1,44 @@ +server: + port: 5003 +spring: + data: + redis: + ##redis 单机环境配置 + host: jeecg-boot-redis + port: 6379 + database: 0 + password: + ssl: + enabled: false + application: + name: seata-product + cloud: + nacos: + config: + import-check: + enabled: false + main: + allow-bean-definition-overriding: true + autoconfigure: + exclude: com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://jeecg-boot-mysql:3306/jeecg_product?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai + username: root + password: root + sql: + init: + schema-locations: classpath:sql/schema-product.sql +seata: + enable-auto-data-source-proxy: true + service: + grouplist: + default: 127.0.0.1:8091 + vgroup-mapping: + springboot-seata-group: default + # seata 事务组编号 用于TC集群名 + tx-service-group: springboot-seata-group + +# 无用配置,为了避免扫码全代码导致启动慢 +minidao: + base-package: com.ghb.base.modules.jmreport.* \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/resources/sql/schema-product.sql b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/resources/sql/schema-product.sql new file mode 100644 index 0000000..9ef8f5b --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-seata/test-cloud-test-seata-product/src/main/resources/sql/schema-product.sql @@ -0,0 +1,38 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for product +-- ---------------------------- +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `price` decimal(10, 2) NULL DEFAULT NULL, + `stock` int(11) NULL DEFAULT NULL, + `last_update_time` timestamp NULL DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of product +-- ---------------------------- +INSERT INTO `product` VALUES (1, 10.00, 20, '2022-01-13 09:52:50'); + +-- ---------------------------- +-- Table structure for undo_log +-- ---------------------------- +DROP TABLE IF EXISTS `undo_log`; +CREATE TABLE `undo_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `branch_id` bigint(20) NOT NULL, + `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + `rollback_info` longblob NOT NULL, + `log_status` int(11) NOT NULL, + `log_created` datetime(0) NOT NULL, + `log_modified` datetime(0) NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/README-ShardingSphere配置说明.md b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/README-ShardingSphere配置说明.md new file mode 100644 index 0000000..e5f3d15 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/README-ShardingSphere配置说明.md @@ -0,0 +1,176 @@ +# JeecgBoot ShardingSphere配置使用说明 + +## 项目中的ShardingSphere配置 + +本项目使用ShardingSphere实现分库分表功能,主要涉及以下配置文件和组件: + +## 1. 配置文件说明 + +### sharding.yaml - 基础分表配置 +```yaml +databaseName: sharding-db # 重要:必须与@DS注解中的名称一致 + +dataSources: + ds0: + dataSourceClassName: com.zaxxer.hikari.HikariDataSource + driverClassName: com.mysql.cj.jdbc.Driver + jdbcUrl: jdbc:mysql://localhost:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8 + username: root + password: root + +rules: + - !SHARDING + tables: + sys_log: # 分表的逻辑表名 + actualDataNodes: ds0.sys_log$->{0..1} # 实际表:sys_log0, sys_log1 + tableStrategy: + standard: + shardingColumn: log_type # 分片字段 + shardingAlgorithmName: table_inline + + shardingAlgorithms: + table_inline: + type: INLINE + props: + algorithm-expression: sys_log$->{log_type % 2} # 根据log_type取模分表 +``` + +### sharding-multi.yaml - 分库分表+读写分离配置 +```yaml +databaseName: sharding-db # 与@DS注解保持一致 + +dataSources: + ds0: # 主库 + jdbcUrl: jdbc:mysql://localhost:3306/jeecg-boot?... + ds1: # 从库 + jdbcUrl: jdbc:mysql://localhost:3306/jeecg-boot2?... + +rules: + - !SHARDING + tables: + sys_log: + actualDataNodes: ds$->{0..1}.sys_log$->{0..1} # 2库2表 + databaseStrategy: # 分库策略 + standard: + shardingColumn: operate_type + shardingAlgorithmName: database-inline + tableStrategy: # 分表策略 + standard: + shardingColumn: log_type + shardingAlgorithmName: table-classbased + + - !READWRITE_SPLITTING # 读写分离 + dataSources: + prds: + writeDataSourceName: ds0 # 写库 + readDataSourceNames: [ds1] # 读库 +``` + +## 2. Spring Boot配置 + +### application-dev.yml中的数据源配置 + +```yaml +spring: + datasource: + dynamic: + datasource: + # 普通数据源 + master: + url: jdbc:mysql://localhost:3306/jeecg-boot + username: root + password: root + + # ShardingSphere分片数据源 + sharding-db: # 数据源名称,对应@DS("sharding-db") + driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver + # 本地配置文件方式 + url: jdbc:shardingsphere:classpath:sharding.yaml + # 或者Nacos配置方式 + url: jdbc:shardingsphere:nacos:sharding.yaml?serverAddr=${spring.cloud.nacos.config.server-addr}&namespace=${spring.cloud.nacos.config.namespace}&group=${spring.cloud.nacos.config.group} +``` + +**关键点:** +- `sharding-db` 是数据源的名称标识 +- 这个名称必须与Service类上的`@DS("sharding-db")`注解保持一致 + +## 3. Service层使用 + +### ShardingSysLogServiceImpl类配置 + +```java +@Service +@DS("sharding-db") // 指定使用sharding-db数据源 +public class ShardingSysLogServiceImpl extends ServiceImpl + implements IShardingSysLogService { +} +``` + +**配置关系说明:** +1. `@DS("sharding-db")` 注解告诉MyBatis-Plus使用名为`sharding-db`的数据源 +2. `sharding-db`对应application-dev.yml中配置的数据源名称 +3. 该数据源使用ShardingSphere驱动,会根据sharding.yaml中的规则进行分片 + +## 4. 使用步骤 + +### 步骤1:准备数据库表 +```sql +-- 在jeecg-boot数据库中创建分表 +CREATE TABLE sys_log0 LIKE sys_log; +CREATE TABLE sys_log1 LIKE sys_log; +``` + +### 步骤2:配置application-dev.yml +```yaml +spring: + datasource: + dynamic: + datasource: + sharding-db: + driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver + url: jdbc:shardingsphere:classpath:sharding.yaml +``` + +### 步骤3:配置sharding.yaml +- 将配置文件放在`src/main/resources/`目录下 +- 确保`databaseName: sharding-db`与数据源名称一致 + +### 步骤4:在Service上添加注解 +```java +@DS("sharding-db") // 使用分片数据源 +public class ShardingSysLogServiceImpl { + // 业务代码 +} +``` + +### 步骤5:正常使用MyBatis-Plus +```java +// 插入数据时会自动根据log_type字段进行分表 +shardingSysLogService.save(sysLog); + +// 查询时也会根据分片规则路由到正确的表 +shardingSysLogService.list(); +``` + +## 5. 配置验证 + +启动项目后查看日志,如果看到类似输出说明配置成功: +``` +Logic SQL: INSERT INTO sys_log (log_type, content) VALUES (?, ?) +Actual SQL: ds0 ::: INSERT INTO sys_log0 (log_type, content) VALUES (?, ?) +``` + +## 6. 注意事项 + +1. **名称一致性**:确保以下三处名称完全一致 + - application-dev.yml中的数据源名称:`sharding-db` + - sharding.yaml中的databaseName:`sharding-db` + - Service类注解:`@DS("sharding-db")` + +2. **表结构一致**:所有分片表的结构必须完全一致 + +3. **分片键选择**:选择分布均匀的字段作为分片键,避免数据倾斜 + +4. **事务支持**:单表事务正常,跨表事务需要注意 + +这样配置后,通过ShardingSysLogServiceImpl操作的数据会自动根据分片规则分布到不同的表中。 diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/doc/db.sql b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/doc/db.sql new file mode 100644 index 0000000..ea2b720 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/doc/db.sql @@ -0,0 +1,47 @@ +CREATE TABLE `sys_log0` ( + `id` varchar(32) NOT NULL, + `log_type` int(2) DEFAULT NULL COMMENT '日志类型(1登录日志,2操作日志)', + `log_content` varchar(1000) DEFAULT NULL COMMENT '日志内容', + `operate_type` int(2) DEFAULT NULL COMMENT '操作类型', + `userid` varchar(32) DEFAULT NULL COMMENT '操作用户账号', + `username` varchar(100) DEFAULT NULL COMMENT '操作用户名称', + `ip` varchar(100) DEFAULT NULL COMMENT 'IP', + `method` varchar(500) DEFAULT NULL COMMENT '请求java方法', + `request_url` varchar(255) DEFAULT NULL COMMENT '请求路径', + `request_param` longtext DEFAULT NULL COMMENT '请求参数', + `request_type` varchar(10) DEFAULT NULL COMMENT '请求类型', + `cost_time` bigint(20) DEFAULT NULL COMMENT '耗时', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `index_table_userid` (`userid`) USING BTREE, + KEY `index_logt_ype` (`log_type`) USING BTREE, + KEY `index_operate_type` (`operate_type`) USING BTREE, + KEY `index_createtime` (`create_time`) USING BTREE +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统日志表'; + +CREATE TABLE `sys_log1` ( + `id` varchar(32) NOT NULL, + `log_type` int(2) DEFAULT NULL COMMENT '日志类型(1登录日志,2操作日志)', + `log_content` varchar(1000) DEFAULT NULL COMMENT '日志内容', + `operate_type` int(2) DEFAULT NULL COMMENT '操作类型', + `userid` varchar(32) DEFAULT NULL COMMENT '操作用户账号', + `username` varchar(100) DEFAULT NULL COMMENT '操作用户名称', + `ip` varchar(100) DEFAULT NULL COMMENT 'IP', + `method` varchar(500) DEFAULT NULL COMMENT '请求java方法', + `request_url` varchar(255) DEFAULT NULL COMMENT '请求路径', + `request_param` longtext DEFAULT NULL COMMENT '请求参数', + `request_type` varchar(10) DEFAULT NULL COMMENT '请求类型', + `cost_time` bigint(20) DEFAULT NULL COMMENT '耗时', + `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `index_table_userid` (`userid`) USING BTREE, + KEY `index_logt_ype` (`log_type`) USING BTREE, + KEY `index_operate_type` (`operate_type`) USING BTREE, + KEY `index_createtime` (`create_time`) USING BTREE +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统日志表'; diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/pom.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/pom.xml new file mode 100644 index 0000000..6c3ff16 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/pom.xml @@ -0,0 +1,21 @@ + + + + test-cloud-test + com.ghb + 3.9.2 + + 4.0.0 + + test-cloud-test-shardingsphere + + + + org.jeecgframework.boot3 + jeecg-boot-starter-shardingsphere + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/algorithm/StandardModTableShardAlgorithm.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/algorithm/StandardModTableShardAlgorithm.java new file mode 100644 index 0000000..c304a29 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/algorithm/StandardModTableShardAlgorithm.java @@ -0,0 +1,64 @@ +package com.ghb.base.modules.test.sharding.algorithm; + + +import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; +import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue; +import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; + +import java.util.Collection; +import java.util.Properties; + +/** + * 用于处理使用单一键 + * 根据分片字段的值和sharding-count进行取模运算 + * SQL 语句中有>,>=, <=,<,=,IN 和 BETWEEN AND 操作符,都可以应用此分片策略。 + * + * @author zyf + */ +public class StandardModTableShardAlgorithm implements StandardShardingAlgorithm { + private Properties props = new Properties(); + + + /** + * 用于处理=和IN的分片 + * + * @param collection 目标分片的集合(表名) + * @param preciseShardingValue 逻辑表相关信息 + * @return + */ + @Override + public String doSharding(Collection collection, PreciseShardingValue preciseShardingValue) { + + for (String name : collection) { + Integer value = preciseShardingValue.getValue(); + //根据值进行取模,得到一个目标值 + if (name.indexOf(value % 2+"") > -1) { + return name; + } + } + throw new UnsupportedOperationException(); + } + + /** + * 用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理 + * + * @param collection + * @param rangeShardingValue + * @return + */ + @Override + public Collection doSharding(Collection collection, RangeShardingValue rangeShardingValue) { + + return collection; + } + + /** + * 对应分片算法(sharding-algorithms)的类型 + * + * @return + */ + @Override + public String getType() { + return "STANDARD_MOD"; + } +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/controller/GhbShardingDemoController.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/controller/GhbShardingDemoController.java new file mode 100644 index 0000000..78475a5 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/controller/GhbShardingDemoController.java @@ -0,0 +1,89 @@ +package com.ghb.base.modules.test.sharding.controller; + +import com.ghb.base.common.api.vo.Result; +import com.ghb.base.common.aspect.annotation.AutoLog; +import com.ghb.base.common.system.base.controller.GhbController; +import com.ghb.base.modules.test.sharding.entity.ShardingSysLog; +import com.ghb.base.modules.test.sharding.service.IShardingSysLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; + +/** + * @Description: 分库分表测试 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +@Slf4j +@Tag(name = "分库分表测试") +@RestController +@RequestMapping("/demo/sharding") +public class GhbShardingDemoController extends GhbController { + @Autowired + private IShardingSysLogService shardingSysLogService; + + /** + * 单库分表 —— 插入 + * @return + */ + @PostMapping(value = "/insert") + @Operation(summary = "单库分表插入") + public Result insert() { + log.info("---------------------------------单库分表插入--------------------------------"); + int size = 10; + for (int i = 0; i < size; i++) { + ShardingSysLog shardingSysLog = new ShardingSysLog(); + shardingSysLog.setLogContent("采用shardingsphere实现分库分表,插入测试!"); + shardingSysLog.setLogType(i); + shardingSysLog.setOperateType(i); + shardingSysLogService.save(shardingSysLog); + } + return Result.OK("单库分表插入10条数据完成!"); + } + + /** + * 单库分表 —— 查询 + * @return + */ + @PostMapping(value = "/list") + @Operation(summary = "单库分表查询") + public Result list() { + return Result.OK(shardingSysLogService.list()); + } + + /** + * 分库分表 - 插入 + * @return + */ + @PostMapping(value = "/insert2") + @Operation(summary = "分库分表插入") + public Result insert2() { + int start=20; + int size=30; + for (int i = start; i <= size; i++) { + ShardingSysLog shardingSysLog = new ShardingSysLog(); + shardingSysLog.setLogContent("分库分表测试"); + shardingSysLog.setLogType(0); + shardingSysLog.setOperateType(i); + shardingSysLogService.save(shardingSysLog); + } + return Result.OK("分库分表插入10条数据完成!"); + } + + /** + * 分库分表 - 查询 + * @return + */ + @PostMapping(value = "/list2") + @Operation(summary = "分库分表查询") + public Result list2() { + return Result.OK(shardingSysLogService.list()); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/entity/ShardingSysLog.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/entity/ShardingSysLog.java new file mode 100644 index 0000000..6191c65 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/entity/ShardingSysLog.java @@ -0,0 +1,109 @@ +package com.ghb.base.modules.test.sharding.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import com.ghb.base.common.aspect.annotation.Dict; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * 系统日志表 + * @author: zyf + * @date: 2022/04/21 + */ +@Data +@TableName("sys_log") +public class ShardingSysLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + private Date updateTime; + + /** + * 耗时 + */ + private Long costTime; + + /** + * IP + */ + private String ip; + + /** + * 请求参数 + */ + private String requestParam; + + /** + * 请求类型 + */ + private String requestType; + + /** + * 请求路径 + */ + private String requestUrl; + /** + * 请求方法 + */ + private String method; + + /** + * 操作人用户名称 + */ + private String username; + /** + * 操作人用户账户 + */ + private String userid; + /** + * 操作详细日志 + */ + private String logContent; + + /** + * 日志类型(1登录日志,2操作日志) + */ + @Dict(dicCode = "log_type") + private Integer logType; + + /** + * 操作类型(1查询,2添加,3修改,4删除,5导入,6导出) + */ + @Dict(dicCode = "operate_type") + private Integer operateType; + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/mapper/ShardingSysLogMapper.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/mapper/ShardingSysLogMapper.java new file mode 100644 index 0000000..0898520 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/mapper/ShardingSysLogMapper.java @@ -0,0 +1,15 @@ +package com.ghb.base.modules.test.sharding.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ghb.base.modules.test.sharding.entity.ShardingSysLog; + + +/** + * @Description: 系统日志表 Mapper 接口 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +public interface ShardingSysLogMapper extends BaseMapper { + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/mapper/xml/ShardingSysLogMapper.xml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/mapper/xml/ShardingSysLogMapper.xml new file mode 100644 index 0000000..86c5f23 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/mapper/xml/ShardingSysLogMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/service/IShardingSysLogService.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/service/IShardingSysLogService.java new file mode 100644 index 0000000..4296f55 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/service/IShardingSysLogService.java @@ -0,0 +1,14 @@ +package com.ghb.base.modules.test.sharding.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.ghb.base.modules.test.sharding.entity.ShardingSysLog; + +/** + * @Description: 系统日志表 服务类 + * @author: zyf + * @date: 2022/01/24 + * @version: V1.0 + */ +public interface IShardingSysLogService extends IService { + +} diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/service/impl/ShardingSysLogServiceImpl.java b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/service/impl/ShardingSysLogServiceImpl.java new file mode 100644 index 0000000..683f549 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/java/com/ghb/base/modules/test/sharding/service/impl/ShardingSysLogServiceImpl.java @@ -0,0 +1,19 @@ +package com.ghb.base.modules.test.sharding.service.impl; + +import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.ghb.base.modules.test.sharding.entity.ShardingSysLog; +import com.ghb.base.modules.test.sharding.mapper.ShardingSysLogMapper; +import com.ghb.base.modules.test.sharding.service.IShardingSysLogService; +import org.springframework.stereotype.Service; + +/** + * 系统日志表 服务实现类 + * @author: zyf + * @date: 2022/04/21 + */ +@Service +@DS("sharding-db") +public class ShardingSysLogServiceImpl extends ServiceImpl implements IShardingSysLogService { + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/resources/sharding-multi.yaml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/resources/sharding-multi.yaml new file mode 100644 index 0000000..5299ea6 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/resources/sharding-multi.yaml @@ -0,0 +1,67 @@ +# !!!数据源名称要和动态数据源中配置的名称一致 +databaseName: sharding-db + +# 具体参看官网文档说明 +dataSources: + ds0: + dataSourceClassName: com.zaxxer.hikari.HikariDataSource + driverClassName: com.mysql.cj.jdbc.Driver + jdbcUrl: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + password: root + username: root + ds1: + dataSourceClassName: com.zaxxer.hikari.HikariDataSource + driverClassName: com.mysql.cj.jdbc.Driver + jdbcUrl: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + password: root + username: root + +rules: + - !SHARDING + bindingTables: + - sys_log + tables: + sys_log: + actualDataNodes: ds$->{0..1}.sys_log$->{0..1} + databaseStrategy: + standard: + shardingColumn: operate_type + shardingAlgorithmName: database-inline + tableStrategy: + standard: + shardingColumn: log_type + shardingAlgorithmName: table-classbased + keyGenerateStrategy: + column: id + keyGeneratorName: snowflake + + keyGenerators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + + shardingAlgorithms: + database-inline: + type: INLINE + props: + algorithm-expression: ds$->{operate_type % 2} + table-classbased: + type: CLASS_BASED + props: + strategy: standard + algorithmClassName: com.ghb.base.modules.test.sharding.algorithm.StandardModTableShardAlgorithm + + - !READWRITE_SPLITTING + dataSources: + prds: + writeDataSourceName: ds0 + readDataSourceNames: + - ds1 + loadBalancerName: round-robin + loadBalancers: + round-robin: + type: ROUND_ROBIN + +props: + sql-show: true \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/resources/sharding.yaml b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/resources/sharding.yaml new file mode 100644 index 0000000..553c94f --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-test/test-cloud-test-shardingsphere/src/main/resources/sharding.yaml @@ -0,0 +1,40 @@ +# !!!数据源名称要和动态数据源中配置的名称一致 +databaseName: sharding-db + +# 具体参看官网文档说明 +dataSources: + db_0: + dataSourceClassName: com.zaxxer.hikari.HikariDataSource + driverClassName: com.mysql.cj.jdbc.Driver + jdbcUrl: jdbc:mysql://jeecg-boot-mysql:3306/jeecg-boot?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + password: root + username: root + +rules: + - !SHARDING + tables: # 数据分片规则配置 + sys_log: # 逻辑表名称 + actualDataNodes: db_0.sys_log$->{0..1} # 由数据源名 + 表名组成(参考 Inline 语法规则) + databaseStrategy: # 分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一 + none: + tableStrategy: # 分表策略 + standard: # 用于单分片键的标准分片场景 + shardingColumn: log_type # 分片列名称 + shardingAlgorithmName: user_inline + keyGenerateStrategy: + column: id + keyGeneratorName: snowflake + keyGenerators: + snowflake: + type: SNOWFLAKE + props: + worker-id: 123 + # 分片算法配置 + shardingAlgorithms: + user_inline: + type: INLINE + props: + algorithm-expression: sys_log$->{log_type % 2} + +props: + sql-show: true \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/Dockerfile b/test-server-cloud/test-visual/test-cloud-xxljob/Dockerfile new file mode 100644 index 0000000..4cdd55e --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/Dockerfile @@ -0,0 +1,26 @@ +FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:17-anolis + +MAINTAINER jeecgos@163.com + +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime + +# 解决linuxkit 精简镜像对 locale 裁剪导致中文乱码问题 java:17-anolis基于anolis(CentOS/RHEL 系)应当使用yum +RUN yum install -y --setopt=tsflags=nodocs \ + glibc-langpack-en \ + glibc-common \ + && yum clean all + +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 +ENV JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8" + +RUN mkdir -p /jeecg-cloud-xxljob + +WORKDIR /jeecg-cloud-xxljob + +EXPOSE 9080 + +ADD ./target/jeecg-cloud-xxljob-3.9.2.jar ./ + +CMD exec java -Djava.security.egd=file:/dev/./urandom -jar jeecg-cloud-xxljob-3.9.2.jar + diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/README.md b/test-server-cloud/test-visual/test-cloud-xxljob/README.md new file mode 100644 index 0000000..ddef2ad --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/README.md @@ -0,0 +1,25 @@ +- 初始化脚本(mysql) + + db\tables_xxl_job.sql + +- 修改数据库连接 + + jeecg-cloud-xxljob\src\main\resources\application.yml + +- 启动项目 + + jeecg-cloud-xxljob\src\main\java\com\xxl\job\admin\XxlJobAdminApplication.java + + - 访问项目 + http://127.0.0.1:9080/xxl-job-admin/toLogin + admin/123456 + + - docker方式安装 + + https://my.oschina.net/jeecg/blog/4729020 + + + + 概念说明 + 1、手工创建执行器,AppName对应服务名字 比如: jeecg-demo + 2、手工创建定时任务,选择执行器(服务)、JobHandler对应XxlJob的值 \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/doc/db/tables_xxl_job.sql b/test-server-cloud/test-visual/test-cloud-xxljob/doc/db/tables_xxl_job.sql new file mode 100644 index 0000000..c349902 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/doc/db/tables_xxl_job.sql @@ -0,0 +1,352 @@ +/* + Navicat Premium Data Transfer + + Source Server : mysql5.7 + Source Server Type : MySQL + Source Server Version : 50738 (5.7.38) + Source Host : 127.0.0.1:3306 + Source Schema : xxl_job + + Target Server Type : MySQL + Target Server Version : 50738 (5.7.38) + File Encoding : 65001 + + Date: 10/02/2025 13:49:31 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for xxl_job_group +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_group`; +CREATE TABLE `xxl_job_group` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `app_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '执行器AppName', + `title` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '执行器名称', + `address_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT '执行器地址类型:0=自动注册、1=手动录入', + `address_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '执行器地址列表,多地址逗号分隔', + `update_time` datetime NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_group +-- ---------------------------- +INSERT INTO `xxl_job_group` VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2025-02-10 13:49:04'); +INSERT INTO `xxl_job_group` VALUES (2, 'jeecg-demo', '测试Demo模块', 0, NULL, '2025-02-10 13:49:04'); +INSERT INTO `xxl_job_group` VALUES (3, 'jeecg-system', '系统System模块', 0, NULL, '2025-02-10 13:49:04'); + +-- ---------------------------- +-- Table structure for xxl_job_info +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_info`; +CREATE TABLE `xxl_job_info` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `job_group` int(11) NOT NULL COMMENT '执行器主键ID', + `job_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `add_time` datetime NULL DEFAULT NULL, + `update_time` datetime NULL DEFAULT NULL, + `author` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '作者', + `alarm_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '报警邮件', + `schedule_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NONE' COMMENT '调度类型', + `schedule_conf` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型', + `misfire_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略', + `executor_route_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器路由策略', + `executor_handler` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务handler', + `executor_param` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务参数', + `executor_block_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '阻塞处理策略', + `executor_timeout` int(11) NOT NULL DEFAULT 0 COMMENT '任务执行超时时间,单位秒', + `executor_fail_retry_count` int(11) NOT NULL DEFAULT 0 COMMENT '失败重试次数', + `glue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'GLUE类型', + `glue_source` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'GLUE源代码', + `glue_remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'GLUE备注', + `glue_updatetime` datetime NULL DEFAULT NULL COMMENT 'GLUE更新时间', + `child_jobid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '子任务ID,多个逗号分隔', + `trigger_status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '调度状态:0-停止,1-运行', + `trigger_last_time` bigint(13) NOT NULL DEFAULT 0 COMMENT '上次调度时间', + `trigger_next_time` bigint(13) NOT NULL DEFAULT 0 COMMENT '下次调度时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_info +-- ---------------------------- +INSERT INTO `xxl_job_info` VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2024-08-21 22:30:30', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJob', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '', 1, 1729353600000, 1739203200000); +INSERT INTO `xxl_job_info` VALUES (2, 3, '测试jeecg xxljob', '2024-08-21 22:41:10', '2024-08-21 22:41:30', 'JEECG', '', 'CRON', '* * * * * ?', 'DO_NOTHING', 'FIRST', 'demoJob', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2024-08-21 22:41:10', '', 1, 1739166572000, 1739166573000); + +-- ---------------------------- +-- Table structure for xxl_job_lock +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_lock`; +CREATE TABLE `xxl_job_lock` ( + `lock_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '锁名称', + PRIMARY KEY (`lock_name`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_lock +-- ---------------------------- +INSERT INTO `xxl_job_lock` VALUES ('schedule_lock'); + +-- ---------------------------- +-- Table structure for xxl_job_log +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_log`; +CREATE TABLE `xxl_job_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `job_group` int(11) NOT NULL COMMENT '执行器主键ID', + `job_id` int(11) NOT NULL COMMENT '任务,主键ID', + `executor_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器地址,本次执行的地址', + `executor_handler` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务handler', + `executor_param` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务参数', + `executor_sharding_param` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2', + `executor_fail_retry_count` int(11) NOT NULL DEFAULT 0 COMMENT '失败重试次数', + `trigger_time` datetime NULL DEFAULT NULL COMMENT '调度-时间', + `trigger_code` int(11) NOT NULL COMMENT '调度-结果', + `trigger_msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '调度-日志', + `handle_time` datetime NULL DEFAULT NULL COMMENT '执行-时间', + `handle_code` int(11) NOT NULL COMMENT '执行-状态', + `handle_msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '执行-日志', + `alarm_status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败', + PRIMARY KEY (`id`) USING BTREE, + INDEX `I_trigger_time`(`trigger_time`) USING BTREE, + INDEX `I_handle_code`(`handle_code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6761 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_log +-- ---------------------------- +INSERT INTO `xxl_job_log` VALUES (6618, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:09', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6619, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:10', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6620, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:11', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6621, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:12', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6622, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:13', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6623, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:14', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6624, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:15', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6625, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:16', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6626, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:17', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6627, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:18', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6628, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:19', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6629, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:20', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6630, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:21', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6631, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:22', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6632, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:23', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6633, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:24', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6634, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:25', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6635, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:26', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6636, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:27', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6637, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:28', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6638, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:29', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6639, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:30', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6640, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:31', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6641, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:32', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6642, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:33', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6643, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:34', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6644, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:35', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6645, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:36', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6646, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:37', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6647, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:38', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6648, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:39', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6649, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:40', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6650, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:41', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6651, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:42', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6652, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:43', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6653, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:44', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6654, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:45', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6655, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:46', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6656, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:47', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6657, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:48', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6658, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:49', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6659, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:50', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6660, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:51', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6661, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:52', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6662, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:53', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6663, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:54', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6664, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:55', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6665, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:56', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6666, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:57', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6667, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:58', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6668, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:59', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6669, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:00', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6670, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:01', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6671, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:02', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6672, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:03', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6673, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:04', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6674, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:05', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6675, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:06', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6676, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:07', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6677, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:08', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6678, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:09', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6679, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:10', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6680, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:11', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6681, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:12', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6682, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:13', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6683, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:14', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6684, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:15', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6685, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:16', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6686, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:17', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6687, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:18', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6688, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:19', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6689, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:20', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6690, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:21', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6691, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:22', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6692, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:23', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6693, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:24', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6694, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:25', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6695, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:26', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6696, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:27', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6697, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:28', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6698, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:29', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6699, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:30', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6700, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:31', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6701, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:32', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6702, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:33', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6703, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:34', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6704, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:35', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6705, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:36', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6706, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:37', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6707, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:38', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6708, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:39', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6709, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:40', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6710, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:41', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6711, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:42', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6712, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:43', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6713, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:44', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6714, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:45', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6715, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:46', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6716, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:47', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6717, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:48', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6718, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:49', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6719, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:50', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6720, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:51', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6721, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:52', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6722, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:53', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6723, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:54', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6724, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:55', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6725, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:56', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6726, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:57', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6727, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:58', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6728, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:59', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6729, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:00', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6730, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:01', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6731, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:02', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6732, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:03', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6733, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:04', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6734, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:05', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6735, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:06', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6736, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:07', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6737, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:08', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6738, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:09', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6739, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:10', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6740, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:11', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6741, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:12', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6742, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:13', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6743, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:14', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6744, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:15', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6745, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:16', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6746, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:17', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6747, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:18', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6748, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:19', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6749, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:20', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6750, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:21', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6751, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:22', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6752, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:23', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6753, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:24', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 2); +INSERT INTO `xxl_job_log` VALUES (6754, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:25', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 0); +INSERT INTO `xxl_job_log` VALUES (6755, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:26', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 0); +INSERT INTO `xxl_job_log` VALUES (6756, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:27', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 0); +INSERT INTO `xxl_job_log` VALUES (6757, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:28', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 0); +INSERT INTO `xxl_job_log` VALUES (6758, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:29', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 0); +INSERT INTO `xxl_job_log` VALUES (6759, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:30', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 0); +INSERT INTO `xxl_job_log` VALUES (6760, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:31', 500, '任务触发类型:Cron触发
调度机器:192.168.1.11
执行器-注册方式:自动注册
执行器-地址列表:null
路由策略:第一个
阻塞处理策略:单机串行
任务超时时间:0
失败重试次数:0

>>>>>>>>>>>触发调度<<<<<<<<<<<
调度失败:执行器地址为空

', NULL, 0, NULL, 0); + +-- ---------------------------- +-- Table structure for xxl_job_log_report +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_log_report`; +CREATE TABLE `xxl_job_log_report` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `trigger_day` datetime NULL DEFAULT NULL COMMENT '调度-时间', + `running_count` int(11) NOT NULL DEFAULT 0 COMMENT '运行中-日志数量', + `suc_count` int(11) NOT NULL DEFAULT 0 COMMENT '执行成功-日志数量', + `fail_count` int(11) NOT NULL DEFAULT 0 COMMENT '执行失败-日志数量', + `update_time` datetime NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `i_trigger_day`(`trigger_day`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_log_report +-- ---------------------------- +INSERT INTO `xxl_job_log_report` VALUES (1, '2024-08-21 00:00:00', 70, 0, 5, NULL); +INSERT INTO `xxl_job_log_report` VALUES (2, '2024-08-20 00:00:00', 0, 0, 0, NULL); +INSERT INTO `xxl_job_log_report` VALUES (3, '2024-08-19 00:00:00', 0, 0, 0, NULL); +INSERT INTO `xxl_job_log_report` VALUES (4, '2024-09-10 00:00:00', 0, 0, 56, NULL); +INSERT INTO `xxl_job_log_report` VALUES (5, '2024-09-09 00:00:00', 0, 0, 0, NULL); +INSERT INTO `xxl_job_log_report` VALUES (6, '2024-09-08 00:00:00', 0, 0, 0, NULL); +INSERT INTO `xxl_job_log_report` VALUES (7, '2024-10-19 00:00:00', 0, 0, 6391, NULL); +INSERT INTO `xxl_job_log_report` VALUES (8, '2024-10-18 00:00:00', 0, 0, 0, NULL); +INSERT INTO `xxl_job_log_report` VALUES (9, '2024-10-17 00:00:00', 0, 0, 0, NULL); +INSERT INTO `xxl_job_log_report` VALUES (10, '2025-02-10 00:00:00', 0, 0, 116, NULL); +INSERT INTO `xxl_job_log_report` VALUES (11, '2025-02-09 00:00:00', 0, 0, 0, NULL); +INSERT INTO `xxl_job_log_report` VALUES (12, '2025-02-08 00:00:00', 0, 0, 0, NULL); + +-- ---------------------------- +-- Table structure for xxl_job_logglue +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_logglue`; +CREATE TABLE `xxl_job_logglue` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `job_id` int(11) NOT NULL COMMENT '任务,主键ID', + `glue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'GLUE类型', + `glue_source` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'GLUE源代码', + `glue_remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'GLUE备注', + `add_time` datetime NULL DEFAULT NULL, + `update_time` datetime NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_logglue +-- ---------------------------- + +-- ---------------------------- +-- Table structure for xxl_job_registry +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_registry`; +CREATE TABLE `xxl_job_registry` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `registry_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `registry_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `registry_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `update_time` datetime NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `i_g_k_v`(`registry_group`, `registry_key`, `registry_value`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_registry +-- ---------------------------- + +-- ---------------------------- +-- Table structure for xxl_job_user +-- ---------------------------- +DROP TABLE IF EXISTS `xxl_job_user`; +CREATE TABLE `xxl_job_user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '账号', + `password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员', + `permission` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `i_username`(`username`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of xxl_job_user +-- ---------------------------- +INSERT INTO `xxl_job_user` VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/pom.xml b/test-server-cloud/test-visual/test-cloud-xxljob/pom.xml new file mode 100644 index 0000000..e94fdf6 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/pom.xml @@ -0,0 +1,102 @@ + + + + test-visual + com.ghb + 3.9.2 + + 4.0.0 + + test-cloud-xxljob + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + + mysql + mysql-connector-java + ${mysql-connector-java.version} + runtime + + + + + + org.springframework.boot + spring-boot-starter-mail + + + + com.xuxueli + xxl-job-core + ${xxl-job-core.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + package + + repackage + + + + + true + com.xxl.job.admin.XxlJobAdminApplication + + + + org.apache.maven.plugins + maven-resources-plugin + + + otf + ttf + woff + woff2 + eot + + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java new file mode 100644 index 0000000..5959387 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java @@ -0,0 +1,27 @@ +package com.xxl.job.admin; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.Environment; + +/** + * @author xuxueli 2018-10-28 00:38:13 + */ +@SpringBootApplication +@Slf4j +public class XxlJobAdminApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext application = SpringApplication.run(XxlJobAdminApplication.class, args); + Environment env = application.getEnvironment(); + String port = env.getProperty("server.port"); + String path = env.getProperty("server.servlet.context-path"); + log.info("\n----------------------------------------------------------\n\t" + + "Application XxlJobAdmin is running! Access URLs:\n\t" + + "Local: \t\thttp://localhost:" + port + path + "/\n\t" + + "----------------------------------------------------------"); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/IndexController.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/IndexController.java new file mode 100644 index 0000000..1a3ac53 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/IndexController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +public class IndexController { + + @Resource + private XxlJobService xxlJobService; + @Resource + private LoginService loginService; + + + @RequestMapping("/") + public String index(Model model) { + + Map dashboardMap = xxlJobService.dashboardInfo(); + model.addAllAttributes(dashboardMap); + + return "index"; + } + + @RequestMapping("/chartInfo") + @ResponseBody + public ReturnT> chartInfo(Date startDate, Date endDate) { + ReturnT> chartInfo = xxlJobService.chartInfo(startDate, endDate); + return chartInfo; + } + + @RequestMapping("/toLogin") + @PermissionLimit(limit=false) + public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response,ModelAndView modelAndView) { + if (loginService.ifLogin(request, response) != null) { + modelAndView.setView(new RedirectView("/",true,false)); + return modelAndView; + } + return new ModelAndView("login"); + } + + @RequestMapping(value="login", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT loginDo(HttpServletRequest request, HttpServletResponse response, String userName, String password, String ifRemember){ + boolean ifRem = (ifRemember!=null && ifRemember.trim().length()>0 && "on".equals(ifRemember))?true:false; + return loginService.login(request, response, userName, password, ifRem); + } + + @RequestMapping(value="logout", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT logout(HttpServletRequest request, HttpServletResponse response){ + return loginService.logout(request, response); + } + + @RequestMapping("/help") + public String help() { + + /*if (!PermissionInterceptor.ifLogin(request)) { + return "redirect:/toLogin"; + }*/ + + return "help"; + } + + @InitBinder + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobApiController.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobApiController.java new file mode 100644 index 0000000..d650913 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobApiController.java @@ -0,0 +1,72 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.GsonTool; +import com.xxl.job.core.util.XxlJobRemotingUtil; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * Created by xuxueli on 17/5/10. + */ +@Controller +@RequestMapping("/api") +public class JobApiController { + + @Resource + private AdminBiz adminBiz; + + /** + * api + * + * @param uri + * @param data + * @return + */ + @RequestMapping("/{uri}") + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) { + + // valid + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support."); + } + if (uri==null || uri.trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty."); + } + if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null + && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0 + && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) { + return new ReturnT(ReturnT.FAIL_CODE, "The access token is wrong."); + } + + // services mapping + if ("callback".equals(uri)) { + List callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class); + return adminBiz.callback(callbackParamList); + } else if ("registry".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registry(registryParam); + } else if ("registryRemove".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registryRemove(registryParam); + } else { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found."); + } + + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobCodeController.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobCodeController.java new file mode 100644 index 0000000..27c8830 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobCodeController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogGlueDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.glue.GlueTypeEnum; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.List; + +/** + * job code controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobcode") +public class JobCodeController { + + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, int jobId) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + List jobLogGlues = xxlJobLogGlueDao.findByJobId(jobId); + + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (GlueTypeEnum.BEAN == GlueTypeEnum.match(jobInfo.getGlueType())) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_gluetype_unvalid")); + } + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + + // Glue类型-字典 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); + + model.addAttribute("jobInfo", jobInfo); + model.addAttribute("jobLogGlues", jobLogGlues); + return "jobcode/jobcode.index"; + } + + @RequestMapping("/save") + @ResponseBody + public ReturnT save(Model model, int id, String glueSource, String glueRemark) { + // valid + if (glueRemark==null) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_remark")) ); + } + if (glueRemark.length()<4 || glueRemark.length()>100) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_remark_limit")); + } + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(id); + if (exists_jobInfo == null) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + + // update new code + exists_jobInfo.setGlueSource(glueSource); + exists_jobInfo.setGlueRemark(glueRemark); + exists_jobInfo.setGlueUpdatetime(new Date()); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + // log old code + XxlJobLogGlue xxlJobLogGlue = new XxlJobLogGlue(); + xxlJobLogGlue.setJobId(exists_jobInfo.getId()); + xxlJobLogGlue.setGlueType(exists_jobInfo.getGlueType()); + xxlJobLogGlue.setGlueSource(glueSource); + xxlJobLogGlue.setGlueRemark(glueRemark); + + xxlJobLogGlue.setAddTime(new Date()); + xxlJobLogGlue.setUpdateTime(new Date()); + xxlJobLogGlueDao.save(xxlJobLogGlue); + + // remove code backup more than 30 + xxlJobLogGlueDao.removeOld(exists_jobInfo.getId(), 30); + + return ReturnT.SUCCESS; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobGroupController.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobGroupController.java new file mode 100644 index 0000000..1dedad3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobGroupController.java @@ -0,0 +1,204 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobRegistryDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * job group controller + * @author xuxueli 2016-10-02 20:52:56 + */ +@Controller +@RequestMapping("/jobgroup") +public class JobGroupController { + + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + + @RequestMapping + @PermissionLimit(adminuser = true) + public String index(Model model) { + return "jobgroup/jobgroup.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + @PermissionLimit(adminuser = true) + public Map pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String appname, String title) { + + // page query + List list = xxlJobGroupDao.pageList(start, length, appname, title); + int list_count = xxlJobGroupDao.pageListCount(start, length, appname, title); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/save") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT save(XxlJobGroup xxlJobGroup){ + + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getAppname().contains(">") || xxlJobGroup.getAppname().contains("<")) { + return new ReturnT(500, "AppName"+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getTitle().contains(">") || xxlJobGroup.getTitle().contains("<")) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_title")+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getAddressType()!=0) { + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + if (xxlJobGroup.getAddressList().contains(">") || xxlJobGroup.getAddressList().contains("<")) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList")+I18nUtil.getString("system_unvalid") ); + } + + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.save(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/update") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT update(XxlJobGroup xxlJobGroup){ + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getAddressType() == 0) { + // 0=自动注册 + List registryList = findRegistryByAppName(xxlJobGroup.getAppname()); + String addressListStr = null; + if (registryList!=null && !registryList.isEmpty()) { + Collections.sort(registryList); + addressListStr = ""; + for (String item:registryList) { + addressListStr += item + ","; + } + addressListStr = addressListStr.substring(0, addressListStr.length()-1); + } + xxlJobGroup.setAddressList(addressListStr); + } else { + // 1=手动录入 + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.update(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + private List findRegistryByAppName(String appnameParam){ + HashMap> appAddressMap = new HashMap>(); + List list = xxlJobRegistryDao.findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List registryList = appAddressMap.get(appname); + if (registryList == null) { + registryList = new ArrayList(); + } + + if (!registryList.contains(item.getRegistryValue())) { + registryList.add(item.getRegistryValue()); + } + appAddressMap.put(appname, registryList); + } + } + } + return appAddressMap.get(appnameParam); + } + + @RequestMapping("/remove") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT remove(int id){ + + // valid + int count = xxlJobInfoDao.pageListCount(0, 10, id, -1, null, null, null); + if (count > 0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_del_limit_0") ); + } + + List allList = xxlJobGroupDao.findAll(); + if (allList.size() == 1) { + return new ReturnT(500, I18nUtil.getString("jobgroup_del_limit_1") ); + } + + int ret = xxlJobGroupDao.remove(id); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/loadById") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT loadById(int id){ + XxlJobGroup jobGroup = xxlJobGroupDao.load(id); + return jobGroup!=null?new ReturnT(jobGroup):new ReturnT(ReturnT.FAIL_CODE, null); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobInfoController.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobInfoController.java new file mode 100644 index 0000000..f5cf653 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobInfoController.java @@ -0,0 +1,172 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.*; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobinfo") +public class JobInfoController { + private static Logger logger = LoggerFactory.getLogger(JobInfoController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobService xxlJobService; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "-1") int jobGroup) { + + // 枚举-字典 + model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values()); // 路由策略-列表 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); // Glue类型-字典 + model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values()); // 阻塞处理策略-字典 + model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values()); // 调度类型 + model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values()); // 调度过期策略 + + // 执行器列表 + List jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List jobGroupList = filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + model.addAttribute("jobGroup", jobGroup); + + return "jobinfo/jobinfo.index"; + } + + public static List filterJobGroupByRole(HttpServletRequest request, List jobGroupList_all){ + List jobGroupList = new ArrayList<>(); + if (jobGroupList_all!=null && jobGroupList_all.size()>0) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getRole() == 1) { + jobGroupList = jobGroupList_all; + } else { + List groupIdStrs = new ArrayList<>(); + if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) { + groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(",")); + } + for (XxlJobGroup groupItem:jobGroupList_all) { + if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) { + jobGroupList.add(groupItem); + } + } + } + } + return jobGroupList; + } + public static void validPermission(HttpServletRequest request, int jobGroup) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (!loginUser.validPermission(jobGroup)) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]"); + } + } + + @RequestMapping("/pageList") + @ResponseBody + public Map pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + } + + @RequestMapping("/add") + @ResponseBody + public ReturnT add(XxlJobInfo jobInfo) { + return xxlJobService.add(jobInfo); + } + + @RequestMapping("/update") + @ResponseBody + public ReturnT update(XxlJobInfo jobInfo) { + return xxlJobService.update(jobInfo); + } + + @RequestMapping("/remove") + @ResponseBody + public ReturnT remove(int id) { + return xxlJobService.remove(id); + } + + @RequestMapping("/stop") + @ResponseBody + public ReturnT pause(int id) { + return xxlJobService.stop(id); + } + + @RequestMapping("/start") + @ResponseBody + public ReturnT start(int id) { + return xxlJobService.start(id); + } + + @RequestMapping("/trigger") + @ResponseBody + public ReturnT triggerJob(HttpServletRequest request, int id, String executorParam, String addressList) { + // login user + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + // trigger + return xxlJobService.trigger(loginUser, id, executorParam, addressList); + } + + @RequestMapping("/nextTriggerTime") + @ResponseBody + public ReturnT> nextTriggerTime(String scheduleType, String scheduleConf) { + + XxlJobInfo paramXxlJobInfo = new XxlJobInfo(); + paramXxlJobInfo.setScheduleType(scheduleType); + paramXxlJobInfo.setScheduleConf(scheduleConf); + + List result = new ArrayList<>(); + try { + Date lastTime = new Date(); + for (int i = 0; i < 5; i++) { + lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime); + if (lastTime != null) { + result.add(DateUtil.formatDateTime(lastTime)); + } else { + break; + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage()); + } + return new ReturnT>(result); + + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobLogController.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobLogController.java new file mode 100644 index 0000000..8c4e9d8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/JobLogController.java @@ -0,0 +1,246 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogDao; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.KillParam; +import com.xxl.job.core.biz.model.LogParam; +import com.xxl.job.core.biz.model.LogResult; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.util.HtmlUtils; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/joblog") +public class JobLogController { + private static Logger logger = LoggerFactory.getLogger(JobLogController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "0") Integer jobId) { + + // 执行器列表 + List jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List jobGroupList = JobInfoController.filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + + // 任务 + if (jobId > 0) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_unvalid")); + } + + model.addAttribute("jobInfo", jobInfo); + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + } + + return "joblog/joblog.index"; + } + + @RequestMapping("/getJobsByGroup") + @ResponseBody + public ReturnT> getJobsByGroup(int jobGroup){ + List list = xxlJobInfoDao.getJobsByGroup(jobGroup); + return new ReturnT>(list); + } + + @RequestMapping("/pageList") + @ResponseBody + public Map pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int jobId, int logStatus, String filterTime) { + + // valid permission + JobInfoController.validPermission(request, jobGroup); // 仅管理员支持查询全部;普通用户仅支持查询有权限的 jobGroup + + // parse param + Date triggerTimeStart = null; + Date triggerTimeEnd = null; + if (filterTime!=null && filterTime.trim().length()>0) { + String[] temp = filterTime.split(" - "); + if (temp.length == 2) { + triggerTimeStart = DateUtil.parseDateTime(temp[0]); + triggerTimeEnd = DateUtil.parseDateTime(temp[1]); + } + } + + // page query + List list = xxlJobLogDao.pageList(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + int list_count = xxlJobLogDao.pageListCount(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/logDetailPage") + public String logDetailPage(int id, Model model){ + + // base check + ReturnT logStatue = ReturnT.SUCCESS; + XxlJobLog jobLog = xxlJobLogDao.load(id); + if (jobLog == null) { + throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid")); + } + + model.addAttribute("triggerCode", jobLog.getTriggerCode()); + model.addAttribute("handleCode", jobLog.getHandleCode()); + model.addAttribute("logId", jobLog.getId()); + return "joblog/joblog.detail"; + } + + @RequestMapping("/logDetailCat") + @ResponseBody + public ReturnT logDetailCat(long logId, int fromLineNum){ + try { + // valid + XxlJobLog jobLog = xxlJobLogDao.load(logId); // todo, need to improve performance + if (jobLog == null) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_logid_unvalid")); + } + + // log cat + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(jobLog.getExecutorAddress()); + ReturnT logResult = executorBiz.log(new LogParam(jobLog.getTriggerTime().getTime(), logId, fromLineNum)); + + // is end + if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) { + if (jobLog.getHandleCode() > 0) { + logResult.getContent().setEnd(true); + } + } + + // fix xss + if (logResult.getContent()!=null && StringUtils.hasText(logResult.getContent().getLogContent())) { + String newLogContent = logResult.getContent().getLogContent(); + newLogContent = HtmlUtils.htmlEscape(newLogContent, "UTF-8"); + logResult.getContent().setLogContent(newLogContent); + } + + return logResult; + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, e.getMessage()); + } + } + + @RequestMapping("/logKill") + @ResponseBody + public ReturnT logKill(int id){ + // base check + XxlJobLog log = xxlJobLogDao.load(id); + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(log.getJobId()); + if (jobInfo==null) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (ReturnT.SUCCESS_CODE != log.getTriggerCode()) { + return new ReturnT(500, I18nUtil.getString("joblog_kill_log_limit")); + } + + // request of kill + ReturnT runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(log.getExecutorAddress()); + runResult = executorBiz.kill(new KillParam(jobInfo.getId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + runResult = new ReturnT(500, e.getMessage()); + } + + if (ReturnT.SUCCESS_CODE == runResult.getCode()) { + log.setHandleCode(ReturnT.FAIL_CODE); + log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():"")); + log.setHandleTime(new Date()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + return new ReturnT(runResult.getMsg()); + } else { + return new ReturnT(500, runResult.getMsg()); + } + } + + @RequestMapping("/clearLog") + @ResponseBody + public ReturnT clearLog(int jobGroup, int jobId, int type){ + + Date clearBeforeTime = null; + int clearBeforeNum = 0; + if (type == 1) { + clearBeforeTime = DateUtil.addMonths(new Date(), -1); // 清理一个月之前日志数据 + } else if (type == 2) { + clearBeforeTime = DateUtil.addMonths(new Date(), -3); // 清理三个月之前日志数据 + } else if (type == 3) { + clearBeforeTime = DateUtil.addMonths(new Date(), -6); // 清理六个月之前日志数据 + } else if (type == 4) { + clearBeforeTime = DateUtil.addYears(new Date(), -1); // 清理一年之前日志数据 + } else if (type == 5) { + clearBeforeNum = 1000; // 清理一千条以前日志数据 + } else if (type == 6) { + clearBeforeNum = 10000; // 清理一万条以前日志数据 + } else if (type == 7) { + clearBeforeNum = 30000; // 清理三万条以前日志数据 + } else if (type == 8) { + clearBeforeNum = 100000; // 清理十万条以前日志数据 + } else if (type == 9) { + clearBeforeNum = 0; // 清理所有日志数据 + } else { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_clean_type_unvalid")); + } + + List logIds = null; + do { + logIds = xxlJobLogDao.findClearLogIds(jobGroup, jobId, clearBeforeTime, clearBeforeNum, 1000); + if (logIds!=null && logIds.size()>0) { + xxlJobLogDao.clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + return ReturnT.SUCCESS; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/UserController.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/UserController.java new file mode 100644 index 0000000..13b999c --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/UserController.java @@ -0,0 +1,179 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author xuxueli 2019-05-04 16:39:50 + */ +@Controller +@RequestMapping("/user") +public class UserController { + + @Resource + private XxlJobUserDao xxlJobUserDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + + @RequestMapping + @PermissionLimit(adminuser = true) + public String index(Model model) { + + // 执行器列表 + List groupList = xxlJobGroupDao.findAll(); + model.addAttribute("groupList", groupList); + + return "user/user.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + @PermissionLimit(adminuser = true) + public Map pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String username, int role) { + + // page list + List list = xxlJobUserDao.pageList(start, length, username, role); + int list_count = xxlJobUserDao.pageListCount(start, length, username, role); + + // filter + if (list!=null && list.size()>0) { + for (XxlJobUser item: list) { + item.setPassword(null); + } + } + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/add") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT add(XxlJobUser xxlJobUser) { + + // valid username + if (!StringUtils.hasText(xxlJobUser.getUsername())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") ); + } + xxlJobUser.setUsername(xxlJobUser.getUsername().trim()); + if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // valid password + if (!StringUtils.hasText(xxlJobUser.getPassword())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") ); + } + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + + // check repeat + XxlJobUser existUser = xxlJobUserDao.loadByUserName(xxlJobUser.getUsername()); + if (existUser != null) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") ); + } + + // write + xxlJobUserDao.save(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/update") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT update(HttpServletRequest request, XxlJobUser xxlJobUser) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getUsername().equals(xxlJobUser.getUsername())) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + // valid password + if (StringUtils.hasText(xxlJobUser.getPassword())) { + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + } else { + xxlJobUser.setPassword(null); + } + + // write + xxlJobUserDao.update(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/remove") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT remove(HttpServletRequest request, int id) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getId() == id) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + xxlJobUserDao.delete(id); + return ReturnT.SUCCESS; + } + + @RequestMapping("/updatePwd") + @ResponseBody + public ReturnT updatePwd(HttpServletRequest request, String password){ + + // valid password + if (password==null || password.trim().length()==0){ + return new ReturnT(ReturnT.FAIL.getCode(), "密码不可为空"); + } + password = password.trim(); + if (!(password.length()>=4 && password.length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + + // md5 password + String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); + + // update pwd + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + + // do write + XxlJobUser existUser = xxlJobUserDao.loadByUserName(loginUser.getUsername()); + existUser.setPassword(md5Password); + xxlJobUserDao.update(existUser); + + return ReturnT.SUCCESS; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java new file mode 100644 index 0000000..379efd4 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java @@ -0,0 +1,29 @@ +package com.xxl.job.admin.controller.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 权限限制 + * @author xuxueli 2015-12-12 18:29:02 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PermissionLimit { + + /** + * 登录拦截 (默认拦截) + */ + boolean limit() default true; + + /** + * 要求管理员权限 + * + * @return + */ + boolean adminuser() default false; + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java new file mode 100644 index 0000000..4e79002 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java @@ -0,0 +1,42 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.core.util.FtlUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import java.util.HashMap; + +/** + * push cookies to model as cookieMap + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class CookieInterceptor implements AsyncHandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + + // cookie + if (modelAndView!=null && request.getCookies()!=null && request.getCookies().length>0) { + HashMap cookieMap = new HashMap(); + for (Cookie ck : request.getCookies()) { + cookieMap.put(ck.getName(), ck); + } + modelAndView.addObject("cookieMap", cookieMap); + } + + // static method + if (modelAndView != null) { + modelAndView.addObject("I18nUtil", FtlUtil.generateStaticModel(I18nUtil.class.getName())); + } + + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java new file mode 100644 index 0000000..cebe1fa --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java @@ -0,0 +1,59 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.service.LoginService; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; + + +/** + * 权限拦截 + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class PermissionInterceptor implements AsyncHandlerInterceptor { + + @Resource + private LoginService loginService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if (!(handler instanceof HandlerMethod)) { + return true; // proceed with the next interceptor + } + + // if need login + boolean needLogin = true; + boolean needAdminuser = false; + HandlerMethod method = (HandlerMethod)handler; + PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class); + if (permission!=null) { + needLogin = permission.limit(); + needAdminuser = permission.adminuser(); + } + + if (needLogin) { + XxlJobUser loginUser = loginService.ifLogin(request, response); + if (loginUser == null) { + response.setStatus(302); + response.setHeader("location", request.getContextPath()+"/toLogin"); + return false; + } + if (needAdminuser && loginUser.getRole()!=1) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit")); + } + request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser); + } + + return true; // proceed with the next interceptor + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java new file mode 100644 index 0000000..9ac56c9 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java @@ -0,0 +1,28 @@ +package com.xxl.job.admin.controller.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import jakarta.annotation.Resource; + +/** + * web mvc config + * + * @author xuxueli 2018-04-02 20:48:20 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Resource + private PermissionInterceptor permissionInterceptor; + @Resource + private CookieInterceptor cookieInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(permissionInterceptor).addPathPatterns("/**"); + registry.addInterceptor(cookieInterceptor).addPathPatterns("/**"); + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java new file mode 100644 index 0000000..53d0325 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java @@ -0,0 +1,66 @@ +package com.xxl.job.admin.controller.resolver; + +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.admin.core.util.JacksonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * common exception resolver + * + * @author xuxueli 2016-1-6 19:22:18 + */ +@Component +public class WebExceptionResolver implements HandlerExceptionResolver { + private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class); + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, Object handler, Exception ex) { + + if (!(ex instanceof XxlJobException)) { + logger.error("WebExceptionResolver:{}", ex); + } + + // if json + boolean isJson = false; + if (handler instanceof HandlerMethod) { + HandlerMethod method = (HandlerMethod)handler; + ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class); + if (responseBody != null) { + isJson = true; + } + } + + // error result + ReturnT errorResult = new ReturnT(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "
")); + + // response + ModelAndView mv = new ModelAndView(); + if (isJson) { + try { + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JacksonUtil.writeValueAsString(errorResult)); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return mv; + } else { + + mv.addObject("exceptionMsg", errorResult.getMsg()); + mv.setViewName("/common/common.exception"); + return mv; + } + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java new file mode 100644 index 0000000..4165ff3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java @@ -0,0 +1,20 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; + +/** + * @author xuxueli 2020-01-19 + */ +public interface JobAlarm { + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java new file mode 100644 index 0000000..797dc90 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java @@ -0,0 +1,65 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class JobAlarmer implements ApplicationContextAware, InitializingBean { + private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class); + + private ApplicationContext applicationContext; + private List jobAlarmList; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + Map serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class); + if (serviceBeanMap != null && serviceBeanMap.size() > 0) { + jobAlarmList = new ArrayList(serviceBeanMap.values()); + } + } + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) { + + boolean result = false; + if (jobAlarmList!=null && jobAlarmList.size()>0) { + result = true; // success means all-success + for (JobAlarm alarm: jobAlarmList) { + boolean resultItem = false; + try { + resultItem = alarm.doAlarm(info, jobLog); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + if (!resultItem) { + result = false; + } + } + } + + return result; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java new file mode 100644 index 0000000..2610c65 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java @@ -0,0 +1,118 @@ +package com.xxl.job.admin.core.alarm.impl; + +import com.xxl.job.admin.core.alarm.JobAlarm; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import jakarta.mail.internet.MimeMessage; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * job alarm by email + * + * @author xuxueli 2020-01-19 + */ +@Component +public class EmailJobAlarm implements JobAlarm { + private static Logger logger = LoggerFactory.getLogger(EmailJobAlarm.class); + + /** + * fail alarm + * + * @param jobLog + */ + @Override + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog){ + boolean alarmResult = true; + + // send monitor email + if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) { + + // alarmContent + String alarmContent = "Alarm Job LogId=" + jobLog.getId(); + if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "
TriggerMsg=
" + jobLog.getTriggerMsg(); + } + if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "
HandleCode=" + jobLog.getHandleMsg(); + } + + // email info + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(Integer.valueOf(info.getJobGroup())); + String personal = I18nUtil.getString("admin_name_full"); + String title = I18nUtil.getString("jobconf_monitor"); + String content = MessageFormat.format(loadEmailJobAlarmTemplate(), + group!=null?group.getTitle():"null", + info.getId(), + info.getJobDesc(), + alarmContent); + + Set emailSet = new HashSet(Arrays.asList(info.getAlarmEmail().split(","))); + for (String email: emailSet) { + + // make mail + try { + MimeMessage mimeMessage = XxlJobAdminConfig.getAdminConfig().getMailSender().createMimeMessage(); + + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); + helper.setFrom(XxlJobAdminConfig.getAdminConfig().getEmailFrom(), personal); + helper.setTo(email); + helper.setSubject(title); + helper.setText(content, true); + + XxlJobAdminConfig.getAdminConfig().getMailSender().send(mimeMessage); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:{}", jobLog.getId(), e); + + alarmResult = false; + } + + } + } + + return alarmResult; + } + + /** + * load email job alarm template + * + * @return + */ + private static final String loadEmailJobAlarmTemplate(){ + String mailBodyTemplate = "
" + I18nUtil.getString("jobconf_monitor_detail") + ":" + + "\n" + + " " + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
"+ I18nUtil.getString("jobinfo_field_jobgroup") +""+ I18nUtil.getString("jobinfo_field_id") +""+ I18nUtil.getString("jobinfo_field_jobdesc") +""+ I18nUtil.getString("jobconf_monitor_alarm_title") +""+ I18nUtil.getString("jobconf_monitor_alarm_content") +"
{0}{1}{2}"+ I18nUtil.getString("jobconf_monitor_alarm_type") +"{3}
"; + + return mailBodyTemplate; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java new file mode 100644 index 0000000..279ad7d --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java @@ -0,0 +1,99 @@ +package com.xxl.job.admin.core.complete; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.context.XxlJobContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.MessageFormat; + +/** + * @author xuxueli 2020-10-30 20:43:10 + */ +public class XxlJobCompleter { + private static Logger logger = LoggerFactory.getLogger(XxlJobCompleter.class); + + /** + * common fresh handle entrance (limit only once) + * + * @param xxlJobLog + * @return + */ + public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) { + + // finish + finishJob(xxlJobLog); + + // text最大64kb 避免长度过长 + if (xxlJobLog.getHandleMsg().length() > 15000) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) ); + } + + // fresh handle + return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog); + } + + + /** + * do somethind to finish job + */ + private static void finishJob(XxlJobLog xxlJobLog){ + + // 1、handle success, to trigger child job + String triggerChildMsg = null; + if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) { + XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(xxlJobLog.getJobId()); + if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) { + triggerChildMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<<
"; + + String[] childJobIds = xxlJobInfo.getChildJobId().split(","); + for (int i = 0; i < childJobIds.length; i++) { + int childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Integer.valueOf(childJobIds[i]):-1; + if (childJobId > 0) { + + JobTriggerPoolHelper.trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null); + ReturnT triggerChildResult = ReturnT.SUCCESS; + + // add msg + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"), + (i+1), + childJobIds.length, + childJobIds[i], + (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")), + triggerChildResult.getMsg()); + } else { + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"), + (i+1), + childJobIds.length, + childJobIds[i]); + } + } + + } + } + + if (triggerChildMsg != null) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg ); + } + + // 2、fix_delay trigger next + // on the way + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java new file mode 100644 index 0000000..cde335f --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java @@ -0,0 +1,158 @@ +package com.xxl.job.admin.core.conf; + +import com.xxl.job.admin.core.alarm.JobAlarmer; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.dao.*; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import javax.sql.DataSource; +import java.util.Arrays; + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ + +@Component +public class XxlJobAdminConfig implements InitializingBean, DisposableBean { + + private static XxlJobAdminConfig adminConfig = null; + public static XxlJobAdminConfig getAdminConfig() { + return adminConfig; + } + + + // ---------------------- XxlJobScheduler ---------------------- + + private XxlJobScheduler xxlJobScheduler; + + @Override + public void afterPropertiesSet() throws Exception { + adminConfig = this; + + xxlJobScheduler = new XxlJobScheduler(); + xxlJobScheduler.init(); + } + + @Override + public void destroy() throws Exception { + xxlJobScheduler.destroy(); + } + + + // ---------------------- XxlJobScheduler ---------------------- + + // conf + @Value("${xxl.job.i18n}") + private String i18n; + + @Value("${xxl.job.accessToken}") + private String accessToken; + + @Value("${spring.mail.from}") + private String emailFrom; + + @Value("${xxl.job.triggerpool.fast.max}") + private int triggerPoolFastMax; + + @Value("${xxl.job.triggerpool.slow.max}") + private int triggerPoolSlowMax; + + @Value("${xxl.job.logretentiondays}") + private int logretentiondays; + + // dao, service + + @Resource + private XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + @Resource + private JavaMailSender mailSender; + @Resource + private DataSource dataSource; + @Resource + private JobAlarmer jobAlarmer; + + + public String getI18n() { + if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) { + return "zh_CN"; + } + return i18n; + } + + public String getAccessToken() { + return accessToken; + } + + public String getEmailFrom() { + return emailFrom; + } + + public int getTriggerPoolFastMax() { + if (triggerPoolFastMax < 200) { + return 200; + } + return triggerPoolFastMax; + } + + public int getTriggerPoolSlowMax() { + if (triggerPoolSlowMax < 100) { + return 100; + } + return triggerPoolSlowMax; + } + + public int getLogretentiondays() { + if (logretentiondays < 7) { + return -1; // Limit greater than or equal to 7, otherwise close + } + return logretentiondays; + } + + public XxlJobLogDao getXxlJobLogDao() { + return xxlJobLogDao; + } + + public XxlJobInfoDao getXxlJobInfoDao() { + return xxlJobInfoDao; + } + + public XxlJobRegistryDao getXxlJobRegistryDao() { + return xxlJobRegistryDao; + } + + public XxlJobGroupDao getXxlJobGroupDao() { + return xxlJobGroupDao; + } + + public XxlJobLogReportDao getXxlJobLogReportDao() { + return xxlJobLogReportDao; + } + + public JavaMailSender getMailSender() { + return mailSender; + } + + public DataSource getDataSource() { + return dataSource; + } + + public JobAlarmer getJobAlarmer() { + return jobAlarmer; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java new file mode 100644 index 0000000..de33db0 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java @@ -0,0 +1,1666 @@ +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ + +package com.xxl.job.admin.core.cron; + +import java.io.Serializable; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.TreeSet; + +/** + * Provides a parser and evaluator for unix-like cron expressions. Cron + * expressions provide the ability to specify complex time combinations such as + * "At 8:00am every Monday through Friday" or "At 1:30am every + * last Friday of the month". + *

+ * Cron expressions are comprised of 6 required fields and one optional field + * separated by white space. The fields respectively are described as follows: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Field Name Allowed Values Allowed Special Characters
Seconds  + * 0-59  + * , - * /
Minutes  + * 0-59  + * , - * /
Hours  + * 0-23  + * , - * /
Day-of-month  + * 1-31  + * , - * ? / L W
Month  + * 0-11 or JAN-DEC  + * , - * /
Day-of-Week  + * 1-7 or SUN-SAT  + * , - * ? / L #
Year (Optional)  + * empty, 1970-2199  + * , - * /
+ *

+ * The '*' character is used to specify all values. For example, "*" + * in the minute field means "every minute". + *

+ * The '?' character is allowed for the day-of-month and day-of-week fields. It + * is used to specify 'no specific value'. This is useful when you need to + * specify something in one of the two fields, but not the other. + *

+ * The '-' character is used to specify ranges For example "10-12" in + * the hour field means "the hours 10, 11 and 12". + *

+ * The ',' character is used to specify additional values. For example + * "MON,WED,FRI" in the day-of-week field means "the days Monday, + * Wednesday, and Friday". + *

+ * The '/' character is used to specify increments. For example "0/15" + * in the seconds field means "the seconds 0, 15, 30, and 45". And + * "5/15" in the seconds field means "the seconds 5, 20, 35, and + * 50". Specifying '*' before the '/' is equivalent to specifying 0 is + * the value to start with. Essentially, for each field in the expression, there + * is a set of numbers that can be turned on or off. For seconds and minutes, + * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to + * 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn + * on every "nth" value in the given set. Thus "7/6" in the + * month field only turns on month "7", it does NOT mean every 6th + * month, please note that subtlety. + *

+ * The 'L' character is allowed for the day-of-month and day-of-week fields. + * This character is short-hand for "last", but it has different + * meaning in each of the two fields. For example, the value "L" in + * the day-of-month field means "the last day of the month" - day 31 + * for January, day 28 for February on non-leap years. If used in the + * day-of-week field by itself, it simply means "7" or + * "SAT". But if used in the day-of-week field after another value, it + * means "the last xxx day of the month" - for example "6L" + * means "the last friday of the month". You can also specify an offset + * from the last day of the month, such as "L-3" which would mean the third-to-last + * day of the calendar month. When using the 'L' option, it is important not to + * specify lists, or ranges of values, as you'll get confusing/unexpected results. + *

+ * The 'W' character is allowed for the day-of-month field. This character + * is used to specify the weekday (Monday-Friday) nearest the given day. As an + * example, if you were to specify "15W" as the value for the + * day-of-month field, the meaning is: "the nearest weekday to the 15th of + * the month". So if the 15th is a Saturday, the trigger will fire on + * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the + * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. + * However if you specify "1W" as the value for day-of-month, and the + * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not + * 'jump' over the boundary of a month's days. The 'W' character can only be + * specified when the day-of-month is a single day, not a range or list of days. + *

+ * The 'L' and 'W' characters can also be combined for the day-of-month + * expression to yield 'LW', which translates to "last weekday of the + * month". + *

+ * The '#' character is allowed for the day-of-week field. This character is + * used to specify "the nth" XXX day of the month. For example, the + * value of "6#3" in the day-of-week field means the third Friday of + * the month (day 6 = Friday and "#3" = the 3rd one in the month). + * Other examples: "2#1" = the first Monday of the month and + * "4#5" = the fifth Wednesday of the month. Note that if you specify + * "#5" and there is not 5 of the given day-of-week in the month, then + * no firing will occur that month. If the '#' character is used, there can + * only be one expression in the day-of-week field ("3#1,6#3" is + * not valid, since there are two expressions). + *

+ * + *

+ * The legal characters and the names of months and days of the week are not + * case sensitive. + * + *

+ * NOTES: + *

    + *
  • Support for specifying both a day-of-week and a day-of-month value is + * not complete (you'll need to use the '?' character in one of these fields). + *
  • + *
  • Overflowing ranges is supported - that is, having a larger number on + * the left hand side than the right. You might do 22-2 to catch 10 o'clock + * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is + * very important to note that overuse of overflowing ranges creates ranges + * that don't make sense and no effort has been made to determine which + * interpretation CronExpression chooses. An example would be + * "0 0 14-6 ? * FRI-MON".
  • + *
+ *

+ * + * + * @author Sharada Jambula, James House + * @author Contributions from Mads Henderson + * @author Refactoring from CronTrigger to CronExpression by Aaron Craven + * + * Borrowed from quartz v2.3.1 + * + */ +public final class CronExpression implements Serializable, Cloneable { + + private static final long serialVersionUID = 12423409423L; + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map monthMap = new HashMap(20); + protected static final Map dayMap = new HashMap(60); + static { + monthMap.put("JAN", 0); + monthMap.put("FEB", 1); + monthMap.put("MAR", 2); + monthMap.put("APR", 3); + monthMap.put("MAY", 4); + monthMap.put("JUN", 5); + monthMap.put("JUL", 6); + monthMap.put("AUG", 7); + monthMap.put("SEP", 8); + monthMap.put("OCT", 9); + monthMap.put("NOV", 10); + monthMap.put("DEC", 11); + + dayMap.put("SUN", 1); + dayMap.put("MON", 2); + dayMap.put("TUE", 3); + dayMap.put("WED", 4); + dayMap.put("THU", 5); + dayMap.put("FRI", 6); + dayMap.put("SAT", 7); + } + + private final String cronExpression; + private TimeZone timeZone = null; + protected transient TreeSet seconds; + protected transient TreeSet minutes; + protected transient TreeSet hours; + protected transient TreeSet daysOfMonth; + protected transient TreeSet months; + protected transient TreeSet daysOfWeek; + protected transient TreeSet years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + + /** + * Constructs a new CronExpression based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws ParseException + * if the string expression cannot be parsed into a valid + * CronExpression + */ + public CronExpression(String cronExpression) throws ParseException { + if (cronExpression == null) { + throw new IllegalArgumentException("cronExpression cannot be null"); + } + + this.cronExpression = cronExpression.toUpperCase(Locale.US); + + buildExpression(this.cronExpression); + } + + /** + * Constructs a new {@code CronExpression} as a copy of an existing + * instance. + * + * @param expression + * The existing cron expression to be copied + */ + public CronExpression(CronExpression expression) { + /* + * We don't call the other constructor here since we need to swallow the + * ParseException. We also elide some of the sanity checking as it is + * not logically trippable. + */ + this.cronExpression = expression.getCronExpression(); + try { + buildExpression(cronExpression); + } catch (ParseException ex) { + throw new AssertionError(); + } + if (expression.getTimeZone() != null) { + setTimeZone((TimeZone) expression.getTimeZone().clone()); + } + } + + /** + * Indicates whether the given date satisfies the cron expression. Note that + * milliseconds are ignored, so two Dates falling on different milliseconds + * of the same second will always have the same result here. + * + * @param date the date to evaluate + * @return a boolean indicating whether the given date satisfies the cron + * expression + */ + public boolean isSatisfiedBy(Date date) { + Calendar testDateCal = Calendar.getInstance(getTimeZone()); + testDateCal.setTime(date); + testDateCal.set(Calendar.MILLISECOND, 0); + Date originalDate = testDateCal.getTime(); + + testDateCal.add(Calendar.SECOND, -1); + + Date timeAfter = getTimeAfter(testDateCal.getTime()); + + return ((timeAfter != null) && (timeAfter.equals(originalDate))); + } + + /** + * Returns the next date/time after the given date/time which + * satisfies the cron expression. + * + * @param date the date/time at which to begin the search for the next valid + * date/time + * @return the next valid date/time + */ + public Date getNextValidTimeAfter(Date date) { + return getTimeAfter(date); + } + + /** + * Returns the next date/time after the given date/time which does + * not satisfy the expression + * + * @param date the date/time at which to begin the search for the next + * invalid date/time + * @return the next valid date/time + */ + public Date getNextInvalidTimeAfter(Date date) { + long difference = 1000; + + //move back to the nearest second so differences will be accurate + Calendar adjustCal = Calendar.getInstance(getTimeZone()); + adjustCal.setTime(date); + adjustCal.set(Calendar.MILLISECOND, 0); + Date lastDate = adjustCal.getTime(); + + Date newDate; + + //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. + + //keep getting the next included time until it's farther than one second + // apart. At that point, lastDate is the last valid fire time. We return + // the second immediately following it. + while (difference == 1000) { + newDate = getTimeAfter(lastDate); + if(newDate == null) + break; + + difference = newDate.getTime() - lastDate.getTime(); + + if (difference == 1000) { + lastDate = newDate; + } + } + + return new Date(lastDate.getTime() + 1000); + } + + /** + * Returns the time zone for which this CronExpression + * will be resolved. + */ + public TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + + return timeZone; + } + + /** + * Sets the time zone for which this CronExpression + * will be resolved. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Returns the string representation of the CronExpression + * + * @return a string representation of the CronExpression + */ + @Override + public String toString() { + return cronExpression; + } + + /** + * Indicates whether the specified cron expression can be parsed into a + * valid cron expression + * + * @param cronExpression the expression to evaluate + * @return a boolean indicating whether the given expression is a valid cron + * expression + */ + public static boolean isValidExpression(String cronExpression) { + + try { + new CronExpression(cronExpression); + } catch (ParseException pe) { + return false; + } + + return true; + } + + public static void validateExpression(String cronExpression) throws ParseException { + + new CronExpression(cronExpression); + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Expression Parsing Functions + // + //////////////////////////////////////////////////////////////////////////// + + protected void buildExpression(String expression) throws ParseException { + expressionParsed = true; + + try { + + if (seconds == null) { + seconds = new TreeSet(); + } + if (minutes == null) { + minutes = new TreeSet(); + } + if (hours == null) { + hours = new TreeSet(); + } + if (daysOfMonth == null) { + daysOfMonth = new TreeSet(); + } + if (months == null) { + months = new TreeSet(); + } + if (daysOfWeek == null) { + daysOfWeek = new TreeSet(); + } + if (years == null) { + years = new TreeSet(); + } + + int exprOn = SECOND; + + StringTokenizer exprsTok = new StringTokenizer(expression, " \t", + false); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + TreeSet dow = getSet(DAY_OF_WEEK); + TreeSet dom = getSet(DAY_OF_MONTH); + + // Copying the logic from the UnsupportedOperationException below + boolean dayOfMSpec = !dom.contains(NO_SPEC); + boolean dayOfWSpec = !dow.contains(NO_SPEC); + + if (!dayOfMSpec || dayOfWSpec) { + if (!dayOfWSpec || dayOfMSpec) { + throw new ParseException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); + } + } + } catch (ParseException pe) { + throw pe; + } catch (Exception e) { + throw new ParseException("Illegal cron expression format (" + + e.toString() + ")", 0); + } + } + + protected int storeExpressionVals(int pos, String s, int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specified for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + checkIncrementRange(incr, type, i); + } else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if(type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if(c == '-') { + ValueSet vs = getValue(0, s, i+1); + lastdayOffset = vs.value; + if(lastdayOffset > 30) + throw new ParseException("Offset from last day must be <= 30", i+1); + i = vs.pos; + } + if(s.length() > i) { + c = s.charAt(i); + if(c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, idxPos); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, idxPos); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, idxPos); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, idxPos); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, idxPos); + } + } + + protected int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if(val < 1 || val > 7) + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if(val > 31) + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { + throw new ParseException("'/' must be followed by an integer.", i); + } + + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + checkIncrementRange(v2, type, i); + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + checkIncrementRange(v3, type, i); + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.Set set) { + + if (set.contains(NO_SPEC)) { + return "?"; + } + if (set.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = set.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.ArrayList list) { + + if (list.contains(NO_SPEC)) { + return "?"; + } + if (list.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = list.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected int skipWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { + } + + return i; + } + + protected int findNextWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { + } + + return i; + } + + protected void addToSet(int val, int end, int incr, int type) + throws ParseException { + + TreeSet set = getSet(type); + + if (type == SECOND || type == MINUTE) { + if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Minute and Second values must be between 0 and 59", + -1); + } + } else if (type == HOUR) { + if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Hour values must be between 0 and 23", -1); + } + } else if (type == DAY_OF_MONTH) { + if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day of month values must be between 1 and 31", -1); + } + } else if (type == MONTH) { + if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Month values must be between 1 and 12", -1); + } + } else if (type == DAY_OF_WEEK) { + if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day-of-Week values must be between 1 and 7", -1); + } + } + + if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { + if (val != -1) { + set.add(val); + } else { + set.add(NO_SPEC); + } + + return; + } + + int startAt = val; + int stopAt = end; + + if (val == ALL_SPEC_INT && incr <= 0) { + incr = 1; + set.add(ALL_SPEC); // put in a marker, but also fill values + } + + if (type == SECOND || type == MINUTE) { + if (stopAt == -1) { + stopAt = 59; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == HOUR) { + if (stopAt == -1) { + stopAt = 23; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == DAY_OF_MONTH) { + if (stopAt == -1) { + stopAt = 31; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == MONTH) { + if (stopAt == -1) { + stopAt = 12; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == DAY_OF_WEEK) { + if (stopAt == -1) { + stopAt = 7; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == YEAR) { + if (stopAt == -1) { + stopAt = MAX_YEAR; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1970; + } + } + + // if the end of the range is before the start, then we need to overflow into + // the next day, month etc. This is done by adding the maximum amount for that + // type, and using modulus max to determine the value being added. + int max = -1; + if (stopAt < startAt) { + switch (type) { + case SECOND : max = 60; break; + case MINUTE : max = 60; break; + case HOUR : max = 24; break; + case MONTH : max = 12; break; + case DAY_OF_WEEK : max = 7; break; + case DAY_OF_MONTH : max = 31; break; + case YEAR : throw new IllegalArgumentException("Start year must be less than stop year"); + default : throw new IllegalArgumentException("Unexpected type encountered"); + } + stopAt += max; + } + + for (int i = startAt; i <= stopAt; i += incr) { + if (max == -1) { + // ie: there's no max to overflow over + set.add(i); + } else { + // take the modulus to get the real value + int i2 = i % max; + + // 1-indexed ranges should not include 0, and should include their max + if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { + i2 = max; + } + + set.add(i2); + } + } + } + + TreeSet getSet(int type) { + switch (type) { + case SECOND: + return seconds; + case MINUTE: + return minutes; + case HOUR: + return hours; + case DAY_OF_MONTH: + return daysOfMonth; + case MONTH: + return months; + case DAY_OF_WEEK: + return daysOfWeek; + case YEAR: + return years; + default: + return null; + } + } + + protected ValueSet getValue(int v, String s, int i) { + char c = s.charAt(i); + StringBuilder s1 = new StringBuilder(String.valueOf(v)); + while (c >= '0' && c <= '9') { + s1.append(c); + i++; + if (i >= s.length()) { + break; + } + c = s.charAt(i); + } + ValueSet val = new ValueSet(); + + val.pos = (i < s.length()) ? i : i + 1; + val.value = Integer.parseInt(s1.toString()); + return val; + } + + protected int getNumericValue(String s, int i) { + int endOfVal = findNextWhiteSpace(i, s); + String val = s.substring(i, endOfVal); + return Integer.parseInt(val); + } + + protected int getMonthNumber(String s) { + Integer integer = monthMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + protected int getDayOfWeekNumber(String s) { + Integer integer = dayMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Computation Functions + // + //////////////////////////////////////////////////////////////////////////// + + public Date getTimeAfter(Date afterTime) { + + // Computation is based on Gregorian year only. + Calendar cl = new java.util.GregorianCalendar(getTimeZone()); + + // move ahead one second, since we're computing the time *after* the + // given time + afterTime = new Date(afterTime.getTime() + 1000); + // CronTrigger does not deal with milliseconds + cl.setTime(afterTime); + cl.set(Calendar.MILLISECOND, 0); + + boolean gotOne = false; + // loop until we've computed the next time, or we've past the endTime + while (!gotOne) { + + //if (endTime != null && cl.getTime().after(endTime)) return null; + if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... + return null; + } + + SortedSet st = null; + int t = 0; + + int sec = cl.get(Calendar.SECOND); + int min = cl.get(Calendar.MINUTE); + + // get second................................................. + st = seconds.tailSet(sec); + if (st != null && st.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if(!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if(t > day) { + mon++; + if(mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } else if(nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(Calendar cal, int hour) { + cal.set(Calendar.HOUR_OF_DAY, hour); + if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(Calendar.HOUR_OF_DAY, hour + 1); + } + } + + /** + * NOT YET IMPLEMENTED: Returns the time before the given time + * that the CronExpression matches. + */ + public Date getTimeBefore(Date endTime) { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + /** + * NOT YET IMPLEMENTED: Returns the final time that the + * CronExpression will match. + */ + public Date getFinalFireTime() { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + protected boolean isLeapYear(int year) { + return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); + } + + protected int getLastDayOfMonth(int monthNum, int year) { + + switch (monthNum) { + case 1: + return 31; + case 2: + return (isLeapYear(year)) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + throw new IllegalArgumentException("Illegal month number: " + + monthNum); + } + } + + + private void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +class ValueSet { + public int value; + + public int pos; +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java new file mode 100644 index 0000000..faa6063 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java @@ -0,0 +1,14 @@ +package com.xxl.job.admin.core.exception; + +/** + * @author xuxueli 2019-05-04 23:19:29 + */ +public class XxlJobException extends RuntimeException { + + public XxlJobException() { + } + public XxlJobException(String message) { + super(message); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java new file mode 100644 index 0000000..dde4b39 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java @@ -0,0 +1,77 @@ +package com.xxl.job.admin.core.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobGroup { + + private int id; + private String appname; + private String title; + private int addressType; // 执行器地址类型:0=自动注册、1=手动录入 + private String addressList; // 执行器地址列表,多地址逗号分隔(手动录入) + private Date updateTime; + + // registry list + private List registryList; // 执行器地址列表(系统注册) + public List getRegistryList() { + if (addressList!=null && addressList.trim().length()>0) { + registryList = new ArrayList(Arrays.asList(addressList.split(","))); + } + return registryList; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getAppname() { + return appname; + } + + public void setAppname(String appname) { + this.appname = appname; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getAddressType() { + return addressType; + } + + public void setAddressType(int addressType) { + this.addressType = addressType; + } + + public String getAddressList() { + return addressList; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public void setAddressList(String addressList) { + this.addressList = addressList; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java new file mode 100644 index 0000000..e47b6dc --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java @@ -0,0 +1,237 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job info + * + * @author xuxueli 2016-1-12 18:25:49 + */ +public class XxlJobInfo { + + private int id; // 主键ID + + private int jobGroup; // 执行器主键ID + private String jobDesc; + + private Date addTime; + private Date updateTime; + + private String author; // 负责人 + private String alarmEmail; // 报警邮件 + + private String scheduleType; // 调度类型 + private String scheduleConf; // 调度配置,值含义取决于调度类型 + private String misfireStrategy; // 调度过期策略 + + private String executorRouteStrategy; // 执行器路由策略 + private String executorHandler; // 执行器,任务Handler名称 + private String executorParam; // 执行器,任务参数 + private String executorBlockStrategy; // 阻塞处理策略 + private int executorTimeout; // 任务执行超时时间,单位秒 + private int executorFailRetryCount; // 失败重试次数 + + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; // GLUE源代码 + private String glueRemark; // GLUE备注 + private Date glueUpdatetime; // GLUE更新时间 + + private String childJobId; // 子任务ID,多个逗号分隔 + + private int triggerStatus; // 调度状态:0-停止,1-运行 + private long triggerLastTime; // 上次调度时间 + private long triggerNextTime; // 下次调度时间 + + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public String getJobDesc() { + return jobDesc; + } + + public void setJobDesc(String jobDesc) { + this.jobDesc = jobDesc; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getAlarmEmail() { + return alarmEmail; + } + + public void setAlarmEmail(String alarmEmail) { + this.alarmEmail = alarmEmail; + } + + public String getScheduleType() { + return scheduleType; + } + + public void setScheduleType(String scheduleType) { + this.scheduleType = scheduleType; + } + + public String getScheduleConf() { + return scheduleConf; + } + + public void setScheduleConf(String scheduleConf) { + this.scheduleConf = scheduleConf; + } + + public String getMisfireStrategy() { + return misfireStrategy; + } + + public void setMisfireStrategy(String misfireStrategy) { + this.misfireStrategy = misfireStrategy; + } + + public String getExecutorRouteStrategy() { + return executorRouteStrategy; + } + + public void setExecutorRouteStrategy(String executorRouteStrategy) { + this.executorRouteStrategy = executorRouteStrategy; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorBlockStrategy() { + return executorBlockStrategy; + } + + public void setExecutorBlockStrategy(String executorBlockStrategy) { + this.executorBlockStrategy = executorBlockStrategy; + } + + public int getExecutorTimeout() { + return executorTimeout; + } + + public void setExecutorTimeout(int executorTimeout) { + this.executorTimeout = executorTimeout; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getGlueUpdatetime() { + return glueUpdatetime; + } + + public void setGlueUpdatetime(Date glueUpdatetime) { + this.glueUpdatetime = glueUpdatetime; + } + + public String getChildJobId() { + return childJobId; + } + + public void setChildJobId(String childJobId) { + this.childJobId = childJobId; + } + + public int getTriggerStatus() { + return triggerStatus; + } + + public void setTriggerStatus(int triggerStatus) { + this.triggerStatus = triggerStatus; + } + + public long getTriggerLastTime() { + return triggerLastTime; + } + + public void setTriggerLastTime(long triggerLastTime) { + this.triggerLastTime = triggerLastTime; + } + + public long getTriggerNextTime() { + return triggerNextTime; + } + + public void setTriggerNextTime(long triggerNextTime) { + this.triggerNextTime = triggerNextTime; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java new file mode 100644 index 0000000..7d3072a --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java @@ -0,0 +1,157 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log, used to track trigger process + * @author xuxueli 2015-12-19 23:19:09 + */ +public class XxlJobLog { + + private long id; + + // job info + private int jobGroup; + private int jobId; + + // execute info + private String executorAddress; + private String executorHandler; + private String executorParam; + private String executorShardingParam; + private int executorFailRetryCount; + + // trigger info + private Date triggerTime; + private int triggerCode; + private String triggerMsg; + + // handle info + private Date handleTime; + private int handleCode; + private String handleMsg; + + // alarm info + private int alarmStatus; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getExecutorAddress() { + return executorAddress; + } + + public void setExecutorAddress(String executorAddress) { + this.executorAddress = executorAddress; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorShardingParam() { + return executorShardingParam; + } + + public void setExecutorShardingParam(String executorShardingParam) { + this.executorShardingParam = executorShardingParam; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public Date getTriggerTime() { + return triggerTime; + } + + public void setTriggerTime(Date triggerTime) { + this.triggerTime = triggerTime; + } + + public int getTriggerCode() { + return triggerCode; + } + + public void setTriggerCode(int triggerCode) { + this.triggerCode = triggerCode; + } + + public String getTriggerMsg() { + return triggerMsg; + } + + public void setTriggerMsg(String triggerMsg) { + this.triggerMsg = triggerMsg; + } + + public Date getHandleTime() { + return handleTime; + } + + public void setHandleTime(Date handleTime) { + this.handleTime = handleTime; + } + + public int getHandleCode() { + return handleCode; + } + + public void setHandleCode(int handleCode) { + this.handleCode = handleCode; + } + + public String getHandleMsg() { + return handleMsg; + } + + public void setHandleMsg(String handleMsg) { + this.handleMsg = handleMsg; + } + + public int getAlarmStatus() { + return alarmStatus; + } + + public void setAlarmStatus(int alarmStatus) { + this.alarmStatus = alarmStatus; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java new file mode 100644 index 0000000..2f59ffa --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java @@ -0,0 +1,75 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log for glue, used to track job code process + * @author xuxueli 2016-5-19 17:57:46 + */ +public class XxlJobLogGlue { + + private int id; + private int jobId; // 任务主键ID + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; + private String glueRemark; + private Date addTime; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java new file mode 100644 index 0000000..e58ff1a --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java @@ -0,0 +1,54 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +public class XxlJobLogReport { + + private int id; + + private Date triggerDay; + + private int runningCount; + private int sucCount; + private int failCount; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Date getTriggerDay() { + return triggerDay; + } + + public void setTriggerDay(Date triggerDay) { + this.triggerDay = triggerDay; + } + + public int getRunningCount() { + return runningCount; + } + + public void setRunningCount(int runningCount) { + this.runningCount = runningCount; + } + + public int getSucCount() { + return sucCount; + } + + public void setSucCount(int sucCount) { + this.sucCount = sucCount; + } + + public int getFailCount() { + return failCount; + } + + public void setFailCount(int failCount) { + this.failCount = failCount; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java new file mode 100644 index 0000000..924d6d3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java @@ -0,0 +1,55 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobRegistry { + + private int id; + private String registryGroup; + private String registryKey; + private String registryValue; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getRegistryGroup() { + return registryGroup; + } + + public void setRegistryGroup(String registryGroup) { + this.registryGroup = registryGroup; + } + + public String getRegistryKey() { + return registryKey; + } + + public void setRegistryKey(String registryKey) { + this.registryKey = registryKey; + } + + public String getRegistryValue() { + return registryValue; + } + + public void setRegistryValue(String registryValue) { + this.registryValue = registryValue; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java new file mode 100644 index 0000000..db17327 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java @@ -0,0 +1,73 @@ +package com.xxl.job.admin.core.model; + +import org.springframework.util.StringUtils; + +/** + * @author xuxueli 2019-05-04 16:43:12 + */ +public class XxlJobUser { + + private int id; + private String username; // 账号 + private String password; // 密码 + private int role; // 角色:0-普通用户、1-管理员 + private String permission; // 权限:执行器ID列表,多个逗号分割 + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getRole() { + return role; + } + + public void setRole(int role) { + this.role = role; + } + + public String getPermission() { + return permission; + } + + public void setPermission(String permission) { + this.permission = permission; + } + + // plugin + public boolean validPermission(int jobGroup){ + if (this.role == 1) { + return true; + } else { + if (StringUtils.hasText(this.permission)) { + for (String permissionItem : this.permission.split(",")) { + if (String.valueOf(jobGroup).equals(permissionItem)) { + return true; + } + } + } + return false; + } + + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java new file mode 100644 index 0000000..7fff93a --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.admin.core.route.strategy.*; +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * Created by xuxueli on 17/3/10. + */ +public enum ExecutorRouteStrategyEnum { + + FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()), + LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()), + ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()), + RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()), + CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()), + LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()), + LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()), + FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()), + BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()), + SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null); + + ExecutorRouteStrategyEnum(String title, ExecutorRouter router) { + this.title = title; + this.router = router; + } + + private String title; + private ExecutorRouter router; + + public String getTitle() { + return title; + } + public ExecutorRouter getRouter() { + return router; + } + + public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){ + if (name != null) { + for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + } + return defaultItem; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java new file mode 100644 index 0000000..5de9a1d --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public abstract class ExecutorRouter { + protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class); + + /** + * route address + * + * @param addressList + * @return ReturnT.content=address + */ + public abstract ReturnT route(TriggerParam triggerParam, List addressList); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java new file mode 100644 index 0000000..868560f --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.IdleBeatParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteBusyover extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + StringBuffer idleBeatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT idleBeatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + idleBeatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e ); + } + idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"

":"") + .append(I18nUtil.getString("jobconf_idleBeat") + ":") + .append("
address:").append(address) + .append("
code:").append(idleBeatResult.getCode()) + .append("
msg:").append(idleBeatResult.getMsg()); + + // beat success + if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) { + idleBeatResult.setMsg(idleBeatResultSB.toString()); + idleBeatResult.setContent(address); + return idleBeatResult; + } + } + + return new ReturnT(ReturnT.FAIL_CODE, idleBeatResultSB.toString()); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java new file mode 100644 index 0000000..41ac671 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java @@ -0,0 +1,85 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器; + * a、virtual node:解决不均衡问题 + * b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围 + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteConsistentHash extends ExecutorRouter { + + private static int VIRTUAL_NODE_NUM = 100; + + /** + * get hash code on 2^32 ring (md5散列的方式计算hash值) + * @param key + * @return + */ + private static long hash(String key) { + + // md5 byte + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + md5.reset(); + byte[] keyBytes = null; + try { + keyBytes = key.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unknown string :" + key, e); + } + + md5.update(keyBytes); + byte[] digest = md5.digest(); + + // hash code, Truncate to 32-bits + long hashCode = ((long) (digest[3] & 0xFF) << 24) + | ((long) (digest[2] & 0xFF) << 16) + | ((long) (digest[1] & 0xFF) << 8) + | (digest[0] & 0xFF); + + long truncateHashCode = hashCode & 0xffffffffL; + return truncateHashCode; + } + + public String hashJob(int jobId, List addressList) { + + // ------A1------A2-------A3------ + // -----------J1------------------ + TreeMap addressRing = new TreeMap(); + for (String address: addressList) { + for (int i = 0; i < VIRTUAL_NODE_NUM; i++) { + long addressHash = hash("SHARD-" + address + "-NODE-" + i); + addressRing.put(addressHash, address); + } + } + + long jobHash = hash(String.valueOf(jobId)); + SortedMap lastRing = addressRing.tailMap(jobHash); + if (!lastRing.isEmpty()) { + return lastRing.get(lastRing.firstKey()); + } + return addressRing.firstEntry().getValue(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = hashJob(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java new file mode 100644 index 0000000..a2e4c90 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFailover extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + + StringBuffer beatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT beatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + beatResult = executorBiz.beat(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + beatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e ); + } + beatResultSB.append( (beatResultSB.length()>0)?"

":"") + .append(I18nUtil.getString("jobconf_beat") + ":") + .append("
address:").append(address) + .append("
code:").append(beatResult.getCode()) + .append("
msg:").append(beatResult.getMsg()); + + // beat success + if (beatResult.getCode() == ReturnT.SUCCESS_CODE) { + + beatResult.setMsg(beatResultSB.toString()); + beatResult.setContent(address); + return beatResult; + } + } + return new ReturnT(ReturnT.FAIL_CODE, beatResultSB.toString()); + + } +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java new file mode 100644 index 0000000..de4d7af --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFirst extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList){ + return new ReturnT(addressList.get(0)); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java new file mode 100644 index 0000000..9df1972 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,使用频率最低的优先被选举 + * a(*)、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLFU extends ExecutorRouter { + + private static ConcurrentMap> jobLfuMap = new ConcurrentHashMap>(); + private static long CACHE_VALID_TIME = 0; + + public String route(int jobId, List addressList) { + + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + jobLfuMap.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + // lfu item init + HashMap lfuItemMap = jobLfuMap.get(jobId); // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList; + if (lfuItemMap == null) { + lfuItemMap = new HashMap(); + jobLfuMap.putIfAbsent(jobId, lfuItemMap); // 避免重复覆盖 + } + + // put new + for (String address: addressList) { + if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) { + lfuItemMap.put(address, new Random().nextInt(addressList.size())); // 初始化时主动Random一次,缓解首次压力 + } + } + // remove old + List delKeys = new ArrayList<>(); + for (String existKey: lfuItemMap.keySet()) { + if (!addressList.contains(existKey)) { + delKeys.add(existKey); + } + } + if (delKeys.size() > 0) { + for (String delKey: delKeys) { + lfuItemMap.remove(delKey); + } + } + + // load least userd count address + List> lfuItemList = new ArrayList>(lfuItemMap.entrySet()); + Collections.sort(lfuItemList, new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return o1.getValue().compareTo(o2.getValue()); + } + }); + + Map.Entry addressItem = lfuItemList.get(0); + String minAddress = addressItem.getKey(); + addressItem.setValue(addressItem.getValue() + 1); + + return addressItem.getKey(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java new file mode 100644 index 0000000..2d54006 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java @@ -0,0 +1,76 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,最久为使用的优先被选举 + * a、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b(*)、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLRU extends ExecutorRouter { + + private static ConcurrentMap> jobLRUMap = new ConcurrentHashMap>(); + private static long CACHE_VALID_TIME = 0; + + public String route(int jobId, List addressList) { + + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + jobLRUMap.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + // init lru + LinkedHashMap lruItem = jobLRUMap.get(jobId); + if (lruItem == null) { + /** + * LinkedHashMap + * a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期; + * b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法; + */ + lruItem = new LinkedHashMap(16, 0.75f, true); + jobLRUMap.putIfAbsent(jobId, lruItem); + } + + // put new + for (String address: addressList) { + if (!lruItem.containsKey(address)) { + lruItem.put(address, address); + } + } + // remove old + List delKeys = new ArrayList<>(); + for (String existKey: lruItem.keySet()) { + if (!addressList.contains(existKey)) { + delKeys.add(existKey); + } + } + if (delKeys.size() > 0) { + for (String delKey: delKeys) { + lruItem.remove(delKey); + } + } + + // load + String eldestKey = lruItem.entrySet().iterator().next().getKey(); + String eldestValue = lruItem.get(eldestKey); + return eldestValue; + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java new file mode 100644 index 0000000..4ff3cf6 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLast extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + return new ReturnT(addressList.get(addressList.size()-1)); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java new file mode 100644 index 0000000..5ea4a38 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java @@ -0,0 +1,23 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRandom extends ExecutorRouter { + + private static Random localRandom = new Random(); + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = addressList.get(localRandom.nextInt(addressList.size())); + return new ReturnT(address); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java new file mode 100644 index 0000000..d0ea2ba --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRound extends ExecutorRouter { + + private static ConcurrentMap routeCountEachJob = new ConcurrentHashMap<>(); + private static long CACHE_VALID_TIME = 0; + + private static int count(int jobId) { + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + routeCountEachJob.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + AtomicInteger count = routeCountEachJob.get(jobId); + if (count == null || count.get() > 1000000) { + // 初始化时主动Random一次,缓解首次压力 + count = new AtomicInteger(new Random().nextInt(100)); + } else { + // count++ + count.addAndGet(1); + } + routeCountEachJob.put(jobId, count); + return count.get(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = addressList.get(count(triggerParam.getJobId())%addressList.size()); + return new ReturnT(address); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java new file mode 100644 index 0000000..0b9b4a9 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java @@ -0,0 +1,39 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum MisfireStrategyEnum { + + /** + * do nothing + */ + DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing")), + + /** + * fire once now + */ + FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now")); + + private String title; + + MisfireStrategyEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static MisfireStrategyEnum match(String name, MisfireStrategyEnum defaultItem){ + for (MisfireStrategyEnum item: MisfireStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java new file mode 100644 index 0000000..aa334fd --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum ScheduleTypeEnum { + + NONE(I18nUtil.getString("schedule_type_none")), + + /** + * schedule by cron + */ + CRON(I18nUtil.getString("schedule_type_cron")), + + /** + * schedule by fixed rate (in seconds) + */ + FIX_RATE(I18nUtil.getString("schedule_type_fix_rate")), + + /** + * schedule by fix delay (in seconds), after the last time + */ + /*FIX_DELAY(I18nUtil.getString("schedule_type_fix_delay"))*/; + + private String title; + + ScheduleTypeEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static ScheduleTypeEnum match(String name, ScheduleTypeEnum defaultItem){ + for (ScheduleTypeEnum item: ScheduleTypeEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java new file mode 100644 index 0000000..bb2cda8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java @@ -0,0 +1,101 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.thread.*; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.client.ExecutorBizClient; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author xuxueli 2018-10-28 00:18:17 + */ + +public class XxlJobScheduler { + private static final Logger logger = LoggerFactory.getLogger(XxlJobScheduler.class); + + + public void init() throws Exception { + // init i18n + initI18n(); + + // admin trigger pool start + JobTriggerPoolHelper.toStart(); + + // admin registry monitor run + JobRegistryHelper.getInstance().start(); + + // admin fail-monitor run + JobFailMonitorHelper.getInstance().start(); + + // admin lose-monitor run ( depend on JobTriggerPoolHelper ) + JobCompleteHelper.getInstance().start(); + + // admin log report start + JobLogReportHelper.getInstance().start(); + + // start-schedule ( depend on JobTriggerPoolHelper ) + JobScheduleHelper.getInstance().start(); + + logger.info(">>>>>>>>> init xxl-job admin success."); + } + + + public void destroy() throws Exception { + + // stop-schedule + JobScheduleHelper.getInstance().toStop(); + + // admin log report stop + JobLogReportHelper.getInstance().toStop(); + + // admin lose-monitor stop + JobCompleteHelper.getInstance().toStop(); + + // admin fail-monitor stop + JobFailMonitorHelper.getInstance().toStop(); + + // admin registry stop + JobRegistryHelper.getInstance().toStop(); + + // admin trigger pool stop + JobTriggerPoolHelper.toStop(); + + } + + // ---------------------- I18n ---------------------- + + private void initI18n(){ + for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) { + item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name()))); + } + } + + // ---------------------- executor-client ---------------------- + private static ConcurrentMap executorBizRepository = new ConcurrentHashMap(); + public static ExecutorBiz getExecutorBiz(String address) throws Exception { + // valid + if (address==null || address.trim().length()==0) { + return null; + } + + // load-cache + address = address.trim(); + ExecutorBiz executorBiz = executorBizRepository.get(address); + if (executorBiz != null) { + return executorBiz; + } + + // set-cache + executorBiz = new ExecutorBizClient(address, XxlJobAdminConfig.getAdminConfig().getAccessToken()); + + executorBizRepository.put(address, executorBiz); + return executorBiz; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java new file mode 100644 index 0000000..5698926 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java @@ -0,0 +1,184 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.*; + +/** + * job lose-monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobCompleteHelper { + private static Logger logger = LoggerFactory.getLogger(JobCompleteHelper.class); + + private static JobCompleteHelper instance = new JobCompleteHelper(); + public static JobCompleteHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private ThreadPoolExecutor callbackThreadPool = null; + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + + // for callback + callbackThreadPool = new ThreadPoolExecutor( + 2, + 20, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(3000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now)."); + } + }); + + + // for monitor + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // wait for JobTriggerPoolHelper-init + try { + TimeUnit.MILLISECONDS.sleep(50); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + // monitor + while (!toStop) { + try { + // 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; + Date losedTime = DateUtil.addMinutes(new Date(), -10); + List losedJobIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime); + + if (losedJobIds!=null && losedJobIds.size()>0) { + for (Long logId: losedJobIds) { + + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setId(logId); + + jobLog.setHandleTime(new Date()); + jobLog.setHandleCode(ReturnT.FAIL_CODE); + jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") ); + + XxlJobCompleter.updateHandleInfoAndFinish(jobLog); + } + + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(60); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobLosedMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + callbackThreadPool.shutdownNow(); + + // stop monitorThread (interrupt and wait) + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT callback(List callbackParamList) { + + callbackThreadPool.execute(new Runnable() { + @Override + public void run() { + for (HandleCallbackParam handleCallbackParam: callbackParamList) { + ReturnT callbackResult = callback(handleCallbackParam); + logger.debug(">>>>>>>>> JobApiController.callback {}, handleCallbackParam={}, callbackResult={}", + (callbackResult.getCode()== ReturnT.SUCCESS_CODE?"success":"fail"), handleCallbackParam, callbackResult); + } + } + }); + + return ReturnT.SUCCESS; + } + + private ReturnT callback(HandleCallbackParam handleCallbackParam) { + // valid log item + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(handleCallbackParam.getLogId()); + if (log == null) { + return new ReturnT(ReturnT.FAIL_CODE, "log item not found."); + } + if (log.getHandleCode() > 0) { + return new ReturnT(ReturnT.FAIL_CODE, "log repeate callback."); // avoid repeat callback, trigger child job etc + } + + // handle msg + StringBuffer handleMsg = new StringBuffer(); + if (log.getHandleMsg()!=null) { + handleMsg.append(log.getHandleMsg()).append("
"); + } + if (handleCallbackParam.getHandleMsg() != null) { + handleMsg.append(handleCallbackParam.getHandleMsg()); + } + + // success, save log + log.setHandleTime(new Date()); + log.setHandleCode(handleCallbackParam.getHandleCode()); + log.setHandleMsg(handleMsg.toString()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + + return ReturnT.SUCCESS; + } + + + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java new file mode 100644 index 0000000..8409d7b --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java @@ -0,0 +1,110 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * job monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobFailMonitorHelper { + private static Logger logger = LoggerFactory.getLogger(JobFailMonitorHelper.class); + + private static JobFailMonitorHelper instance = new JobFailMonitorHelper(); + public static JobFailMonitorHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // monitor + while (!toStop) { + try { + + List failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000); + if (failLogIds!=null && !failLogIds.isEmpty()) { + for (long failLogId: failLogIds) { + + // lock log + int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1); + if (lockRet < 1) { + continue; + } + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId); + XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId()); + + // 1、fail retry monitor + if (log.getExecutorFailRetryCount() > 0) { + JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null); + String retryMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<<
"; + log.setTriggerMsg(log.getTriggerMsg() + retryMsg); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log); + } + + // 2、fail alarm monitor + int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 + if (info != null) { + boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log); + newAlarmStatus = alarmResult?2:3; + } else { + newAlarmStatus = 1; + } + + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(10); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobFailMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java new file mode 100644 index 0000000..2387a0c --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java @@ -0,0 +1,152 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * job log report helper + * + * @author xuxueli 2019-11-22 + */ +public class JobLogReportHelper { + private static Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class); + + private static JobLogReportHelper instance = new JobLogReportHelper(); + public static JobLogReportHelper getInstance(){ + return instance; + } + + + private Thread logrThread; + private volatile boolean toStop = false; + public void start(){ + logrThread = new Thread(new Runnable() { + + @Override + public void run() { + + // last clean log time + long lastCleanLogTime = 0; + + + while (!toStop) { + + // 1、log-report refresh: refresh log report in 3 days + try { + + for (int i = 0; i < 3; i++) { + + // today + Calendar itemDay = Calendar.getInstance(); + itemDay.add(Calendar.DAY_OF_MONTH, -i); + itemDay.set(Calendar.HOUR_OF_DAY, 0); + itemDay.set(Calendar.MINUTE, 0); + itemDay.set(Calendar.SECOND, 0); + itemDay.set(Calendar.MILLISECOND, 0); + + Date todayFrom = itemDay.getTime(); + + itemDay.set(Calendar.HOUR_OF_DAY, 23); + itemDay.set(Calendar.MINUTE, 59); + itemDay.set(Calendar.SECOND, 59); + itemDay.set(Calendar.MILLISECOND, 999); + + Date todayTo = itemDay.getTime(); + + // refresh log-report every minute + XxlJobLogReport xxlJobLogReport = new XxlJobLogReport(); + xxlJobLogReport.setTriggerDay(todayFrom); + xxlJobLogReport.setRunningCount(0); + xxlJobLogReport.setSucCount(0); + xxlJobLogReport.setFailCount(0); + + Map triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo); + if (triggerCountMap!=null && triggerCountMap.size()>0) { + int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0; + int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0; + int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0; + int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc; + + xxlJobLogReport.setRunningCount(triggerDayCountRunning); + xxlJobLogReport.setSucCount(triggerDayCountSuc); + xxlJobLogReport.setFailCount(triggerDayCountFail); + } + + // do refresh + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e); + } + } + + // 2、log-clean: switch open & once each day + if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0 + && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) { + + // expire-time + Calendar expiredDay = Calendar.getInstance(); + expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays()); + expiredDay.set(Calendar.HOUR_OF_DAY, 0); + expiredDay.set(Calendar.MINUTE, 0); + expiredDay.set(Calendar.SECOND, 0); + expiredDay.set(Calendar.MILLISECOND, 0); + Date clearBeforeTime = expiredDay.getTime(); + + // clean expired log + List logIds = null; + do { + logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000); + if (logIds!=null && logIds.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + // update clean time + lastCleanLogTime = System.currentTimeMillis(); + } + + try { + TimeUnit.MINUTES.sleep(1); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job log report thread stop"); + + } + }); + logrThread.setDaemon(true); + logrThread.setName("xxl-job, admin JobLogReportHelper"); + logrThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + logrThread.interrupt(); + try { + logrThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java new file mode 100644 index 0000000..37edfd9 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java @@ -0,0 +1,204 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.concurrent.*; + +/** + * job registry instance + * @author xuxueli 2016-10-02 19:10:24 + */ +public class JobRegistryHelper { + private static Logger logger = LoggerFactory.getLogger(JobRegistryHelper.class); + + private static JobRegistryHelper instance = new JobRegistryHelper(); + public static JobRegistryHelper getInstance(){ + return instance; + } + + private ThreadPoolExecutor registryOrRemoveThreadPool = null; + private Thread registryMonitorThread; + private volatile boolean toStop = false; + + public void start(){ + + // for registry or remove + registryOrRemoveThreadPool = new ThreadPoolExecutor( + 2, + 10, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now)."); + } + }); + + // for monitor + registryMonitorThread = new Thread(new Runnable() { + @Override + public void run() { + while (!toStop) { + try { + // auto registry group + List groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0); + if (groupList!=null && !groupList.isEmpty()) { + + // remove dead address (admin/executor) + List ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (ids!=null && ids.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids); + } + + // fresh online address (admin/executor) + HashMap> appAddressMap = new HashMap>(); + List list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List registryList = appAddressMap.get(appname); + if (registryList == null) { + registryList = new ArrayList(); + } + + if (!registryList.contains(item.getRegistryValue())) { + registryList.add(item.getRegistryValue()); + } + appAddressMap.put(appname, registryList); + } + } + } + + // fresh group address + for (XxlJobGroup group: groupList) { + List registryList = appAddressMap.get(group.getAppname()); + String addressListStr = null; + if (registryList!=null && !registryList.isEmpty()) { + Collections.sort(registryList); + StringBuilder addressListSB = new StringBuilder(); + for (String item:registryList) { + addressListSB.append(item).append(","); + } + addressListStr = addressListSB.toString(); + addressListStr = addressListStr.substring(0, addressListStr.length()-1); + } + group.setAddressList(addressListStr); + group.setUpdateTime(new Date()); + + XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group); + } + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + try { + TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop"); + } + }); + registryMonitorThread.setDaemon(true); + registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread"); + registryMonitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + registryOrRemoveThreadPool.shutdownNow(); + + // stop monitir (interrupt and wait) + registryMonitorThread.interrupt(); + try { + registryMonitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT registry(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + public ReturnT registryRemove(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue()); + if (ret > 0) { + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + private void freshGroupRegistryInfo(RegistryParam registryParam){ + // Under consideration, prevent affecting core tables + } + + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java new file mode 100644 index 0000000..831bcf6 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java @@ -0,0 +1,369 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * @author xuxueli 2019-05-21 + */ +public class JobScheduleHelper { + private static Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class); + + private static JobScheduleHelper instance = new JobScheduleHelper(); + public static JobScheduleHelper getInstance(){ + return instance; + } + + public static final long PRE_READ_MS = 5000; // pre read + + private Thread scheduleThread; + private Thread ringThread; + private volatile boolean scheduleThreadToStop = false; + private volatile boolean ringThreadToStop = false; + private volatile static Map> ringData = new ConcurrentHashMap<>(); + + public void start(){ + + // schedule thread + scheduleThread = new Thread(new Runnable() { + @Override + public void run() { + + try { + TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + logger.info(">>>>>>>>> init xxl-job admin scheduler success."); + + // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20) + int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20; + + while (!scheduleThreadToStop) { + + // Scan Job + long start = System.currentTimeMillis(); + + Connection conn = null; + Boolean connAutoCommit = null; + PreparedStatement preparedStatement = null; + + boolean preReadSuc = true; + try { + + conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection(); + connAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); + + preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" ); + preparedStatement.execute(); + + // tx start + + // 1、pre read + long nowTime = System.currentTimeMillis(); + List scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount); + if (scheduleList!=null && scheduleList.size()>0) { + // 2、push time-ring + for (XxlJobInfo jobInfo: scheduleList) { + + // time-ring jump + if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) { + // 2.1、trigger-expire > 5s:pass && make next-trigger-time + logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId()); + + // 1、misfire match + MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); + if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) { + // FIRE_ONCE_NOW 》 trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + } + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + } else if (nowTime > jobInfo.getTriggerNextTime()) { + // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time + + // 1、trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + // next-trigger-time in 5s, pre-read again + if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) { + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } else { + // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } + + // 3、update trigger info + for (XxlJobInfo jobInfo: scheduleList) { + XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo); + } + + } else { + preReadSuc = false; + } + + // tx stop + + + } catch (Exception e) { + if (!scheduleThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e); + } + } finally { + + // commit + if (conn != null) { + try { + conn.commit(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.setAutoCommit(connAutoCommit); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + // close PreparedStatement + if (null != preparedStatement) { + try { + preparedStatement.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + } + long cost = System.currentTimeMillis()-start; + + + // Wait seconds, align second + if (cost < 1000) { // scan-overtime, not wait + try { + // pre-read period: success > scan each second; fail > skip this period; + TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop"); + } + }); + scheduleThread.setDaemon(true); + scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread"); + scheduleThread.start(); + + + // ring thread + ringThread = new Thread(new Runnable() { + @Override + public void run() { + + while (!ringThreadToStop) { + + // align second + try { + TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000); + } catch (InterruptedException e) { + if (!ringThreadToStop) { + logger.error(e.getMessage(), e); + } + } + + try { + // second data + List ringItemData = new ArrayList<>(); + int nowSecond = Calendar.getInstance().get(Calendar.SECOND); // 避免处理耗时太长,跨过刻度,向前校验一个刻度; + for (int i = 0; i < 2; i++) { + List tmpData = ringData.remove( (nowSecond+60-i)%60 ); + if (tmpData != null) { + ringItemData.addAll(tmpData); + } + } + + // ring trigger + logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) ); + if (ringItemData.size() > 0) { + // do trigger + for (int jobId: ringItemData) { + // do trigger + JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null); + } + // clear + ringItemData.clear(); + } + } catch (Exception e) { + if (!ringThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop"); + } + }); + ringThread.setDaemon(true); + ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread"); + ringThread.start(); + } + + private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + Date nextValidTime = generateNextValidTime(jobInfo, fromTime); + if (nextValidTime != null) { + jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime()); + jobInfo.setTriggerNextTime(nextValidTime.getTime()); + } else { + jobInfo.setTriggerStatus(0); + jobInfo.setTriggerLastTime(0); + jobInfo.setTriggerNextTime(0); + logger.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}", + jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf()); + } + } + + private void pushTimeRing(int ringSecond, int jobId){ + // push async ring + List ringItemData = ringData.get(ringSecond); + if (ringItemData == null) { + ringItemData = new ArrayList(); + ringData.put(ringSecond, ringItemData); + } + ringItemData.add(jobId); + + logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) ); + } + + public void toStop(){ + + // 1、stop schedule + scheduleThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); // wait + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (scheduleThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + scheduleThread.interrupt(); + try { + scheduleThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // if has ring data + boolean hasRingData = false; + if (!ringData.isEmpty()) { + for (int second : ringData.keySet()) { + List tmpData = ringData.get(second); + if (tmpData!=null && tmpData.size()>0) { + hasRingData = true; + break; + } + } + } + if (hasRingData) { + try { + TimeUnit.SECONDS.sleep(8); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // stop ring (wait job-in-memory stop) + ringThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (ringThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + ringThread.interrupt(); + try { + ringThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop"); + } + + + // ---------------------- tools ---------------------- + public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (ScheduleTypeEnum.CRON == scheduleTypeEnum) { + Date nextValidTime = new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime); + return nextValidTime; + } else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) { + return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 ); + } + return null; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java new file mode 100644 index 0000000..398713d --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java @@ -0,0 +1,150 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.trigger.XxlJobTrigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * job trigger thread pool helper + * + * @author xuxueli 2018-07-03 21:08:07 + */ +public class JobTriggerPoolHelper { + private static Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class); + + + // ---------------------- trigger pool ---------------------- + + // fast/slow thread pool + private ThreadPoolExecutor fastTriggerPool = null; + private ThreadPoolExecutor slowTriggerPool = null; + + public void start(){ + fastTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(1000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()); + } + }); + + slowTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode()); + } + }); + } + + + public void stop() { + //triggerPool.shutdown(); + fastTriggerPool.shutdownNow(); + slowTriggerPool.shutdownNow(); + logger.info(">>>>>>>>> xxl-job trigger thread pool shutdown success."); + } + + + // job timeout count + private volatile long minTim = System.currentTimeMillis()/60000; // ms > min + private volatile ConcurrentMap jobTimeoutCountMap = new ConcurrentHashMap<>(); + + + /** + * add trigger + */ + public void addTrigger(final int jobId, + final TriggerTypeEnum triggerType, + final int failRetryCount, + final String executorShardingParam, + final String executorParam, + final String addressList) { + + // choose thread pool + ThreadPoolExecutor triggerPool_ = fastTriggerPool; + AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId); + if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) { // job-timeout 10 times in 1 min + triggerPool_ = slowTriggerPool; + } + + // trigger + triggerPool_.execute(new Runnable() { + @Override + public void run() { + + long start = System.currentTimeMillis(); + + try { + // do trigger + XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + + // check timeout-count-map + long minTim_now = System.currentTimeMillis()/60000; + if (minTim != minTim_now) { + minTim = minTim_now; + jobTimeoutCountMap.clear(); + } + + // incr timeout-count-map + long cost = System.currentTimeMillis()-start; + if (cost > 500) { // ob-timeout threshold 500ms + AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1)); + if (timeoutCount != null) { + timeoutCount.incrementAndGet(); + } + } + + } + + } + }); + } + + + + // ---------------------- helper ---------------------- + + private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper(); + + public static void toStart() { + helper.start(); + } + public static void toStop() { + helper.stop(); + } + + /** + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + */ + public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { + helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java new file mode 100644 index 0000000..446c90e --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java @@ -0,0 +1,27 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * trigger type enum + * + * @author xuxueli 2018-09-16 04:56:41 + */ +public enum TriggerTypeEnum { + + MANUAL(I18nUtil.getString("jobconf_trigger_type_manual")), + CRON(I18nUtil.getString("jobconf_trigger_type_cron")), + RETRY(I18nUtil.getString("jobconf_trigger_type_retry")), + PARENT(I18nUtil.getString("jobconf_trigger_type_parent")), + API(I18nUtil.getString("jobconf_trigger_type_api")), + MISFIRE(I18nUtil.getString("jobconf_trigger_type_misfire")); + + private TriggerTypeEnum(String title){ + this.title = title; + } + private String title; + public String getTitle() { + return title; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java new file mode 100644 index 0000000..748befc --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java @@ -0,0 +1,226 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.util.IpUtil; +import com.xxl.job.core.util.ThrowableUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * xxl-job trigger + * Created by xuxueli on 17/7/13. + */ +public class XxlJobTrigger { + private static Logger logger = LoggerFactory.getLogger(XxlJobTrigger.class); + + /** + * trigger job + * + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + * @param addressList + * null: use executor addressList + * not null: cover + */ + public static void trigger(int jobId, + TriggerTypeEnum triggerType, + int failRetryCount, + String executorShardingParam, + String executorParam, + String addressList) { + + // load data + XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId); + if (jobInfo == null) { + logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId); + return; + } + if (executorParam != null) { + jobInfo.setExecutorParam(executorParam); + } + int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount(); + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup()); + + // cover addressList + if (addressList!=null && addressList.trim().length()>0) { + group.setAddressType(1); + group.setAddressList(addressList.trim()); + } + + // sharding param + int[] shardingParam = null; + if (executorShardingParam!=null){ + String[] shardingArr = executorShardingParam.split("/"); + if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) { + shardingParam = new int[2]; + shardingParam[0] = Integer.valueOf(shardingArr[0]); + shardingParam[1] = Integer.valueOf(shardingArr[1]); + } + } + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) + && group.getRegistryList()!=null && !group.getRegistryList().isEmpty() + && shardingParam==null) { + for (int i = 0; i < group.getRegistryList().size(); i++) { + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size()); + } + } else { + if (shardingParam == null) { + shardingParam = new int[]{0, 1}; + } + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]); + } + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * @param group job group, registry list may be empty + * @param jobInfo + * @param finalFailRetryCount + * @param triggerType + * @param index sharding index + * @param total sharding index + */ + private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){ + + // param + ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy + ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); // route strategy + String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null; + + // 1、save log-id + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setJobGroup(jobInfo.getJobGroup()); + jobLog.setJobId(jobInfo.getId()); + jobLog.setTriggerTime(new Date()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog); + logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId()); + + // 2、init trigger-param + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setJobId(jobInfo.getId()); + triggerParam.setExecutorHandler(jobInfo.getExecutorHandler()); + triggerParam.setExecutorParams(jobInfo.getExecutorParam()); + triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout()); + triggerParam.setLogId(jobLog.getId()); + triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime()); + triggerParam.setGlueType(jobInfo.getGlueType()); + triggerParam.setGlueSource(jobInfo.getGlueSource()); + triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime()); + triggerParam.setBroadcastIndex(index); + triggerParam.setBroadcastTotal(total); + + // 3、init address + String address = null; + ReturnT routeAddressResult = null; + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { + if (index < group.getRegistryList().size()) { + address = group.getRegistryList().get(index); + } else { + address = group.getRegistryList().get(0); + } + } else { + routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList()); + if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) { + address = routeAddressResult.getContent(); + } + } + } else { + routeAddressResult = new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty")); + } + + // 4、trigger remote executor + ReturnT triggerResult = null; + if (address != null) { + triggerResult = runExecutor(triggerParam, address); + } else { + triggerResult = new ReturnT(ReturnT.FAIL_CODE, null); + } + + // 5、collection trigger info + StringBuffer triggerMsgSb = new StringBuffer(); + triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":") + .append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") ); + triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle()); + if (shardingParam != null) { + triggerMsgSb.append("("+shardingParam+")"); + } + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount); + + triggerMsgSb.append("

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<<
") + .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"

":"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():""); + + // 6、save log trigger-info + jobLog.setExecutorAddress(address); + jobLog.setExecutorHandler(jobInfo.getExecutorHandler()); + jobLog.setExecutorParam(jobInfo.getExecutorParam()); + jobLog.setExecutorShardingParam(shardingParam); + jobLog.setExecutorFailRetryCount(finalFailRetryCount); + //jobLog.setTriggerTime(); + jobLog.setTriggerCode(triggerResult.getCode()); + jobLog.setTriggerMsg(triggerMsgSb.toString()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog); + + logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId()); + } + + /** + * run executor + * @param triggerParam + * @param address + * @return + */ + public static ReturnT runExecutor(TriggerParam triggerParam, String address){ + ReturnT runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + runResult = executorBiz.run(triggerParam); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e); + runResult = new ReturnT(ReturnT.FAIL_CODE, ThrowableUtil.toString(e)); + } + + StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":"); + runResultSB.append("
address:").append(address); + runResultSB.append("
code:").append(runResult.getCode()); + runResultSB.append("
msg:").append(runResult.getMsg()); + + runResult.setMsg(runResultSB.toString()); + return runResult; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java new file mode 100644 index 0000000..cddb27f --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java @@ -0,0 +1,98 @@ +package com.xxl.job.admin.core.util; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Cookie.Util + * + * @author xuxueli 2015-12-12 18:01:06 + */ +public class CookieUtil { + + // 默认缓存时间,单位/秒, 2H + private static final int COOKIE_MAX_AGE = Integer.MAX_VALUE; + // 保存路径,根路径 + private static final String COOKIE_PATH = "/"; + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param ifRemember + */ + public static void set(HttpServletResponse response, String key, String value, boolean ifRemember) { + int age = ifRemember?COOKIE_MAX_AGE:-1; + set(response, key, value, null, COOKIE_PATH, age, true); + } + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param maxAge + */ + private static void set(HttpServletResponse response, String key, String value, String domain, String path, int maxAge, boolean isHttpOnly) { + Cookie cookie = new Cookie(key, value); + if (domain != null) { + cookie.setDomain(domain); + } + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(isHttpOnly); + response.addCookie(cookie); + } + + /** + * 查询value + * + * @param request + * @param key + * @return + */ + public static String getValue(HttpServletRequest request, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + /** + * 查询Cookie + * + * @param request + * @param key + */ + private static Cookie get(HttpServletRequest request, String key) { + Cookie[] arr_cookie = request.getCookies(); + if (arr_cookie != null && arr_cookie.length > 0) { + for (Cookie cookie : arr_cookie) { + if (cookie.getName().equals(key)) { + return cookie; + } + } + } + return null; + } + + /** + * 删除Cookie + * + * @param request + * @param response + * @param key + */ + public static void remove(HttpServletRequest request, HttpServletResponse response, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + set(response, key, "", null, COOKIE_PATH, 0, true); + } + } + +} \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java new file mode 100644 index 0000000..e90af43 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.core.util; + +import freemarker.ext.beans.BeansWrapper; +import freemarker.ext.beans.BeansWrapperBuilder; +import freemarker.template.Configuration; +import freemarker.template.TemplateHashModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ftl util + * + * @author xuxueli 2018-01-17 20:37:48 + */ +public class FtlUtil { + private static Logger logger = LoggerFactory.getLogger(FtlUtil.class); + + private static BeansWrapper wrapper = new BeansWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build(); //BeansWrapper.getDefaultInstance(); + + public static TemplateHashModel generateStaticModel(String packageName) { + try { + TemplateHashModel staticModels = wrapper.getStaticModels(); + TemplateHashModel fileStatics = (TemplateHashModel) staticModels.get(packageName); + return fileStatics; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return null; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java new file mode 100644 index 0000000..772a96e --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.util; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * i18n util + * + * @author xuxueli 2018-01-17 20:39:06 + */ +public class I18nUtil { + private static Logger logger = LoggerFactory.getLogger(I18nUtil.class); + + private static Properties prop = null; + public static Properties loadI18nProp(){ + if (prop != null) { + return prop; + } + try { + // build i18n prop + String i18n = XxlJobAdminConfig.getAdminConfig().getI18n(); + String i18nFile = MessageFormat.format("i18n/message_{0}.properties", i18n); + + // load prop + Resource resource = new ClassPathResource(i18nFile); + EncodedResource encodedResource = new EncodedResource(resource,"UTF-8"); + prop = PropertiesLoaderUtils.loadProperties(encodedResource); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return prop; + } + + /** + * get val of i18n key + * + * @param key + * @return + */ + public static String getString(String key) { + return loadI18nProp().getProperty(key); + } + + /** + * get mult val of i18n mult key, as json + * + * @param keys + * @return + */ + public static String getMultString(String... keys) { + Map map = new HashMap(); + + Properties prop = loadI18nProp(); + if (keys!=null && keys.length>0) { + for (String key: keys) { + map.put(key, prop.getProperty(key)); + } + } else { + for (String key: prop.stringPropertyNames()) { + map.put(key, prop.getProperty(key)); + } + } + + String json = JacksonUtil.writeValueAsString(map); + return json; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java new file mode 100644 index 0000000..4f4ea3c --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java @@ -0,0 +1,92 @@ +package com.xxl.job.admin.core.util; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Jackson util + * + * 1、obj need private and set/get; + * 2、do not support inner class; + * + * @author xuxueli 2015-9-25 18:02:56 + */ +public class JacksonUtil { + private static Logger logger = LoggerFactory.getLogger(JacksonUtil.class); + + private final static ObjectMapper objectMapper = new ObjectMapper(); + public static ObjectMapper getInstance() { + return objectMapper; + } + + /** + * bean、array、List、Map --> json + * + * @param obj + * @return json string + * @throws Exception + */ + public static String writeValueAsString(Object obj) { + try { + return getInstance().writeValueAsString(obj); + } catch (JsonGenerationException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * string --> bean、Map、List(array) + * + * @param jsonStr + * @param clazz + * @return obj + * @throws Exception + */ + public static T readValue(String jsonStr, Class clazz) { + try { + return getInstance().readValue(jsonStr, clazz); + } catch (JsonParseException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * string --> List... + * + * @param jsonStr + * @param parametrized + * @param parameterClasses + * @param + * @return + */ + public static T readValue(String jsonStr, Class parametrized, Class... parameterClasses) { + try { + JavaType javaType = getInstance().getTypeFactory().constructParametricType(parametrized, parameterClasses); + return getInstance().readValue(jsonStr, javaType); + } catch (JsonParseException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java new file mode 100644 index 0000000..fbab061 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java @@ -0,0 +1,133 @@ +package com.xxl.job.admin.core.util; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * local cache tool + * + * @author xuxueli 2018-01-22 21:37:34 + */ +public class LocalCacheUtil { + + private static ConcurrentMap cacheRepository = new ConcurrentHashMap(); // 类型建议用抽象父类,兼容性更好; + private static class LocalCacheData{ + private String key; + private Object val; + private long timeoutTime; + + public LocalCacheData() { + } + + public LocalCacheData(String key, Object val, long timeoutTime) { + this.key = key; + this.val = val; + this.timeoutTime = timeoutTime; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Object getVal() { + return val; + } + + public void setVal(Object val) { + this.val = val; + } + + public long getTimeoutTime() { + return timeoutTime; + } + + public void setTimeoutTime(long timeoutTime) { + this.timeoutTime = timeoutTime; + } + } + + + /** + * set cache + * + * @param key + * @param val + * @param cacheTime + * @return + */ + public static boolean set(String key, Object val, long cacheTime){ + + // clean timeout cache, before set new cache (avoid cache too much) + cleanTimeoutCache(); + + // set new cache + if (key==null || key.trim().length()==0) { + return false; + } + if (val == null) { + remove(key); + } + if (cacheTime <= 0) { + remove(key); + } + long timeoutTime = System.currentTimeMillis() + cacheTime; + LocalCacheData localCacheData = new LocalCacheData(key, val, timeoutTime); + cacheRepository.put(localCacheData.getKey(), localCacheData); + return true; + } + + /** + * remove cache + * + * @param key + * @return + */ + public static boolean remove(String key){ + if (key==null || key.trim().length()==0) { + return false; + } + cacheRepository.remove(key); + return true; + } + + /** + * get cache + * + * @param key + * @return + */ + public static Object get(String key){ + if (key==null || key.trim().length()==0) { + return null; + } + LocalCacheData localCacheData = cacheRepository.get(key); + if (localCacheData!=null && System.currentTimeMillis()=localCacheData.getTimeoutTime()) { + cacheRepository.remove(key); + } + } + } + return true; + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java new file mode 100644 index 0000000..b608d9f --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java @@ -0,0 +1,37 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobGroup; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobGroupDao { + + public List findAll(); + + public List findByAddressType(@Param("addressType") int addressType); + + public int save(XxlJobGroup xxlJobGroup); + + public int update(XxlJobGroup xxlJobGroup); + + public int remove(@Param("id") int id); + + public XxlJobGroup load(@Param("id") int id); + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("appname") String appname, + @Param("title") String title); + + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("appname") String appname, + @Param("title") String title); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java new file mode 100644 index 0000000..d640eff --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java @@ -0,0 +1,49 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + + +/** + * job info + * @author xuxueli 2016-1-12 18:03:45 + */ +@Mapper +public interface XxlJobInfoDao { + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("triggerStatus") int triggerStatus, + @Param("jobDesc") String jobDesc, + @Param("executorHandler") String executorHandler, + @Param("author") String author); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("triggerStatus") int triggerStatus, + @Param("jobDesc") String jobDesc, + @Param("executorHandler") String executorHandler, + @Param("author") String author); + + public int save(XxlJobInfo info); + + public XxlJobInfo loadById(@Param("id") int id); + + public int update(XxlJobInfo xxlJobInfo); + + public int delete(@Param("id") long id); + + public List getJobsByGroup(@Param("jobGroup") int jobGroup); + + public int findAllCount(); + + public List scheduleJobQuery(@Param("maxNextTime") long maxNextTime, @Param("pagesize") int pagesize ); + + public int scheduleUpdate(XxlJobInfo xxlJobInfo); + + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java new file mode 100644 index 0000000..62fa3b4 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java @@ -0,0 +1,62 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLog; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * job log + * @author xuxueli 2016-1-12 18:03:06 + */ +@Mapper +public interface XxlJobLogDao { + + // exist jobId not use jobGroup, not exist use jobGroup + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("triggerTimeStart") Date triggerTimeStart, + @Param("triggerTimeEnd") Date triggerTimeEnd, + @Param("logStatus") int logStatus); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("triggerTimeStart") Date triggerTimeStart, + @Param("triggerTimeEnd") Date triggerTimeEnd, + @Param("logStatus") int logStatus); + + public XxlJobLog load(@Param("id") long id); + + public long save(XxlJobLog xxlJobLog); + + public int updateTriggerInfo(XxlJobLog xxlJobLog); + + public int updateHandleInfo(XxlJobLog xxlJobLog); + + public int delete(@Param("jobId") int jobId); + + public Map findLogReport(@Param("from") Date from, + @Param("to") Date to); + + public List findClearLogIds(@Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("clearBeforeTime") Date clearBeforeTime, + @Param("clearBeforeNum") int clearBeforeNum, + @Param("pagesize") int pagesize); + public int clearLog(@Param("logIds") List logIds); + + public List findFailJobLogIds(@Param("pagesize") int pagesize); + + public int updateAlarmStatus(@Param("logId") long logId, + @Param("oldAlarmStatus") int oldAlarmStatus, + @Param("newAlarmStatus") int newAlarmStatus); + + public List findLostJobIds(@Param("losedTime") Date losedTime); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java new file mode 100644 index 0000000..3028aed --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * job log for glue + * @author xuxueli 2016-5-19 18:04:56 + */ +@Mapper +public interface XxlJobLogGlueDao { + + public int save(XxlJobLogGlue xxlJobLogGlue); + + public List findByJobId(@Param("jobId") int jobId); + + public int removeOld(@Param("jobId") int jobId, @Param("limit") int limit); + + public int deleteByJobId(@Param("jobId") int jobId); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java new file mode 100644 index 0000000..f4b3dc8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java @@ -0,0 +1,26 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * job log + * @author xuxueli 2019-11-22 + */ +@Mapper +public interface XxlJobLogReportDao { + + public int save(XxlJobLogReport xxlJobLogReport); + + public int update(XxlJobLogReport xxlJobLogReport); + + public List queryLogReport(@Param("triggerDayFrom") Date triggerDayFrom, + @Param("triggerDayTo") Date triggerDayTo); + + public XxlJobLogReport queryLogReportTotal(); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java new file mode 100644 index 0000000..1005c46 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java @@ -0,0 +1,38 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobRegistry; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobRegistryDao { + + public List findDead(@Param("timeout") int timeout, + @Param("nowTime") Date nowTime); + + public int removeDead(@Param("ids") List ids); + + public List findAll(@Param("timeout") int timeout, + @Param("nowTime") Date nowTime); + + public int registryUpdate(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registrySave(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registryDelete(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java new file mode 100644 index 0000000..e840494 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * @author xuxueli 2019-05-04 16:44:59 + */ +@Mapper +public interface XxlJobUserDao { + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + + public XxlJobUser loadByUserName(@Param("username") String username); + + public int save(XxlJobUser xxlJobUser); + + public int update(XxlJobUser xxlJobUser); + + public int delete(@Param("id") int id); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/LoginService.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/LoginService.java new file mode 100644 index 0000000..fe15799 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/LoginService.java @@ -0,0 +1,107 @@ +package com.xxl.job.admin.service; + +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.CookieUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.core.util.JacksonUtil; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.DigestUtils; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.math.BigInteger; + +/** + * @author xuxueli 2019-05-04 22:13:264 + */ +@Configuration +public class LoginService { + + public static final String LOGIN_IDENTITY_KEY = "XXL_JOB_LOGIN_IDENTITY"; + + @Resource + private XxlJobUserDao xxlJobUserDao; + + + private String makeToken(XxlJobUser xxlJobUser){ + String tokenJson = JacksonUtil.writeValueAsString(xxlJobUser); + String tokenHex = new BigInteger(tokenJson.getBytes()).toString(16); + return tokenHex; + } + private XxlJobUser parseToken(String tokenHex){ + XxlJobUser xxlJobUser = null; + if (tokenHex != null) { + String tokenJson = new String(new BigInteger(tokenHex, 16).toByteArray()); // username_password(md5) + xxlJobUser = JacksonUtil.readValue(tokenJson, XxlJobUser.class); + } + return xxlJobUser; + } + + + public ReturnT login(HttpServletRequest request, HttpServletResponse response, String username, String password, boolean ifRemember){ + + // param + if (username==null || username.trim().length()==0 || password==null || password.trim().length()==0){ + return new ReturnT(500, I18nUtil.getString("login_param_empty")); + } + + // valid passowrd + XxlJobUser xxlJobUser = xxlJobUserDao.loadByUserName(username); + if (xxlJobUser == null) { + return new ReturnT(500, I18nUtil.getString("login_param_unvalid")); + } + String passwordMd5 = DigestUtils.md5DigestAsHex(password.getBytes()); + if (!passwordMd5.equals(xxlJobUser.getPassword())) { + return new ReturnT(500, I18nUtil.getString("login_param_unvalid")); + } + + String loginToken = makeToken(xxlJobUser); + + // do login + CookieUtil.set(response, LOGIN_IDENTITY_KEY, loginToken, ifRemember); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @param response + */ + public ReturnT logout(HttpServletRequest request, HttpServletResponse response){ + CookieUtil.remove(request, response, LOGIN_IDENTITY_KEY); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @return + */ + public XxlJobUser ifLogin(HttpServletRequest request, HttpServletResponse response){ + String cookieToken = CookieUtil.getValue(request, LOGIN_IDENTITY_KEY); + if (cookieToken != null) { + XxlJobUser cookieUser = null; + try { + cookieUser = parseToken(cookieToken); + } catch (Exception e) { + logout(request, response); + } + if (cookieUser != null) { + XxlJobUser dbUser = xxlJobUserDao.loadByUserName(cookieUser.getUsername()); + if (dbUser != null) { + if (cookieUser.getPassword().equals(dbUser.getPassword())) { + return dbUser; + } + } + } + } + return null; + } + + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/XxlJobService.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/XxlJobService.java new file mode 100644 index 0000000..60b4bb8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/XxlJobService.java @@ -0,0 +1,98 @@ +package com.xxl.job.admin.service; + + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.core.biz.model.ReturnT; + +import java.util.Date; +import java.util.Map; + +/** + * core job action for xxl-job + * + * @author xuxueli 2016-5-28 15:30:33 + */ +public interface XxlJobService { + + /** + * page list + * + * @param start + * @param length + * @param jobGroup + * @param jobDesc + * @param executorHandler + * @param author + * @return + */ + public Map pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author); + + /** + * add job + * + * @param jobInfo + * @return + */ + public ReturnT add(XxlJobInfo jobInfo); + + /** + * update job + * + * @param jobInfo + * @return + */ + public ReturnT update(XxlJobInfo jobInfo); + + /** + * remove job + * * + * @param id + * @return + */ + public ReturnT remove(int id); + + /** + * start job + * + * @param id + * @return + */ + public ReturnT start(int id); + + /** + * stop job + * + * @param id + * @return + */ + public ReturnT stop(int id); + + /** + * trigger + * + * @param loginUser + * @param jobId + * @param executorParam + * @param addressList + * @return + */ + public ReturnT trigger(XxlJobUser loginUser, int jobId, String executorParam, String addressList); + + /** + * dashboard info + * + * @return + */ + public Map dashboardInfo(); + + /** + * chart info + * + * @param startDate + * @param endDate + * @return + */ + public ReturnT> chartInfo(Date startDate, Date endDate); + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java new file mode 100644 index 0000000..a0c432c --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java @@ -0,0 +1,38 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.thread.JobCompleteHelper; +import com.xxl.job.admin.core.thread.JobRegistryHelper; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.text.MessageFormat; +import java.util.Date; +import java.util.List; + +/** + * @author xuxueli 2017-07-27 21:54:20 + */ +@Service +public class AdminBizImpl implements AdminBiz { + + + @Override + public ReturnT callback(List callbackParamList) { + return JobCompleteHelper.getInstance().callback(callbackParamList); + } + + @Override + public ReturnT registry(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registry(registryParam); + } + + @Override + public ReturnT registryRemove(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registryRemove(registryParam); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java new file mode 100644 index 0000000..f264798 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java @@ -0,0 +1,473 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.*; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.text.MessageFormat; +import java.util.*; + +/** + * core job action for xxl-job + * @author xuxueli 2016-5-28 15:30:33 + */ +@Service +public class XxlJobServiceImpl implements XxlJobService { + private static Logger logger = LoggerFactory.getLogger(XxlJobServiceImpl.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + + @Override + public Map pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + // page list + List list = xxlJobInfoDao.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + int list_count = xxlJobInfoDao.pageListCount(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @Override + public ReturnT add(XxlJobInfo jobInfo) { + + // valid base + XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (group == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup")) ); + } + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid")); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE/* || scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid job + if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) ); + } + if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && (jobInfo.getExecutorHandler()==null || jobInfo.getExecutorHandler().trim().length()==0) ) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+"JobHandler") ); + } + // 》fix "\r" in shell + if (GlueTypeEnum.GLUE_SHELL==GlueTypeEnum.match(jobInfo.getGlueType()) && jobInfo.getGlueSource()!=null) { + jobInfo.setGlueSource(jobInfo.getGlueSource().replaceAll("\r", "")); + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // add in db + jobInfo.setAddTime(new Date()); + jobInfo.setUpdateTime(new Date()); + jobInfo.setGlueUpdatetime(new Date()); + xxlJobInfoDao.save(jobInfo); + if (jobInfo.getId() < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) ); + } + + return new ReturnT(String.valueOf(jobInfo.getId())); + } + + private boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @Override + public ReturnT update(XxlJobInfo jobInfo) { + + // valid base + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid") ); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE /*|| scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // group valid + XxlJobGroup jobGroup = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (jobGroup == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_jobgroup")+I18nUtil.getString("system_unvalid")) ); + } + + // stage job info + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId()); + if (exists_jobInfo == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_id")+I18nUtil.getString("system_not_found")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = exists_jobInfo.getTriggerNextTime(); + boolean scheduleDataNotChanged = jobInfo.getScheduleType().equals(exists_jobInfo.getScheduleType()) && jobInfo.getScheduleConf().equals(exists_jobInfo.getScheduleConf()); + if (exists_jobInfo.getTriggerStatus() == 1 && !scheduleDataNotChanged) { + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(jobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + exists_jobInfo.setJobGroup(jobInfo.getJobGroup()); + exists_jobInfo.setJobDesc(jobInfo.getJobDesc()); + exists_jobInfo.setAuthor(jobInfo.getAuthor()); + exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail()); + exists_jobInfo.setScheduleType(jobInfo.getScheduleType()); + exists_jobInfo.setScheduleConf(jobInfo.getScheduleConf()); + exists_jobInfo.setMisfireStrategy(jobInfo.getMisfireStrategy()); + exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy()); + exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler()); + exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam()); + exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout()); + exists_jobInfo.setExecutorFailRetryCount(jobInfo.getExecutorFailRetryCount()); + exists_jobInfo.setChildJobId(jobInfo.getChildJobId()); + exists_jobInfo.setTriggerNextTime(nextTriggerTime); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + + return ReturnT.SUCCESS; + } + + @Override + public ReturnT remove(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + if (xxlJobInfo == null) { + return ReturnT.SUCCESS; + } + + xxlJobInfoDao.delete(id); + xxlJobLogDao.delete(id); + xxlJobLogGlueDao.deleteByJobId(id); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT start(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + // valid + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(xxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE); + if (ScheduleTypeEnum.NONE == scheduleTypeEnum) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type_none_limit_start")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = 0; + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(xxlJobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + + xxlJobInfo.setTriggerStatus(1); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(nextTriggerTime); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT stop(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + xxlJobInfo.setTriggerStatus(0); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(0); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + + + @Override + public ReturnT trigger(XxlJobUser loginUser, int jobId, String executorParam, String addressList) { + // permission + if (loginUser == null) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("system_permission_limit")); + } + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(jobId); + if (xxlJobInfo == null) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (!hasPermission(loginUser, xxlJobInfo.getJobGroup())) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("system_permission_limit")); + } + + // force cover job param + if (executorParam == null) { + executorParam = ""; + } + + JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); + return ReturnT.SUCCESS; + } + + private boolean hasPermission(XxlJobUser loginUser, int jobGroup){ + if (loginUser.getRole() == 1) { + return true; + } + List groupIdStrs = new ArrayList<>(); + if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) { + groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(",")); + } + return groupIdStrs.contains(String.valueOf(jobGroup)); + } + + @Override + public Map dashboardInfo() { + + int jobInfoCount = xxlJobInfoDao.findAllCount(); + int jobLogCount = 0; + int jobLogSuccessCount = 0; + XxlJobLogReport xxlJobLogReport = xxlJobLogReportDao.queryLogReportTotal(); + if (xxlJobLogReport != null) { + jobLogCount = xxlJobLogReport.getRunningCount() + xxlJobLogReport.getSucCount() + xxlJobLogReport.getFailCount(); + jobLogSuccessCount = xxlJobLogReport.getSucCount(); + } + + // executor count + Set executorAddressSet = new HashSet(); + List groupList = xxlJobGroupDao.findAll(); + + if (groupList!=null && !groupList.isEmpty()) { + for (XxlJobGroup group: groupList) { + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + executorAddressSet.addAll(group.getRegistryList()); + } + } + } + + int executorCount = executorAddressSet.size(); + + Map dashboardMap = new HashMap(); + dashboardMap.put("jobInfoCount", jobInfoCount); + dashboardMap.put("jobLogCount", jobLogCount); + dashboardMap.put("jobLogSuccessCount", jobLogSuccessCount); + dashboardMap.put("executorCount", executorCount); + return dashboardMap; + } + + @Override + public ReturnT> chartInfo(Date startDate, Date endDate) { + + // process + List triggerDayList = new ArrayList(); + List triggerDayCountRunningList = new ArrayList(); + List triggerDayCountSucList = new ArrayList(); + List triggerDayCountFailList = new ArrayList(); + int triggerCountRunningTotal = 0; + int triggerCountSucTotal = 0; + int triggerCountFailTotal = 0; + + List logReportList = xxlJobLogReportDao.queryLogReport(startDate, endDate); + + if (logReportList!=null && logReportList.size()>0) { + for (XxlJobLogReport item: logReportList) { + String day = DateUtil.formatDate(item.getTriggerDay()); + int triggerDayCountRunning = item.getRunningCount(); + int triggerDayCountSuc = item.getSucCount(); + int triggerDayCountFail = item.getFailCount(); + + triggerDayList.add(day); + triggerDayCountRunningList.add(triggerDayCountRunning); + triggerDayCountSucList.add(triggerDayCountSuc); + triggerDayCountFailList.add(triggerDayCountFail); + + triggerCountRunningTotal += triggerDayCountRunning; + triggerCountSucTotal += triggerDayCountSuc; + triggerCountFailTotal += triggerDayCountFail; + } + } else { + for (int i = -6; i <= 0; i++) { + triggerDayList.add(DateUtil.formatDate(DateUtil.addDays(new Date(), i))); + triggerDayCountRunningList.add(0); + triggerDayCountSucList.add(0); + triggerDayCountFailList.add(0); + } + } + + Map result = new HashMap(); + result.put("triggerDayList", triggerDayList); + result.put("triggerDayCountRunningList", triggerDayCountRunningList); + result.put("triggerDayCountSucList", triggerDayCountSucList); + result.put("triggerDayCountFailList", triggerDayCountFailList); + + result.put("triggerCountRunningTotal", triggerCountRunningTotal); + result.put("triggerCountSucTotal", triggerCountSucTotal); + result.put("triggerCountFailTotal", triggerCountFailTotal); + + return new ReturnT>(result); + } + +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/application.yml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/application.yml new file mode 100644 index 0000000..de88244 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/application.yml @@ -0,0 +1,78 @@ +server: + port: 9080 + servlet: + context-path: /xxl-job-admin + #数据源配置 +spring: + datasource: + url: jdbc:mysql://jeecg-boot-mysql:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai + username: ${MYSQL-USER:root} + password: ${MYSQL-PWD:root} + driver-class-name: com.mysql.jdbc.Driver + type: com.zaxxer.hikari.HikariDataSource + hikari: + minimum-idle: 10 + maximum-pool-size: 30 + auto-commit: true + idle-timeout: 30000 + pool-name: HikariCP + max-lifetime: 900000 + connection-timeout: 10000 + connection-test-query: SELECT 1 + #邮箱配置 + mail: + host: smtphz.qiye.163.com + port: 994 + username: zhuwei@aboatedu.com + from: zhuwei@aboatedu.com + password: zwass1314 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + socketFactory: + class: javax.net.ssl.SSLSocketFactory + #MVC配置 + mvc: + servlet: + load-on-startup: 0 + static-path-pattern: /static/** + resources: + static-locations: classpath:/static/ + #freemarker配置 + freemarker: + templateLoaderPath=classpath: /templates/ + suffix: .ftl + charset: UTF-8 + request-context-attribute: request + settings: + number_format: 0.########## +#通用配置,开放端点 +management: + server: + servlet: + context-path: /actuator + health: + mail: + enabled: false +#mybatis配置 +mybatis: + mapper-locations: classpath:/mybatis-mapper/*Mapper.xml +#XXL-job配置 +xxl: + job: + login: + username: admin + password: 123456 + accessToken: + i18n: zh_CN + #触发池 + triggerpool: + fast: + max: 200 + slow: + max: 100 + logretentiondays: 30 \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_en.properties b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_en.properties new file mode 100644 index 0000000..8ce0b08 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_en.properties @@ -0,0 +1,276 @@ +admin_name=Scheduling Center +admin_name_full=Distributed Task Scheduling Platform XXL-JOB +admin_version=2.4.2-SNAPSHOT +admin_i18n=en + +## system +system_tips=System message +system_ok=Confirm +system_close=Close +system_save=Save +system_cancel=Cancel +system_search=Search +system_status=Status +system_opt=Operate +system_please_input=please input +system_please_choose=please choose +system_success=success +system_fail=fail +system_add_suc=add success +system_add_fail=add fail +system_update_suc=update success +system_update_fail=update fail +system_all=All +system_api_error=net error +system_show=Show +system_empty=Empty +system_opt_suc=operate success +system_opt_fail=operate fail +system_opt_edit=Edit +system_opt_del=Delete +system_opt_copy=Copy +system_unvalid=illegal +system_not_found=not exist +system_nav=Navigation +system_digits=digits +system_lengh_limit=Length limit +system_permission_limit=Permission limit +system_welcome=Welcome + +## daterangepicker +daterangepicker_ranges_recent_hour=recent one hour +daterangepicker_ranges_today=today +daterangepicker_ranges_yesterday=yesterday +daterangepicker_ranges_this_month=this month +daterangepicker_ranges_last_month=last month +daterangepicker_ranges_recent_week=recent one week +daterangepicker_ranges_recent_month=recent one month +daterangepicker_custom_name=custom +daterangepicker_custom_starttime=start time +daterangepicker_custom_endtime=end time +daterangepicker_custom_daysofweek=Sun,Mon,Tue,Wed,Thu,Fri,Sat +daterangepicker_custom_monthnames=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec + +## dataTable +dataTable_sProcessing=processing... +dataTable_sLengthMenu= _MENU_ records per page +dataTable_sZeroRecords=No matching results +dataTable_sInfo=page _PAGE_ ( Total _PAGES_ pages,_TOTAL_ records ) +dataTable_sInfoEmpty=No Record +dataTable_sInfoFiltered=(Filtered by _MAX_ results) +dataTable_sSearch=Search +dataTable_sEmptyTable=Table data is empty +dataTable_sLoadingRecords=Loading... +dataTable_sFirst=FIRST PAGE +dataTable_sPrevious=Previous Page +dataTable_sNext=Next Page +dataTable_sLast=LAST PAGE +dataTable_sSortAscending=: Rank this column in ascending order +dataTable_sSortDescending=: Rank this column in descending order + +## login +login_btn=Login +login_remember_me=Remember Me +login_username_placeholder=Please enter username +login_password_placeholder=Please enter password +login_username_empty=Please enter username +login_username_lt_4=Username length should not be less than 4 +login_password_empty=Please enter password +login_password_lt_4=Password length should not be less than 4 +login_success=Login success +login_fail=Login fail +login_param_empty=Username or password is empty +login_param_unvalid=Username or password error + +## logout +logout_btn=Logout +logout_confirm=Confirm logout? +logout_success=Logout success +logout_fail=Logout fail + +## change pwd +change_pwd=Change password +change_pwd_suc_to_logout=Change password successful, about to log out login +change_pwd_field_newpwd=new password + +## dashboard +job_dashboard_name=Run report +job_dashboard_job_num=Job number +job_dashboard_job_num_tip=The number of tasks running in the scheduling center +job_dashboard_trigger_num=trigger number +job_dashboard_trigger_num_tip=The number of trigger record scheduled by the scheduling center +job_dashboard_jobgroup_num=Executor number +job_dashboard_jobgroup_num_tip=The number of online executor machines perceived by the scheduling center +job_dashboard_report=Scheduling report +job_dashboard_report_loaddata_fail=Scheduling report load data error +job_dashboard_date_report=Date distribution +job_dashboard_rate_report=Percentage distribution + +## job info +jobinfo_name=Job Manage +jobinfo_job=Job +jobinfo_field_add=Add Job +jobinfo_field_update=Edit Job +jobinfo_field_id=Job ID +jobinfo_field_jobgroup=Executor +jobinfo_field_jobdesc=Job description +jobinfo_field_timeout=Job timeout period +jobinfo_field_gluetype=GLUE Type +jobinfo_field_executorparam=Param +jobinfo_field_author=Author +jobinfo_field_alarmemail=Alarm email +jobinfo_field_alarmemail_placeholder=Please enter alarm mail, if there are more than one comma separated +jobinfo_field_executorRouteStrategy=Route Strategy +jobinfo_field_childJobId=Child Job ID +jobinfo_field_childJobId_placeholder=Please enter the Child job ID, if there are more than one comma separated +jobinfo_field_executorBlockStrategy=Block Strategy +jobinfo_field_executorFailRetryCount=Fail Retry Count +jobinfo_field_executorFailRetryCount_placeholder=Fail Retry Count. effect if greater than zero +jobinfo_script_location=Script location +jobinfo_shard_index=Shard index +jobinfo_shard_total=Shard total +jobinfo_opt_stop=Stop +jobinfo_opt_start=Start +jobinfo_opt_log=Query Log +jobinfo_opt_run=Run Once +jobinfo_opt_run_tips=Please input the address for this trigger. Null will be obtained from the executor +jobinfo_opt_registryinfo=Registry Info +jobinfo_opt_next_time=Next trigger time +jobinfo_glue_remark=Resource Remark +jobinfo_glue_remark_limit=Resource Remark length is limited to 4~100 +jobinfo_glue_rollback=Version Backtrack +jobinfo_glue_jobid_unvalid=Job ID is illegal +jobinfo_glue_gluetype_unvalid=The job is not GLUE Type +jobinfo_field_executorTimeout_placeholder=Job Timeout period,in seconds. effect if greater than zero +schedule_type=Schedule Type +schedule_type_none=None +schedule_type_cron=Cron +schedule_type_fix_rate=Fix rate +schedule_type_fix_delay=Fix delay +schedule_type_none_limit_start=The current schedule type disables startup +misfire_strategy=Misfire strategy +misfire_strategy_do_nothing=Do nothing +misfire_strategy_fire_once_now=Fire once now +jobinfo_conf_base=Base configuration +jobinfo_conf_schedule=Schedule configuration +jobinfo_conf_job=Job configuration +jobinfo_conf_advanced=Advanced configuration + +## job log +joblog_name=Trigger Log +joblog_status=Status +joblog_status_all=All +joblog_status_suc=Success +joblog_status_fail=Fail +joblog_status_running=Running +joblog_field_triggerTime=Trigger Time +joblog_field_triggerCode=Trigger Result +joblog_field_triggerMsg=Trigger Msg +joblog_field_handleTime=Handle Time +joblog_field_handleCode=Handle Result +joblog_field_handleMsg=Trigger Msg +joblog_field_executorAddress=Executor Address +joblog_clean=Clean +joblog_clean_log=Clean Log +joblog_clean_type=Clean Type +joblog_clean_type_1=Clean up log data a month ago +joblog_clean_type_2=Clean up log data three month ago +joblog_clean_type_3=Clean up log data six month ago +joblog_clean_type_4=Clean up log data a year ago +joblog_clean_type_5=Clean up log data a thousand record ago +joblog_clean_type_6=Clean up log data ten thousand record ago +joblog_clean_type_7=Clean up log data thirty thousand record ago +joblog_clean_type_8=Clean up log data hundred thousand record ago +joblog_clean_type_9=Clean up all log data +joblog_clean_type_unvalid=Clean type is illegal +joblog_handleCode_200=Success +joblog_handleCode_500=Fail +joblog_handleCode_502=Timeout +joblog_kill_log=Kill Job +joblog_kill_log_limit=Trigger Fail, can not kill job +joblog_kill_log_byman=Manual operation, kill job +joblog_lost_fail=Job result lost, marked as failure +joblog_rolling_log=Rolling log +joblog_rolling_log_refresh=Refresh +joblog_rolling_log_triggerfail=The job trigger fail, can not view the rolling log +joblog_rolling_log_failoften=The request for the Rolling log is terminated, the number of failed requests exceeds the limit, Reload the log on the refresh page +joblog_logid_unvalid=Log ID is illegal + +## job group +jobgroup_name=Executor Manage +jobgroup_list=Executor List +jobgroup_add=Add Executor +jobgroup_edit=Edit Executor +jobgroup_del=Delete Executor +jobgroup_field_title=Title +jobgroup_field_addressType=Registry Type +jobgroup_field_addressType_0=Automatic registration +jobgroup_field_addressType_1=Manual registration +jobgroup_field_addressType_limit=Manually registration type, the machine address must not be empty +jobgroup_field_registryList=machine address +jobgroup_field_registryList_unvalid=registry machine address is illegal +jobgroup_field_registryList_placeholder=Please enter the machine address, if there are more than one comma separated +jobgroup_field_appname_limit=Limit the beginning of a lowercase letter, consists of lowercase letters、number and hyphen. +jobgroup_field_appname_length=AppName length is limited to 4~64 +jobgroup_field_title_length=Title length is limited to 4~12 +jobgroup_field_order_digits=Please enter a positive integer +jobgroup_field_orderrange=Order is limited to 1~1000 +jobgroup_del_limit_0=Refuse to delete, the executor is being used +jobgroup_del_limit_1=Refuses to delete, the system retains at least one executor +jobgroup_empty=There is no valid executor. Please contact the administrator + +## job conf +jobconf_block_SERIAL_EXECUTION=Serial execution +jobconf_block_DISCARD_LATER=Discard Later +jobconf_block_COVER_EARLY=Cover Early +jobconf_route_first=First +jobconf_route_last=Last +jobconf_route_round=Round +jobconf_route_random=Random +jobconf_route_consistenthash=Consistent Hash +jobconf_route_lfu=Least Frequently Used +jobconf_route_lru=Least Recently Used +jobconf_route_failover=Failover +jobconf_route_busyover=Busyover +jobconf_route_shard=Sharding Broadcast +jobconf_idleBeat=Idle check +jobconf_beat=Heartbeats +jobconf_monitor=Task Scheduling Center monitor alarm +jobconf_monitor_detail=monitor alarm details +jobconf_monitor_alarm_title=Alarm Type +jobconf_monitor_alarm_type=Trigger Fail +jobconf_monitor_alarm_content=Alarm Content +jobconf_trigger_admin_adress=Trigger machine address +jobconf_trigger_exe_regtype=Execotor-Registry Type +jobconf_trigger_exe_regaddress=Execotor-Registry Address +jobconf_trigger_address_empty=Trigger Fail:registry address is empty +jobconf_trigger_run=Trigger Job +jobconf_trigger_child_run=Trigger child job +jobconf_callback_child_msg1={0}/{1} [Job ID={2}], Trigger {3}, Trigger msg: {4}
+jobconf_callback_child_msg2={0}/{1} [Job ID={2}], Trigger Fail, Trigger msg: Job ID is illegal
+jobconf_trigger_type=Job trigger type +jobconf_trigger_type_cron=Cron trigger +jobconf_trigger_type_manual=Manual trigger +jobconf_trigger_type_parent=Parent job trigger +jobconf_trigger_type_api=Api trigger +jobconf_trigger_type_retry=Fail retry trigger +jobconf_trigger_type_misfire=Misfire compensation trigger + +## user +user_manage=User Manage +user_username=Username +user_password=Password +user_role=Role +user_role_admin=Admin User +user_role_normal=Normal User +user_permission=Permission +user_add=Add User +user_update=Edit User +user_username_repeat=Username Repeat +user_username_valid=Restrictions start with a lowercase letter and consist of lowercase letters and Numbers +user_password_update_placeholder=Please input password, empty means not update +user_update_loginuser_limit=Operation of current login account is not allowed + +## help +job_help=Tutorial +job_help_document=Official Document diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_zh_CN.properties b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_zh_CN.properties new file mode 100644 index 0000000..ccc4112 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_zh_CN.properties @@ -0,0 +1,276 @@ +admin_name=任务调度中心 +admin_name_full=分布式任务调度平台XXL-JOB +admin_version=2.4.2-SNAPSHOT +admin_i18n= + +## system +system_tips=系统提示 +system_ok=确定 +system_close=关闭 +system_save=保存 +system_cancel=取消 +system_search=搜索 +system_status=状态 +system_opt=操作 +system_please_input=请输入 +system_please_choose=请选择 +system_success=成功 +system_fail=失败 +system_add_suc=新增成功 +system_add_fail=新增失败 +system_update_suc=更新成功 +system_update_fail=更新失败 +system_all=全部 +system_api_error=接口异常 +system_show=查看 +system_empty=无 +system_opt_suc=操作成功 +system_opt_fail=操作失败 +system_opt_edit=编辑 +system_opt_del=删除 +system_opt_copy=复制 +system_unvalid=非法 +system_not_found=不存在 +system_nav=导航 +system_digits=整数 +system_lengh_limit=长度限制 +system_permission_limit=权限拦截 +system_welcome=欢迎 + +## daterangepicker +daterangepicker_ranges_recent_hour=最近一小时 +daterangepicker_ranges_today=今日 +daterangepicker_ranges_yesterday=昨日 +daterangepicker_ranges_this_month=本月 +daterangepicker_ranges_last_month=上个月 +daterangepicker_ranges_recent_week=最近一周 +daterangepicker_ranges_recent_month=最近一月 +daterangepicker_custom_name=自定义 +daterangepicker_custom_starttime=起始时间 +daterangepicker_custom_endtime=结束时间 +daterangepicker_custom_daysofweek=日,一,二,三,四,五,六 +daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing=处理中... +dataTable_sLengthMenu=每页 _MENU_ 条记录 +dataTable_sZeroRecords=没有匹配结果 +dataTable_sInfo=第 _PAGE_ 页 ( 总共 _PAGES_ 页,_TOTAL_ 条记录 ) +dataTable_sInfoEmpty=无记录 +dataTable_sInfoFiltered=(由 _MAX_ 项结果过滤) +dataTable_sSearch=搜索 +dataTable_sEmptyTable=表中数据为空 +dataTable_sLoadingRecords=载入中... +dataTable_sFirst=首页 +dataTable_sPrevious=上页 +dataTable_sNext=下页 +dataTable_sLast=末页 +dataTable_sSortAscending=: 以升序排列此列 +dataTable_sSortDescending=: 以降序排列此列 + +## login +login_btn=登录 +login_remember_me=记住密码 +login_username_placeholder=请输入登录账号 +login_password_placeholder=请输入登录密码 +login_username_empty=请输入登录账号 +login_username_lt_4=登录账号不应低于4位 +login_password_empty=请输入登录密码 +login_password_lt_4=登录密码不应低于4位 +login_success=登录成功 +login_fail=登录失败 +login_param_empty=账号或密码为空 +login_param_unvalid=账号或密码错误 + +## logout +logout_btn=注销 +logout_confirm=确认注销登录? +logout_success=注销成功 +logout_fail=注销失败 + +## change pwd +change_pwd=修改密码 +change_pwd_suc_to_logout=修改密码成功,即将注销登陆 +change_pwd_field_newpwd=新密码 + +## dashboard +job_dashboard_name=运行报表 +job_dashboard_job_num=任务数量 +job_dashboard_job_num_tip=调度中心运行的任务数量 +job_dashboard_trigger_num=调度次数 +job_dashboard_trigger_num_tip=调度中心触发的调度次数 +job_dashboard_jobgroup_num=执行器数量 +job_dashboard_jobgroup_num_tip=调度中心在线的执行器机器数量 +job_dashboard_report=调度报表 +job_dashboard_report_loaddata_fail=调度报表数据加载异常 +job_dashboard_date_report=日期分布图 +job_dashboard_rate_report=成功比例图 + +## job info +jobinfo_name=任务管理 +jobinfo_job=任务 +jobinfo_field_add=新增 +jobinfo_field_update=更新任务 +jobinfo_field_id=任务ID +jobinfo_field_jobgroup=执行器 +jobinfo_field_jobdesc=任务描述 +jobinfo_field_gluetype=运行模式 +jobinfo_field_executorparam=任务参数 +jobinfo_field_author=负责人 +jobinfo_field_timeout=任务超时时间 +jobinfo_field_alarmemail=报警邮件 +jobinfo_field_alarmemail_placeholder=请输入报警邮件,多个邮件地址则逗号分隔 +jobinfo_field_executorRouteStrategy=路由策略 +jobinfo_field_childJobId=子任务ID +jobinfo_field_childJobId_placeholder=请输入子任务的任务ID,如存在多个则逗号分隔 +jobinfo_field_executorBlockStrategy=阻塞处理策略 +jobinfo_field_executorFailRetryCount=失败重试次数 +jobinfo_field_executorFailRetryCount_placeholder=失败重试次数,大于零时生效 +jobinfo_script_location=脚本位置 +jobinfo_shard_index=分片序号 +jobinfo_shard_total=分片总数 +jobinfo_opt_stop=停止 +jobinfo_opt_start=启动 +jobinfo_opt_log=查询日志 +jobinfo_opt_run=执行一次 +jobinfo_opt_run_tips=请输入本次执行的机器地址,为空则从执行器获取 +jobinfo_opt_registryinfo=注册节点 +jobinfo_opt_next_time=下次执行时间 +jobinfo_glue_remark=源码备注 +jobinfo_glue_remark_limit=源码备注长度限制为4~100 +jobinfo_glue_rollback=版本回溯 +jobinfo_glue_jobid_unvalid=任务ID非法 +jobinfo_glue_gluetype_unvalid=该任务非GLUE模式 +jobinfo_field_executorTimeout_placeholder=任务超时时间,单位秒,大于零时生效 +schedule_type=调度类型 +schedule_type_none=无 +schedule_type_cron=CRON +schedule_type_fix_rate=固定速度 +schedule_type_fix_delay=固定延迟 +schedule_type_none_limit_start=当前调度类型禁止启动 +misfire_strategy=调度过期策略 +misfire_strategy_do_nothing=忽略 +misfire_strategy_fire_once_now=立即执行一次 +jobinfo_conf_base=基础配置 +jobinfo_conf_schedule=调度配置 +jobinfo_conf_job=任务配置 +jobinfo_conf_advanced=高级配置 + +## job log +joblog_name=调度日志 +joblog_status=状态 +joblog_status_all=全部 +joblog_status_suc=成功 +joblog_status_fail=失败 +joblog_status_running=进行中 +joblog_field_triggerTime=调度时间 +joblog_field_triggerCode=调度结果 +joblog_field_triggerMsg=调度备注 +joblog_field_handleTime=执行时间 +joblog_field_handleCode=执行结果 +joblog_field_handleMsg=执行备注 +joblog_field_executorAddress=执行器地址 +joblog_clean=清理 +joblog_clean_log=日志清理 +joblog_clean_type=清理方式 +joblog_clean_type_1=清理一个月之前日志数据 +joblog_clean_type_2=清理三个月之前日志数据 +joblog_clean_type_3=清理六个月之前日志数据 +joblog_clean_type_4=清理一年之前日志数据 +joblog_clean_type_5=清理一千条以前日志数据 +joblog_clean_type_6=清理一万条以前日志数据 +joblog_clean_type_7=清理三万条以前日志数据 +joblog_clean_type_8=清理十万条以前日志数据 +joblog_clean_type_9=清理所有日志数据 +joblog_clean_type_unvalid=清理类型参数异常 +joblog_handleCode_200=成功 +joblog_handleCode_500=失败 +joblog_handleCode_502=失败(超时) +joblog_kill_log=终止任务 +joblog_kill_log_limit=调度失败,无法终止日志 +joblog_kill_log_byman=人为操作,主动终止 +joblog_lost_fail=任务结果丢失,标记失败 +joblog_rolling_log=执行日志 +joblog_rolling_log_refresh=刷新 +joblog_rolling_log_triggerfail=任务发起调度失败,无法查看执行日志 +joblog_rolling_log_failoften=终止请求Rolling日志,请求失败次数超上限,可刷新页面重新加载日志 +joblog_logid_unvalid=日志ID非法 + +## job group +jobgroup_name=执行器管理 +jobgroup_list=执行器列表 +jobgroup_add=新增执行器 +jobgroup_edit=编辑执行器 +jobgroup_del=删除执行器 +jobgroup_field_title=名称 +jobgroup_field_addressType=注册方式 +jobgroup_field_addressType_0=自动注册 +jobgroup_field_addressType_1=手动录入 +jobgroup_field_addressType_limit=手动录入注册方式,机器地址不可为空 +jobgroup_field_registryList=机器地址 +jobgroup_field_registryList_unvalid=机器地址格式非法 +jobgroup_field_registryList_placeholder=请输入执行器地址列表,多地址逗号分隔 +jobgroup_field_appname_limit=限制以小写字母开头,由小写字母、数字和中划线组成 +jobgroup_field_appname_length=AppName长度限制为4~64 +jobgroup_field_title_length=名称长度限制为4~12 +jobgroup_field_order_digits=请输入整数 +jobgroup_field_orderrange=取值范围为1~1000 +jobgroup_del_limit_0=拒绝删除,该执行器使用中 +jobgroup_del_limit_1=拒绝删除, 系统至少保留一个执行器 +jobgroup_empty=不存在有效执行器,请联系管理员 + +## job conf +jobconf_block_SERIAL_EXECUTION=单机串行 +jobconf_block_DISCARD_LATER=丢弃后续调度 +jobconf_block_COVER_EARLY=覆盖之前调度 +jobconf_route_first=第一个 +jobconf_route_last=最后一个 +jobconf_route_round=轮询 +jobconf_route_random=随机 +jobconf_route_consistenthash=一致性HASH +jobconf_route_lfu=最不经常使用 +jobconf_route_lru=最近最久未使用 +jobconf_route_failover=故障转移 +jobconf_route_busyover=忙碌转移 +jobconf_route_shard=分片广播 +jobconf_idleBeat=空闲检测 +jobconf_beat=心跳检测 +jobconf_monitor=任务调度中心监控报警 +jobconf_monitor_detail=监控告警明细 +jobconf_monitor_alarm_title=告警类型 +jobconf_monitor_alarm_type=调度失败 +jobconf_monitor_alarm_content=告警内容 +jobconf_trigger_admin_adress=调度机器 +jobconf_trigger_exe_regtype=执行器-注册方式 +jobconf_trigger_exe_regaddress=执行器-地址列表 +jobconf_trigger_address_empty=调度失败:执行器地址为空 +jobconf_trigger_run=触发调度 +jobconf_trigger_child_run=触发子任务 +jobconf_callback_child_msg1={0}/{1} [任务ID={2}], 触发{3}, 触发备注: {4}
+jobconf_callback_child_msg2={0}/{1} [任务ID={2}], 触发失败, 触发备注: 任务ID格式错误
+jobconf_trigger_type=任务触发类型 +jobconf_trigger_type_cron=Cron触发 +jobconf_trigger_type_manual=手动触发 +jobconf_trigger_type_parent=父任务触发 +jobconf_trigger_type_api=API触发 +jobconf_trigger_type_retry=失败重试触发 +jobconf_trigger_type_misfire=调度过期补偿 + +## user +user_manage=用户管理 +user_username=账号 +user_password=密码 +user_role=角色 +user_role_admin=管理员 +user_role_normal=普通用户 +user_permission=权限 +user_add=新增用户 +user_update=更新用户 +user_username_repeat=账号重复 +user_username_valid=限制以小写字母开头,由小写字母、数字组成 +user_password_update_placeholder=请输入新密码,为空则不更新密码 +user_update_loginuser_limit=禁止操作当前登录账号 + +## help +job_help=使用教程 +job_help_document=官方文档 \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_zh_TC.properties b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_zh_TC.properties new file mode 100644 index 0000000..9a8d008 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/i18n/message_zh_TC.properties @@ -0,0 +1,276 @@ +admin_name=任務調度中心 +admin_name_full=分布式任務調度平臺XXL-JOB +admin_version=2.4.2-SNAPSHOT +admin_i18n= + +## system +system_tips=系統提示 +system_ok=確定 +system_close=關閉 +system_save=儲存 +system_cancel=取消 +system_search=搜尋 +system_status=狀態 +system_opt=操作 +system_please_input=請輸入 +system_please_choose=请選擇 +system_success=成功 +system_fail=失敗 +system_add_suc=新增成功 +system_add_fail=新增失敗 +system_update_suc=更新成功 +system_update_fail=更新失敗 +system_all=全部 +system_api_error=API錯誤 +system_show=查看 +system_empty=無 +system_opt_suc=操作成功 +system_opt_fail=操作失敗 +system_opt_edit=編輯 +system_opt_del=刪除 +system_opt_copy=復制 +system_unvalid=非法 +system_not_found=不存在 +system_nav=導航 +system_digits=整數 +system_lengh_limit=長度限制 +system_permission_limit=權限控管 +system_welcome=歡迎 + +## daterangepicker +daterangepicker_ranges_recent_hour=最近一小時 +daterangepicker_ranges_today=今日 +daterangepicker_ranges_yesterday=昨日 +daterangepicker_ranges_this_month=本月 +daterangepicker_ranges_last_month=上個月 +daterangepicker_ranges_recent_week=最近一周 +daterangepicker_ranges_recent_month=最近一月 +daterangepicker_custom_name=自定義 +daterangepicker_custom_starttime=起始時間 +daterangepicker_custom_endtime=結束時間 +daterangepicker_custom_daysofweek=日,一,二,三,四,五,六 +daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing=處理中... +dataTable_sLengthMenu=每頁 _MENU_ 條記錄 +dataTable_sZeroRecords=沒有相符合記錄 +dataTable_sInfo=第 _PAGE_ 頁 ( 總共 _PAGES_ 頁,_TOTAL_ 條記錄 ) +dataTable_sInfoEmpty=無記錄 +dataTable_sInfoFiltered=(由 _MAX_ 項結果過濾) +dataTable_sSearch=搜尋 +dataTable_sEmptyTable=表中資料為空 +dataTable_sLoadingRecords=載入中... +dataTable_sFirst=首頁 +dataTable_sPrevious=上頁 +dataTable_sNext=下頁 +dataTable_sLast=末頁 +dataTable_sSortAscending=: 以升幂排序此列 +dataTable_sSortDescending=: 以降幂排序此列 + +## login +login_btn=登入 +login_remember_me=記住密碼 +login_username_placeholder=請輸入登入帳號 +login_password_placeholder=請輸入登入密碼 +login_username_empty=請輸入登入帳號 +login_username_lt_4=登入帳號不應低於4位數 +login_password_empty=請輸入登入密碼 +login_password_lt_4=登入密碼不應低於4位數 +login_success=登入成功 +login_fail=登入失敗 +login_param_empty=帳號或密碼為空值 +login_param_unvalid=帳號或密碼錯誤 + +## logout +logout_btn=登出 +logout_confirm=確認登出? +logout_success=登出成功 +logout_fail=登出失敗 + +## change pwd +change_pwd=修改密碼 +change_pwd_suc_to_logout=修改密碼成功,即將登出 +change_pwd_field_newpwd=新密碼 + +## dashboard +job_dashboard_name=運行報表 +job_dashboard_job_num=任務數量 +job_dashboard_job_num_tip=調度中心運行的任務數量 +job_dashboard_trigger_num=調度次數 +job_dashboard_trigger_num_tip=調度中心觸發的調度次數 +job_dashboard_jobgroup_num=執行器數量 +job_dashboard_jobgroup_num_tip=調度中心在線的執行器機器數量 +job_dashboard_report=調度報表 +job_dashboard_report_loaddata_fail=調度報表資料加載異常 +job_dashboard_date_report=日期分布圖 +job_dashboard_rate_report=成功比例圖 + +## job info +jobinfo_name=任務管理 +jobinfo_job=任務 +jobinfo_field_add=新增 +jobinfo_field_update=更新任務 +jobinfo_field_id=任務ID +jobinfo_field_jobgroup=執行器 +jobinfo_field_jobdesc=任務描述 +jobinfo_field_gluetype=運行模式 +jobinfo_field_executorparam=任務參數 +jobinfo_field_author=負責人 +jobinfo_field_timeout=任務超時秒數 +jobinfo_field_alarmemail=告警郵件 +jobinfo_field_alarmemail_placeholder=輸入多個告警郵件地址,請以逗號分隔 +jobinfo_field_executorRouteStrategy=路由策略 +jobinfo_field_childJobId=子任務ID +jobinfo_field_childJobId_placeholder=輸入子任務ID,如有多個請以逗號分隔 +jobinfo_field_executorBlockStrategy=阻塞處理策略 +jobinfo_field_executorFailRetryCount=失敗重試次數 +jobinfo_field_executorFailRetryCount_placeholder=失敗重試次數,大於零時生效 +jobinfo_script_location=腳本位置 +jobinfo_shard_index=分片序號 +jobinfo_shard_total=分片總數 +jobinfo_opt_stop=停止 +jobinfo_opt_start=啟動 +jobinfo_opt_log=查詢日誌 +jobinfo_opt_run=執行一次 +jobinfo_opt_run_tips=請輸入本次執行的機器地址,為空則從執行器獲取 +jobinfo_opt_registryinfo=注冊節點 +jobinfo_opt_next_time=下次執行時間 +jobinfo_glue_remark=源碼備註 +jobinfo_glue_remark_limit=源碼備註長度限制為4~100 +jobinfo_glue_rollback=版本回復 +jobinfo_glue_jobid_unvalid=任務ID非法 +jobinfo_glue_gluetype_unvalid=該任務非GLUE模式 +jobinfo_field_executorTimeout_placeholder=任務超時時間,單位秒,大於零時生效 +schedule_type=調度類型 +schedule_type_none=無 +schedule_type_cron=CRON +schedule_type_fix_rate=固定速度 +schedule_type_fix_delay=固定延遲 +schedule_type_none_limit_start=當前調度類型禁止啟動 +misfire_strategy=調度過期策略 +misfire_strategy_do_nothing=忽略 +misfire_strategy_fire_once_now=立即執行壹次 +jobinfo_conf_base=基礎配置 +jobinfo_conf_schedule=調度配置 +jobinfo_conf_job=任務配置 +jobinfo_conf_advanced=高級配置 + +## job log +joblog_name=調度日誌 +joblog_status=狀態 +joblog_status_all=全部 +joblog_status_suc=成功 +joblog_status_fail=失敗 +joblog_status_running=進行中 +joblog_field_triggerTime=調度時間 +joblog_field_triggerCode=調度結果 +joblog_field_triggerMsg=調度備註 +joblog_field_handleTime=執行時間 +joblog_field_handleCode=執行结果 +joblog_field_handleMsg=執行備註 +joblog_field_executorAddress=執行器地址 +joblog_clean=清理 +joblog_clean_log=日誌清理 +joblog_clean_type=清理方式 +joblog_clean_type_1=清理一個月之前日誌資料 +joblog_clean_type_2=清理三個月之前日誌資料 +joblog_clean_type_3=清理六個月之前日誌資料 +joblog_clean_type_4=清理一年之前日誌資料 +joblog_clean_type_5=清理一千條以前日誌資料 +joblog_clean_type_6=清理一萬條以前日誌資料 +joblog_clean_type_7=清理三萬條以前日誌資料 +joblog_clean_type_8=清理十萬條以前日誌資料 +joblog_clean_type_9=清理所有日誌資料 +joblog_clean_type_unvalid=清理類型參数異常 +joblog_handleCode_200=成功 +joblog_handleCode_500=失敗 +joblog_handleCode_502=失敗(超時) +joblog_kill_log=终止任務 +joblog_kill_log_limit=調度失敗,無法终止日誌 +joblog_kill_log_byman=人為操作,主動終止 +joblog_lost_fail=任務結果丟失,標記失敗 +joblog_rolling_log=執行日誌 +joblog_rolling_log_refresh=更新 +joblog_rolling_log_triggerfail=任務發起調度失敗,無法查看執行日誌 +joblog_rolling_log_failoften=終止請求Rolling日誌,請求失敗次數超上限,可刷新頁面重新加載日誌 +joblog_logid_unvalid=日誌ID非法 + +## job group +jobgroup_name=執行器管理 +jobgroup_list=執行器列表 +jobgroup_add=新增執行器 +jobgroup_edit=編輯執行器 +jobgroup_del=刪除執行器 +jobgroup_field_title=名稱 +jobgroup_field_addressType=注冊方式 +jobgroup_field_addressType_0=自動注冊 +jobgroup_field_addressType_1=手動登錄 +jobgroup_field_addressType_limit=手動登錄注冊方式,機器地址不可為空 +jobgroup_field_registryList=機器地址 +jobgroup_field_registryList_unvalid=機器地址格式非法 +jobgroup_field_registryList_placeholder=請輸入執行器地址列表,多個地址請以逗號分隔 +jobgroup_field_appname_limit=限制以小寫字母開頭,由小寫字母、數字和中划線組成 +jobgroup_field_appname_length=AppName長度限制為4~64 +jobgroup_field_title_length=名稱長度限制為4~12 +jobgroup_field_order_digits=請輸入整數 +jobgroup_field_orderrange=取值範圍為1~1000 +jobgroup_del_limit_0=拒絕刪除,該執行器使用中 +jobgroup_del_limit_1=拒絕删除,系统至少保留一個執行器 +jobgroup_empty=不存在有效執行器,請聯絡系統管理員 + +## job conf +jobconf_block_SERIAL_EXECUTION=單機串行 +jobconf_block_DISCARD_LATER=丢棄后續調度 +jobconf_block_COVER_EARLY=覆蓋之前調度 +jobconf_route_first=第一個 +jobconf_route_last=最後一個 +jobconf_route_round=輪詢 +jobconf_route_random=隨機 +jobconf_route_consistenthash=一致性HASH +jobconf_route_lfu=最不經常使用 +jobconf_route_lru=最近最久未使用 +jobconf_route_failover=故障轉移 +jobconf_route_busyover=忙碌轉移 +jobconf_route_shard=分片廣播 +jobconf_idleBeat=空閒檢測 +jobconf_beat=心跳檢測 +jobconf_monitor=任務調度中心監控告警 +jobconf_monitor_detail=監控告警明细 +jobconf_monitor_alarm_title=告警類型 +jobconf_monitor_alarm_type=調度失敗 +jobconf_monitor_alarm_content=告警内容 +jobconf_trigger_admin_adress=調度機器 +jobconf_trigger_exe_regtype=執行器-注冊方式 +jobconf_trigger_exe_regaddress=執行器-地址列表 +jobconf_trigger_address_empty=調度失敗:執行器地址為空 +jobconf_trigger_run=觸發調度 +jobconf_trigger_child_run=觸發子任務 +jobconf_callback_child_msg1={0}/{1} [任務ID={2}], 觸發{3}, 觸發備註: {4}
+jobconf_callback_child_msg2={0}/{1} [任務ID={2}], 觸發失败, 觸發備註: 任務ID格式錯誤
+jobconf_trigger_type=任務觸發類型 +jobconf_trigger_type_cron=Cron觸發 +jobconf_trigger_type_manual=手動觸發 +jobconf_trigger_type_parent=父任務觸發 +jobconf_trigger_type_api=API觸發 +jobconf_trigger_type_retry=失敗重試觸發 +jobconf_trigger_type_misfire=調度過期補償 + +## user +user_manage=用户管理 +user_username=帳號 +user_password=密碼 +user_role=角色 +user_role_admin=管理員 +user_role_normal=普通用戶 +user_permission=權限 +user_add=新增用戶 +user_update=更新用戶 +user_username_repeat=帳號重複 +user_username_valid=限制以小寫字母開頭,由小寫字母、數字組成 +user_password_update_placeholder=請輸入新密碼,為空則不更新密碼 +user_update_loginuser_limit=禁止操作當前登入帳號 + +## help +job_help=使用教程 +job_help_document=官方文件 \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/logback.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/logback.xml new file mode 100644 index 0000000..d4b08c2 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/logback.xml @@ -0,0 +1,29 @@ + + + + logback + + + + + %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n + + + + + ${log.path} + + ${log.path}.%d{yyyy-MM-dd}.zip + + + %date %level [%thread] %logger{36} [%file : %line] %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml new file mode 100644 index 0000000..87299f8 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + t.id, + t.app_name, + t.title, + t.address_type, + t.address_list, + t.update_time + + + + + + + + INSERT INTO xxl_job_group ( `app_name`, `title`, `address_type`, `address_list`, `update_time`) + values ( #{appname}, #{title}, #{addressType}, #{addressList}, #{updateTime} ); + + + + UPDATE xxl_job_group + SET `app_name` = #{appname}, + `title` = #{title}, + `address_type` = #{addressType}, + `address_list` = #{addressList}, + `update_time` = #{updateTime} + WHERE id = #{id} + + + + DELETE FROM xxl_job_group + WHERE id = #{id} + + + + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml new file mode 100644 index 0000000..7b3c3a3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + t.id, + t.job_group, + t.job_desc, + t.add_time, + t.update_time, + t.author, + t.alarm_email, + t.schedule_type, + t.schedule_conf, + t.misfire_strategy, + t.executor_route_strategy, + t.executor_handler, + t.executor_param, + t.executor_block_strategy, + t.executor_timeout, + t.executor_fail_retry_count, + t.glue_type, + t.glue_source, + t.glue_remark, + t.glue_updatetime, + t.child_jobid, + t.trigger_status, + t.trigger_last_time, + t.trigger_next_time + + + + + + + + INSERT INTO xxl_job_info ( + job_group, + job_desc, + add_time, + update_time, + author, + alarm_email, + schedule_type, + schedule_conf, + misfire_strategy, + executor_route_strategy, + executor_handler, + executor_param, + executor_block_strategy, + executor_timeout, + executor_fail_retry_count, + glue_type, + glue_source, + glue_remark, + glue_updatetime, + child_jobid, + trigger_status, + trigger_last_time, + trigger_next_time + ) VALUES ( + #{jobGroup}, + #{jobDesc}, + #{addTime}, + #{updateTime}, + #{author}, + #{alarmEmail}, + #{scheduleType}, + #{scheduleConf}, + #{misfireStrategy}, + #{executorRouteStrategy}, + #{executorHandler}, + #{executorParam}, + #{executorBlockStrategy}, + #{executorTimeout}, + #{executorFailRetryCount}, + #{glueType}, + #{glueSource}, + #{glueRemark}, + #{glueUpdatetime}, + #{childJobId}, + #{triggerStatus}, + #{triggerLastTime}, + #{triggerNextTime} + ); + + + + + + + UPDATE xxl_job_info + SET + job_group = #{jobGroup}, + job_desc = #{jobDesc}, + update_time = #{updateTime}, + author = #{author}, + alarm_email = #{alarmEmail}, + schedule_type = #{scheduleType}, + schedule_conf = #{scheduleConf}, + misfire_strategy = #{misfireStrategy}, + executor_route_strategy = #{executorRouteStrategy}, + executor_handler = #{executorHandler}, + executor_param = #{executorParam}, + executor_block_strategy = #{executorBlockStrategy}, + executor_timeout = ${executorTimeout}, + executor_fail_retry_count = ${executorFailRetryCount}, + glue_type = #{glueType}, + glue_source = #{glueSource}, + glue_remark = #{glueRemark}, + glue_updatetime = #{glueUpdatetime}, + child_jobid = #{childJobId}, + trigger_status = #{triggerStatus}, + trigger_last_time = #{triggerLastTime}, + trigger_next_time = #{triggerNextTime} + WHERE id = #{id} + + + + DELETE + FROM xxl_job_info + WHERE id = #{id} + + + + + + + + + + + UPDATE xxl_job_info + SET + trigger_last_time = #{triggerLastTime}, + trigger_next_time = #{triggerNextTime}, + trigger_status = #{triggerStatus} + WHERE id = #{id} + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml new file mode 100644 index 0000000..699277c --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + t.id, + t.job_id, + t.glue_type, + t.glue_source, + t.glue_remark, + t.add_time, + t.update_time + + + + INSERT INTO xxl_job_logglue ( + `job_id`, + `glue_type`, + `glue_source`, + `glue_remark`, + `add_time`, + `update_time` + ) VALUES ( + #{jobId}, + #{glueType}, + #{glueSource}, + #{glueRemark}, + #{addTime}, + #{updateTime} + ); + + + + + + + DELETE FROM xxl_job_logglue + WHERE id NOT in( + SELECT id FROM( + SELECT id FROM xxl_job_logglue + WHERE `job_id` = #{jobId} + ORDER BY update_time desc + LIMIT 0, #{limit} + ) t1 + ) AND `job_id` = #{jobId} + + + + DELETE FROM xxl_job_logglue + WHERE `job_id` = #{jobId} + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml new file mode 100644 index 0000000..4155f17 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + t.id, + t.job_group, + t.job_id, + t.executor_address, + t.executor_handler, + t.executor_param, + t.executor_sharding_param, + t.executor_fail_retry_count, + t.trigger_time, + t.trigger_code, + t.trigger_msg, + t.handle_time, + t.handle_code, + t.handle_msg, + t.alarm_status + + + + + + + + + + + INSERT INTO xxl_job_log ( + `job_group`, + `job_id`, + `trigger_time`, + `trigger_code`, + `handle_code` + ) VALUES ( + #{jobGroup}, + #{jobId}, + #{triggerTime}, + #{triggerCode}, + #{handleCode} + ); + + + + + UPDATE xxl_job_log + SET + `trigger_time`= #{triggerTime}, + `trigger_code`= #{triggerCode}, + `trigger_msg`= #{triggerMsg}, + `executor_address`= #{executorAddress}, + `executor_handler`=#{executorHandler}, + `executor_param`= #{executorParam}, + `executor_sharding_param`= #{executorShardingParam}, + `executor_fail_retry_count`= #{executorFailRetryCount} + WHERE `id`= #{id} + + + + UPDATE xxl_job_log + SET + `handle_time`= #{handleTime}, + `handle_code`= #{handleCode}, + `handle_msg`= #{handleMsg} + WHERE `id`= #{id} + + + + delete from xxl_job_log + WHERE job_id = #{jobId} + + + + + + + + + + delete from xxl_job_log + WHERE id in + + #{item} + + + + + + + UPDATE xxl_job_log + SET + `alarm_status` = #{newAlarmStatus} + WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus} + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml new file mode 100644 index 0000000..579d5f3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + t.id, + t.trigger_day, + t.running_count, + t.suc_count, + t.fail_count + + + + INSERT INTO xxl_job_log_report ( + `trigger_day`, + `running_count`, + `suc_count`, + `fail_count` + ) VALUES ( + #{triggerDay}, + #{runningCount}, + #{sucCount}, + #{failCount} + ); + + + + + UPDATE xxl_job_log_report + SET `running_count` = #{runningCount}, + `suc_count` = #{sucCount}, + `fail_count` = #{failCount} + WHERE `trigger_day` = #{triggerDay} + + + + + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml new file mode 100644 index 0000000..4cae667 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + t.id, + t.registry_group, + t.registry_key, + t.registry_value, + t.update_time + + + + + + DELETE FROM xxl_job_registry + WHERE id in + + #{item} + + + + + + + UPDATE xxl_job_registry + SET `update_time` = #{updateTime} + WHERE `registry_group` = #{registryGroup} + AND `registry_key` = #{registryKey} + AND `registry_value` = #{registryValue} + + + + INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`) + VALUES( #{registryGroup} , #{registryKey} , #{registryValue}, #{updateTime}) + + + + DELETE FROM xxl_job_registry + WHERE registry_group = #{registryGroup} + AND registry_key = #{registryKey} + AND registry_value = #{registryValue} + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml new file mode 100644 index 0000000..9e09b4a --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + t.id, + t.username, + t.password, + t.role, + t.permission + + + + + + + + + + INSERT INTO xxl_job_user ( + username, + password, + role, + permission + ) VALUES ( + #{username}, + #{password}, + #{role}, + #{permission} + ); + + + + UPDATE xxl_job_user + SET + + password = #{password}, + + role = #{role}, + permission = #{permission} + WHERE id = #{id} + + + + DELETE + FROM xxl_job_user + WHERE id = #{id} + + + \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css new file mode 100644 index 0000000..baba9e9 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css @@ -0,0 +1,11 @@ +@charset "UTF-8";/*! + Ionicons, v2.0.0 + Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ + https://twitter.com/benjsperry https://twitter.com/ionicframework + MIT License: https://github.com/driftyco/ionicons + + Android-style icons originally built by Google’s + Material Design Icons: https://github.com/google/material-design-icons + used under CC BY http://creativecommons.org/licenses/by/4.0/ + Modified icons to fit ionicon’s grid from original. +*/@font-face{font-family:"Ionicons";src:url("../fonts/ionicons.eot?v=2.0.0");src:url("../fonts/ionicons.eot?v=2.0.0#iefix") format("embedded-opentype"),url("../fonts/ionicons.ttf?v=2.0.0") format("truetype"),url("../fonts/ionicons.woff?v=2.0.0") format("woff"),url("../fonts/ionicons.svg?v=2.0.0#Ionicons") format("svg");font-weight:normal;font-style:normal}.ion,.ionicons,.ion-alert:before,.ion-alert-circled:before,.ion-android-add:before,.ion-android-add-circle:before,.ion-android-alarm-clock:before,.ion-android-alert:before,.ion-android-apps:before,.ion-android-archive:before,.ion-android-arrow-back:before,.ion-android-arrow-down:before,.ion-android-arrow-dropdown:before,.ion-android-arrow-dropdown-circle:before,.ion-android-arrow-dropleft:before,.ion-android-arrow-dropleft-circle:before,.ion-android-arrow-dropright:before,.ion-android-arrow-dropright-circle:before,.ion-android-arrow-dropup:before,.ion-android-arrow-dropup-circle:before,.ion-android-arrow-forward:before,.ion-android-arrow-up:before,.ion-android-attach:before,.ion-android-bar:before,.ion-android-bicycle:before,.ion-android-boat:before,.ion-android-bookmark:before,.ion-android-bulb:before,.ion-android-bus:before,.ion-android-calendar:before,.ion-android-call:before,.ion-android-camera:before,.ion-android-cancel:before,.ion-android-car:before,.ion-android-cart:before,.ion-android-chat:before,.ion-android-checkbox:before,.ion-android-checkbox-blank:before,.ion-android-checkbox-outline:before,.ion-android-checkbox-outline-blank:before,.ion-android-checkmark-circle:before,.ion-android-clipboard:before,.ion-android-close:before,.ion-android-cloud:before,.ion-android-cloud-circle:before,.ion-android-cloud-done:before,.ion-android-cloud-outline:before,.ion-android-color-palette:before,.ion-android-compass:before,.ion-android-contact:before,.ion-android-contacts:before,.ion-android-contract:before,.ion-android-create:before,.ion-android-delete:before,.ion-android-desktop:before,.ion-android-document:before,.ion-android-done:before,.ion-android-done-all:before,.ion-android-download:before,.ion-android-drafts:before,.ion-android-exit:before,.ion-android-expand:before,.ion-android-favorite:before,.ion-android-favorite-outline:before,.ion-android-film:before,.ion-android-folder:before,.ion-android-folder-open:before,.ion-android-funnel:before,.ion-android-globe:before,.ion-android-hand:before,.ion-android-hangout:before,.ion-android-happy:before,.ion-android-home:before,.ion-android-image:before,.ion-android-laptop:before,.ion-android-list:before,.ion-android-locate:before,.ion-android-lock:before,.ion-android-mail:before,.ion-android-map:before,.ion-android-menu:before,.ion-android-microphone:before,.ion-android-microphone-off:before,.ion-android-more-horizontal:before,.ion-android-more-vertical:before,.ion-android-navigate:before,.ion-android-notifications:before,.ion-android-notifications-none:before,.ion-android-notifications-off:before,.ion-android-open:before,.ion-android-options:before,.ion-android-people:before,.ion-android-person:before,.ion-android-person-add:before,.ion-android-phone-landscape:before,.ion-android-phone-portrait:before,.ion-android-pin:before,.ion-android-plane:before,.ion-android-playstore:before,.ion-android-print:before,.ion-android-radio-button-off:before,.ion-android-radio-button-on:before,.ion-android-refresh:before,.ion-android-remove:before,.ion-android-remove-circle:before,.ion-android-restaurant:before,.ion-android-sad:before,.ion-android-search:before,.ion-android-send:before,.ion-android-settings:before,.ion-android-share:before,.ion-android-share-alt:before,.ion-android-star:before,.ion-android-star-half:before,.ion-android-star-outline:before,.ion-android-stopwatch:before,.ion-android-subway:before,.ion-android-sunny:before,.ion-android-sync:before,.ion-android-textsms:before,.ion-android-time:before,.ion-android-train:before,.ion-android-unlock:before,.ion-android-upload:before,.ion-android-volume-down:before,.ion-android-volume-mute:before,.ion-android-volume-off:before,.ion-android-volume-up:before,.ion-android-walk:before,.ion-android-warning:before,.ion-android-watch:before,.ion-android-wifi:before,.ion-aperture:before,.ion-archive:before,.ion-arrow-down-a:before,.ion-arrow-down-b:before,.ion-arrow-down-c:before,.ion-arrow-expand:before,.ion-arrow-graph-down-left:before,.ion-arrow-graph-down-right:before,.ion-arrow-graph-up-left:before,.ion-arrow-graph-up-right:before,.ion-arrow-left-a:before,.ion-arrow-left-b:before,.ion-arrow-left-c:before,.ion-arrow-move:before,.ion-arrow-resize:before,.ion-arrow-return-left:before,.ion-arrow-return-right:before,.ion-arrow-right-a:before,.ion-arrow-right-b:before,.ion-arrow-right-c:before,.ion-arrow-shrink:before,.ion-arrow-swap:before,.ion-arrow-up-a:before,.ion-arrow-up-b:before,.ion-arrow-up-c:before,.ion-asterisk:before,.ion-at:before,.ion-backspace:before,.ion-backspace-outline:before,.ion-bag:before,.ion-battery-charging:before,.ion-battery-empty:before,.ion-battery-full:before,.ion-battery-half:before,.ion-battery-low:before,.ion-beaker:before,.ion-beer:before,.ion-bluetooth:before,.ion-bonfire:before,.ion-bookmark:before,.ion-bowtie:before,.ion-briefcase:before,.ion-bug:before,.ion-calculator:before,.ion-calendar:before,.ion-camera:before,.ion-card:before,.ion-cash:before,.ion-chatbox:before,.ion-chatbox-working:before,.ion-chatboxes:before,.ion-chatbubble:before,.ion-chatbubble-working:before,.ion-chatbubbles:before,.ion-checkmark:before,.ion-checkmark-circled:before,.ion-checkmark-round:before,.ion-chevron-down:before,.ion-chevron-left:before,.ion-chevron-right:before,.ion-chevron-up:before,.ion-clipboard:before,.ion-clock:before,.ion-close:before,.ion-close-circled:before,.ion-close-round:before,.ion-closed-captioning:before,.ion-cloud:before,.ion-code:before,.ion-code-download:before,.ion-code-working:before,.ion-coffee:before,.ion-compass:before,.ion-compose:before,.ion-connection-bars:before,.ion-contrast:before,.ion-crop:before,.ion-cube:before,.ion-disc:before,.ion-document:before,.ion-document-text:before,.ion-drag:before,.ion-earth:before,.ion-easel:before,.ion-edit:before,.ion-egg:before,.ion-eject:before,.ion-email:before,.ion-email-unread:before,.ion-erlenmeyer-flask:before,.ion-erlenmeyer-flask-bubbles:before,.ion-eye:before,.ion-eye-disabled:before,.ion-female:before,.ion-filing:before,.ion-film-marker:before,.ion-fireball:before,.ion-flag:before,.ion-flame:before,.ion-flash:before,.ion-flash-off:before,.ion-folder:before,.ion-fork:before,.ion-fork-repo:before,.ion-forward:before,.ion-funnel:before,.ion-gear-a:before,.ion-gear-b:before,.ion-grid:before,.ion-hammer:before,.ion-happy:before,.ion-happy-outline:before,.ion-headphone:before,.ion-heart:before,.ion-heart-broken:before,.ion-help:before,.ion-help-buoy:before,.ion-help-circled:before,.ion-home:before,.ion-icecream:before,.ion-image:before,.ion-images:before,.ion-information:before,.ion-information-circled:before,.ion-ionic:before,.ion-ios-alarm:before,.ion-ios-alarm-outline:before,.ion-ios-albums:before,.ion-ios-albums-outline:before,.ion-ios-americanfootball:before,.ion-ios-americanfootball-outline:before,.ion-ios-analytics:before,.ion-ios-analytics-outline:before,.ion-ios-arrow-back:before,.ion-ios-arrow-down:before,.ion-ios-arrow-forward:before,.ion-ios-arrow-left:before,.ion-ios-arrow-right:before,.ion-ios-arrow-thin-down:before,.ion-ios-arrow-thin-left:before,.ion-ios-arrow-thin-right:before,.ion-ios-arrow-thin-up:before,.ion-ios-arrow-up:before,.ion-ios-at:before,.ion-ios-at-outline:before,.ion-ios-barcode:before,.ion-ios-barcode-outline:before,.ion-ios-baseball:before,.ion-ios-baseball-outline:before,.ion-ios-basketball:before,.ion-ios-basketball-outline:before,.ion-ios-bell:before,.ion-ios-bell-outline:before,.ion-ios-body:before,.ion-ios-body-outline:before,.ion-ios-bolt:before,.ion-ios-bolt-outline:before,.ion-ios-book:before,.ion-ios-book-outline:before,.ion-ios-bookmarks:before,.ion-ios-bookmarks-outline:before,.ion-ios-box:before,.ion-ios-box-outline:before,.ion-ios-briefcase:before,.ion-ios-briefcase-outline:before,.ion-ios-browsers:before,.ion-ios-browsers-outline:before,.ion-ios-calculator:before,.ion-ios-calculator-outline:before,.ion-ios-calendar:before,.ion-ios-calendar-outline:before,.ion-ios-camera:before,.ion-ios-camera-outline:before,.ion-ios-cart:before,.ion-ios-cart-outline:before,.ion-ios-chatboxes:before,.ion-ios-chatboxes-outline:before,.ion-ios-chatbubble:before,.ion-ios-chatbubble-outline:before,.ion-ios-checkmark:before,.ion-ios-checkmark-empty:before,.ion-ios-checkmark-outline:before,.ion-ios-circle-filled:before,.ion-ios-circle-outline:before,.ion-ios-clock:before,.ion-ios-clock-outline:before,.ion-ios-close:before,.ion-ios-close-empty:before,.ion-ios-close-outline:before,.ion-ios-cloud:before,.ion-ios-cloud-download:before,.ion-ios-cloud-download-outline:before,.ion-ios-cloud-outline:before,.ion-ios-cloud-upload:before,.ion-ios-cloud-upload-outline:before,.ion-ios-cloudy:before,.ion-ios-cloudy-night:before,.ion-ios-cloudy-night-outline:before,.ion-ios-cloudy-outline:before,.ion-ios-cog:before,.ion-ios-cog-outline:before,.ion-ios-color-filter:before,.ion-ios-color-filter-outline:before,.ion-ios-color-wand:before,.ion-ios-color-wand-outline:before,.ion-ios-compose:before,.ion-ios-compose-outline:before,.ion-ios-contact:before,.ion-ios-contact-outline:before,.ion-ios-copy:before,.ion-ios-copy-outline:before,.ion-ios-crop:before,.ion-ios-crop-strong:before,.ion-ios-download:before,.ion-ios-download-outline:before,.ion-ios-drag:before,.ion-ios-email:before,.ion-ios-email-outline:before,.ion-ios-eye:before,.ion-ios-eye-outline:before,.ion-ios-fastforward:before,.ion-ios-fastforward-outline:before,.ion-ios-filing:before,.ion-ios-filing-outline:before,.ion-ios-film:before,.ion-ios-film-outline:before,.ion-ios-flag:before,.ion-ios-flag-outline:before,.ion-ios-flame:before,.ion-ios-flame-outline:before,.ion-ios-flask:before,.ion-ios-flask-outline:before,.ion-ios-flower:before,.ion-ios-flower-outline:before,.ion-ios-folder:before,.ion-ios-folder-outline:before,.ion-ios-football:before,.ion-ios-football-outline:before,.ion-ios-game-controller-a:before,.ion-ios-game-controller-a-outline:before,.ion-ios-game-controller-b:before,.ion-ios-game-controller-b-outline:before,.ion-ios-gear:before,.ion-ios-gear-outline:before,.ion-ios-glasses:before,.ion-ios-glasses-outline:before,.ion-ios-grid-view:before,.ion-ios-grid-view-outline:before,.ion-ios-heart:before,.ion-ios-heart-outline:before,.ion-ios-help:before,.ion-ios-help-empty:before,.ion-ios-help-outline:before,.ion-ios-home:before,.ion-ios-home-outline:before,.ion-ios-infinite:before,.ion-ios-infinite-outline:before,.ion-ios-information:before,.ion-ios-information-empty:before,.ion-ios-information-outline:before,.ion-ios-ionic-outline:before,.ion-ios-keypad:before,.ion-ios-keypad-outline:before,.ion-ios-lightbulb:before,.ion-ios-lightbulb-outline:before,.ion-ios-list:before,.ion-ios-list-outline:before,.ion-ios-location:before,.ion-ios-location-outline:before,.ion-ios-locked:before,.ion-ios-locked-outline:before,.ion-ios-loop:before,.ion-ios-loop-strong:before,.ion-ios-medical:before,.ion-ios-medical-outline:before,.ion-ios-medkit:before,.ion-ios-medkit-outline:before,.ion-ios-mic:before,.ion-ios-mic-off:before,.ion-ios-mic-outline:before,.ion-ios-minus:before,.ion-ios-minus-empty:before,.ion-ios-minus-outline:before,.ion-ios-monitor:before,.ion-ios-monitor-outline:before,.ion-ios-moon:before,.ion-ios-moon-outline:before,.ion-ios-more:before,.ion-ios-more-outline:before,.ion-ios-musical-note:before,.ion-ios-musical-notes:before,.ion-ios-navigate:before,.ion-ios-navigate-outline:before,.ion-ios-nutrition:before,.ion-ios-nutrition-outline:before,.ion-ios-paper:before,.ion-ios-paper-outline:before,.ion-ios-paperplane:before,.ion-ios-paperplane-outline:before,.ion-ios-partlysunny:before,.ion-ios-partlysunny-outline:before,.ion-ios-pause:before,.ion-ios-pause-outline:before,.ion-ios-paw:before,.ion-ios-paw-outline:before,.ion-ios-people:before,.ion-ios-people-outline:before,.ion-ios-person:before,.ion-ios-person-outline:before,.ion-ios-personadd:before,.ion-ios-personadd-outline:before,.ion-ios-photos:before,.ion-ios-photos-outline:before,.ion-ios-pie:before,.ion-ios-pie-outline:before,.ion-ios-pint:before,.ion-ios-pint-outline:before,.ion-ios-play:before,.ion-ios-play-outline:before,.ion-ios-plus:before,.ion-ios-plus-empty:before,.ion-ios-plus-outline:before,.ion-ios-pricetag:before,.ion-ios-pricetag-outline:before,.ion-ios-pricetags:before,.ion-ios-pricetags-outline:before,.ion-ios-printer:before,.ion-ios-printer-outline:before,.ion-ios-pulse:before,.ion-ios-pulse-strong:before,.ion-ios-rainy:before,.ion-ios-rainy-outline:before,.ion-ios-recording:before,.ion-ios-recording-outline:before,.ion-ios-redo:before,.ion-ios-redo-outline:before,.ion-ios-refresh:before,.ion-ios-refresh-empty:before,.ion-ios-refresh-outline:before,.ion-ios-reload:before,.ion-ios-reverse-camera:before,.ion-ios-reverse-camera-outline:before,.ion-ios-rewind:before,.ion-ios-rewind-outline:before,.ion-ios-rose:before,.ion-ios-rose-outline:before,.ion-ios-search:before,.ion-ios-search-strong:before,.ion-ios-settings:before,.ion-ios-settings-strong:before,.ion-ios-shuffle:before,.ion-ios-shuffle-strong:before,.ion-ios-skipbackward:before,.ion-ios-skipbackward-outline:before,.ion-ios-skipforward:before,.ion-ios-skipforward-outline:before,.ion-ios-snowy:before,.ion-ios-speedometer:before,.ion-ios-speedometer-outline:before,.ion-ios-star:before,.ion-ios-star-half:before,.ion-ios-star-outline:before,.ion-ios-stopwatch:before,.ion-ios-stopwatch-outline:before,.ion-ios-sunny:before,.ion-ios-sunny-outline:before,.ion-ios-telephone:before,.ion-ios-telephone-outline:before,.ion-ios-tennisball:before,.ion-ios-tennisball-outline:before,.ion-ios-thunderstorm:before,.ion-ios-thunderstorm-outline:before,.ion-ios-time:before,.ion-ios-time-outline:before,.ion-ios-timer:before,.ion-ios-timer-outline:before,.ion-ios-toggle:before,.ion-ios-toggle-outline:before,.ion-ios-trash:before,.ion-ios-trash-outline:before,.ion-ios-undo:before,.ion-ios-undo-outline:before,.ion-ios-unlocked:before,.ion-ios-unlocked-outline:before,.ion-ios-upload:before,.ion-ios-upload-outline:before,.ion-ios-videocam:before,.ion-ios-videocam-outline:before,.ion-ios-volume-high:before,.ion-ios-volume-low:before,.ion-ios-wineglass:before,.ion-ios-wineglass-outline:before,.ion-ios-world:before,.ion-ios-world-outline:before,.ion-ipad:before,.ion-iphone:before,.ion-ipod:before,.ion-jet:before,.ion-key:before,.ion-knife:before,.ion-laptop:before,.ion-leaf:before,.ion-levels:before,.ion-lightbulb:before,.ion-link:before,.ion-load-a:before,.ion-load-b:before,.ion-load-c:before,.ion-load-d:before,.ion-location:before,.ion-lock-combination:before,.ion-locked:before,.ion-log-in:before,.ion-log-out:before,.ion-loop:before,.ion-magnet:before,.ion-male:before,.ion-man:before,.ion-map:before,.ion-medkit:before,.ion-merge:before,.ion-mic-a:before,.ion-mic-b:before,.ion-mic-c:before,.ion-minus:before,.ion-minus-circled:before,.ion-minus-round:before,.ion-model-s:before,.ion-monitor:before,.ion-more:before,.ion-mouse:before,.ion-music-note:before,.ion-navicon:before,.ion-navicon-round:before,.ion-navigate:before,.ion-network:before,.ion-no-smoking:before,.ion-nuclear:before,.ion-outlet:before,.ion-paintbrush:before,.ion-paintbucket:before,.ion-paper-airplane:before,.ion-paperclip:before,.ion-pause:before,.ion-person:before,.ion-person-add:before,.ion-person-stalker:before,.ion-pie-graph:before,.ion-pin:before,.ion-pinpoint:before,.ion-pizza:before,.ion-plane:before,.ion-planet:before,.ion-play:before,.ion-playstation:before,.ion-plus:before,.ion-plus-circled:before,.ion-plus-round:before,.ion-podium:before,.ion-pound:before,.ion-power:before,.ion-pricetag:before,.ion-pricetags:before,.ion-printer:before,.ion-pull-request:before,.ion-qr-scanner:before,.ion-quote:before,.ion-radio-waves:before,.ion-record:before,.ion-refresh:before,.ion-reply:before,.ion-reply-all:before,.ion-ribbon-a:before,.ion-ribbon-b:before,.ion-sad:before,.ion-sad-outline:before,.ion-scissors:before,.ion-search:before,.ion-settings:before,.ion-share:before,.ion-shuffle:before,.ion-skip-backward:before,.ion-skip-forward:before,.ion-social-android:before,.ion-social-android-outline:before,.ion-social-angular:before,.ion-social-angular-outline:before,.ion-social-apple:before,.ion-social-apple-outline:before,.ion-social-bitcoin:before,.ion-social-bitcoin-outline:before,.ion-social-buffer:before,.ion-social-buffer-outline:before,.ion-social-chrome:before,.ion-social-chrome-outline:before,.ion-social-codepen:before,.ion-social-codepen-outline:before,.ion-social-css3:before,.ion-social-css3-outline:before,.ion-social-designernews:before,.ion-social-designernews-outline:before,.ion-social-dribbble:before,.ion-social-dribbble-outline:before,.ion-social-dropbox:before,.ion-social-dropbox-outline:before,.ion-social-euro:before,.ion-social-euro-outline:before,.ion-social-facebook:before,.ion-social-facebook-outline:before,.ion-social-foursquare:before,.ion-social-foursquare-outline:before,.ion-social-freebsd-devil:before,.ion-social-github:before,.ion-social-github-outline:before,.ion-social-google:before,.ion-social-google-outline:before,.ion-social-googleplus:before,.ion-social-googleplus-outline:before,.ion-social-hackernews:before,.ion-social-hackernews-outline:before,.ion-social-html5:before,.ion-social-html5-outline:before,.ion-social-instagram:before,.ion-social-instagram-outline:before,.ion-social-javascript:before,.ion-social-javascript-outline:before,.ion-social-linkedin:before,.ion-social-linkedin-outline:before,.ion-social-markdown:before,.ion-social-nodejs:before,.ion-social-octocat:before,.ion-social-pinterest:before,.ion-social-pinterest-outline:before,.ion-social-python:before,.ion-social-reddit:before,.ion-social-reddit-outline:before,.ion-social-rss:before,.ion-social-rss-outline:before,.ion-social-sass:before,.ion-social-skype:before,.ion-social-skype-outline:before,.ion-social-snapchat:before,.ion-social-snapchat-outline:before,.ion-social-tumblr:before,.ion-social-tumblr-outline:before,.ion-social-tux:before,.ion-social-twitch:before,.ion-social-twitch-outline:before,.ion-social-twitter:before,.ion-social-twitter-outline:before,.ion-social-usd:before,.ion-social-usd-outline:before,.ion-social-vimeo:before,.ion-social-vimeo-outline:before,.ion-social-whatsapp:before,.ion-social-whatsapp-outline:before,.ion-social-windows:before,.ion-social-windows-outline:before,.ion-social-wordpress:before,.ion-social-wordpress-outline:before,.ion-social-yahoo:before,.ion-social-yahoo-outline:before,.ion-social-yen:before,.ion-social-yen-outline:before,.ion-social-youtube:before,.ion-social-youtube-outline:before,.ion-soup-can:before,.ion-soup-can-outline:before,.ion-speakerphone:before,.ion-speedometer:before,.ion-spoon:before,.ion-star:before,.ion-stats-bars:before,.ion-steam:before,.ion-stop:before,.ion-thermometer:before,.ion-thumbsdown:before,.ion-thumbsup:before,.ion-toggle:before,.ion-toggle-filled:before,.ion-transgender:before,.ion-trash-a:before,.ion-trash-b:before,.ion-trophy:before,.ion-tshirt:before,.ion-tshirt-outline:before,.ion-umbrella:before,.ion-university:before,.ion-unlocked:before,.ion-upload:before,.ion-usb:before,.ion-videocamera:before,.ion-volume-high:before,.ion-volume-low:before,.ion-volume-medium:before,.ion-volume-mute:before,.ion-wand:before,.ion-waterdrop:before,.ion-wifi:before,.ion-wineglass:before,.ion-woman:before,.ion-wrench:before,.ion-xbox:before{display:inline-block;font-family:"Ionicons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ion-alert:before{content:"\f101"}.ion-alert-circled:before{content:"\f100"}.ion-android-add:before{content:"\f2c7"}.ion-android-add-circle:before{content:"\f359"}.ion-android-alarm-clock:before{content:"\f35a"}.ion-android-alert:before{content:"\f35b"}.ion-android-apps:before{content:"\f35c"}.ion-android-archive:before{content:"\f2c9"}.ion-android-arrow-back:before{content:"\f2ca"}.ion-android-arrow-down:before{content:"\f35d"}.ion-android-arrow-dropdown:before{content:"\f35f"}.ion-android-arrow-dropdown-circle:before{content:"\f35e"}.ion-android-arrow-dropleft:before{content:"\f361"}.ion-android-arrow-dropleft-circle:before{content:"\f360"}.ion-android-arrow-dropright:before{content:"\f363"}.ion-android-arrow-dropright-circle:before{content:"\f362"}.ion-android-arrow-dropup:before{content:"\f365"}.ion-android-arrow-dropup-circle:before{content:"\f364"}.ion-android-arrow-forward:before{content:"\f30f"}.ion-android-arrow-up:before{content:"\f366"}.ion-android-attach:before{content:"\f367"}.ion-android-bar:before{content:"\f368"}.ion-android-bicycle:before{content:"\f369"}.ion-android-boat:before{content:"\f36a"}.ion-android-bookmark:before{content:"\f36b"}.ion-android-bulb:before{content:"\f36c"}.ion-android-bus:before{content:"\f36d"}.ion-android-calendar:before{content:"\f2d1"}.ion-android-call:before{content:"\f2d2"}.ion-android-camera:before{content:"\f2d3"}.ion-android-cancel:before{content:"\f36e"}.ion-android-car:before{content:"\f36f"}.ion-android-cart:before{content:"\f370"}.ion-android-chat:before{content:"\f2d4"}.ion-android-checkbox:before{content:"\f374"}.ion-android-checkbox-blank:before{content:"\f371"}.ion-android-checkbox-outline:before{content:"\f373"}.ion-android-checkbox-outline-blank:before{content:"\f372"}.ion-android-checkmark-circle:before{content:"\f375"}.ion-android-clipboard:before{content:"\f376"}.ion-android-close:before{content:"\f2d7"}.ion-android-cloud:before{content:"\f37a"}.ion-android-cloud-circle:before{content:"\f377"}.ion-android-cloud-done:before{content:"\f378"}.ion-android-cloud-outline:before{content:"\f379"}.ion-android-color-palette:before{content:"\f37b"}.ion-android-compass:before{content:"\f37c"}.ion-android-contact:before{content:"\f2d8"}.ion-android-contacts:before{content:"\f2d9"}.ion-android-contract:before{content:"\f37d"}.ion-android-create:before{content:"\f37e"}.ion-android-delete:before{content:"\f37f"}.ion-android-desktop:before{content:"\f380"}.ion-android-document:before{content:"\f381"}.ion-android-done:before{content:"\f383"}.ion-android-done-all:before{content:"\f382"}.ion-android-download:before{content:"\f2dd"}.ion-android-drafts:before{content:"\f384"}.ion-android-exit:before{content:"\f385"}.ion-android-expand:before{content:"\f386"}.ion-android-favorite:before{content:"\f388"}.ion-android-favorite-outline:before{content:"\f387"}.ion-android-film:before{content:"\f389"}.ion-android-folder:before{content:"\f2e0"}.ion-android-folder-open:before{content:"\f38a"}.ion-android-funnel:before{content:"\f38b"}.ion-android-globe:before{content:"\f38c"}.ion-android-hand:before{content:"\f2e3"}.ion-android-hangout:before{content:"\f38d"}.ion-android-happy:before{content:"\f38e"}.ion-android-home:before{content:"\f38f"}.ion-android-image:before{content:"\f2e4"}.ion-android-laptop:before{content:"\f390"}.ion-android-list:before{content:"\f391"}.ion-android-locate:before{content:"\f2e9"}.ion-android-lock:before{content:"\f392"}.ion-android-mail:before{content:"\f2eb"}.ion-android-map:before{content:"\f393"}.ion-android-menu:before{content:"\f394"}.ion-android-microphone:before{content:"\f2ec"}.ion-android-microphone-off:before{content:"\f395"}.ion-android-more-horizontal:before{content:"\f396"}.ion-android-more-vertical:before{content:"\f397"}.ion-android-navigate:before{content:"\f398"}.ion-android-notifications:before{content:"\f39b"}.ion-android-notifications-none:before{content:"\f399"}.ion-android-notifications-off:before{content:"\f39a"}.ion-android-open:before{content:"\f39c"}.ion-android-options:before{content:"\f39d"}.ion-android-people:before{content:"\f39e"}.ion-android-person:before{content:"\f3a0"}.ion-android-person-add:before{content:"\f39f"}.ion-android-phone-landscape:before{content:"\f3a1"}.ion-android-phone-portrait:before{content:"\f3a2"}.ion-android-pin:before{content:"\f3a3"}.ion-android-plane:before{content:"\f3a4"}.ion-android-playstore:before{content:"\f2f0"}.ion-android-print:before{content:"\f3a5"}.ion-android-radio-button-off:before{content:"\f3a6"}.ion-android-radio-button-on:before{content:"\f3a7"}.ion-android-refresh:before{content:"\f3a8"}.ion-android-remove:before{content:"\f2f4"}.ion-android-remove-circle:before{content:"\f3a9"}.ion-android-restaurant:before{content:"\f3aa"}.ion-android-sad:before{content:"\f3ab"}.ion-android-search:before{content:"\f2f5"}.ion-android-send:before{content:"\f2f6"}.ion-android-settings:before{content:"\f2f7"}.ion-android-share:before{content:"\f2f8"}.ion-android-share-alt:before{content:"\f3ac"}.ion-android-star:before{content:"\f2fc"}.ion-android-star-half:before{content:"\f3ad"}.ion-android-star-outline:before{content:"\f3ae"}.ion-android-stopwatch:before{content:"\f2fd"}.ion-android-subway:before{content:"\f3af"}.ion-android-sunny:before{content:"\f3b0"}.ion-android-sync:before{content:"\f3b1"}.ion-android-textsms:before{content:"\f3b2"}.ion-android-time:before{content:"\f3b3"}.ion-android-train:before{content:"\f3b4"}.ion-android-unlock:before{content:"\f3b5"}.ion-android-upload:before{content:"\f3b6"}.ion-android-volume-down:before{content:"\f3b7"}.ion-android-volume-mute:before{content:"\f3b8"}.ion-android-volume-off:before{content:"\f3b9"}.ion-android-volume-up:before{content:"\f3ba"}.ion-android-walk:before{content:"\f3bb"}.ion-android-warning:before{content:"\f3bc"}.ion-android-watch:before{content:"\f3bd"}.ion-android-wifi:before{content:"\f305"}.ion-aperture:before{content:"\f313"}.ion-archive:before{content:"\f102"}.ion-arrow-down-a:before{content:"\f103"}.ion-arrow-down-b:before{content:"\f104"}.ion-arrow-down-c:before{content:"\f105"}.ion-arrow-expand:before{content:"\f25e"}.ion-arrow-graph-down-left:before{content:"\f25f"}.ion-arrow-graph-down-right:before{content:"\f260"}.ion-arrow-graph-up-left:before{content:"\f261"}.ion-arrow-graph-up-right:before{content:"\f262"}.ion-arrow-left-a:before{content:"\f106"}.ion-arrow-left-b:before{content:"\f107"}.ion-arrow-left-c:before{content:"\f108"}.ion-arrow-move:before{content:"\f263"}.ion-arrow-resize:before{content:"\f264"}.ion-arrow-return-left:before{content:"\f265"}.ion-arrow-return-right:before{content:"\f266"}.ion-arrow-right-a:before{content:"\f109"}.ion-arrow-right-b:before{content:"\f10a"}.ion-arrow-right-c:before{content:"\f10b"}.ion-arrow-shrink:before{content:"\f267"}.ion-arrow-swap:before{content:"\f268"}.ion-arrow-up-a:before{content:"\f10c"}.ion-arrow-up-b:before{content:"\f10d"}.ion-arrow-up-c:before{content:"\f10e"}.ion-asterisk:before{content:"\f314"}.ion-at:before{content:"\f10f"}.ion-backspace:before{content:"\f3bf"}.ion-backspace-outline:before{content:"\f3be"}.ion-bag:before{content:"\f110"}.ion-battery-charging:before{content:"\f111"}.ion-battery-empty:before{content:"\f112"}.ion-battery-full:before{content:"\f113"}.ion-battery-half:before{content:"\f114"}.ion-battery-low:before{content:"\f115"}.ion-beaker:before{content:"\f269"}.ion-beer:before{content:"\f26a"}.ion-bluetooth:before{content:"\f116"}.ion-bonfire:before{content:"\f315"}.ion-bookmark:before{content:"\f26b"}.ion-bowtie:before{content:"\f3c0"}.ion-briefcase:before{content:"\f26c"}.ion-bug:before{content:"\f2be"}.ion-calculator:before{content:"\f26d"}.ion-calendar:before{content:"\f117"}.ion-camera:before{content:"\f118"}.ion-card:before{content:"\f119"}.ion-cash:before{content:"\f316"}.ion-chatbox:before{content:"\f11b"}.ion-chatbox-working:before{content:"\f11a"}.ion-chatboxes:before{content:"\f11c"}.ion-chatbubble:before{content:"\f11e"}.ion-chatbubble-working:before{content:"\f11d"}.ion-chatbubbles:before{content:"\f11f"}.ion-checkmark:before{content:"\f122"}.ion-checkmark-circled:before{content:"\f120"}.ion-checkmark-round:before{content:"\f121"}.ion-chevron-down:before{content:"\f123"}.ion-chevron-left:before{content:"\f124"}.ion-chevron-right:before{content:"\f125"}.ion-chevron-up:before{content:"\f126"}.ion-clipboard:before{content:"\f127"}.ion-clock:before{content:"\f26e"}.ion-close:before{content:"\f12a"}.ion-close-circled:before{content:"\f128"}.ion-close-round:before{content:"\f129"}.ion-closed-captioning:before{content:"\f317"}.ion-cloud:before{content:"\f12b"}.ion-code:before{content:"\f271"}.ion-code-download:before{content:"\f26f"}.ion-code-working:before{content:"\f270"}.ion-coffee:before{content:"\f272"}.ion-compass:before{content:"\f273"}.ion-compose:before{content:"\f12c"}.ion-connection-bars:before{content:"\f274"}.ion-contrast:before{content:"\f275"}.ion-crop:before{content:"\f3c1"}.ion-cube:before{content:"\f318"}.ion-disc:before{content:"\f12d"}.ion-document:before{content:"\f12f"}.ion-document-text:before{content:"\f12e"}.ion-drag:before{content:"\f130"}.ion-earth:before{content:"\f276"}.ion-easel:before{content:"\f3c2"}.ion-edit:before{content:"\f2bf"}.ion-egg:before{content:"\f277"}.ion-eject:before{content:"\f131"}.ion-email:before{content:"\f132"}.ion-email-unread:before{content:"\f3c3"}.ion-erlenmeyer-flask:before{content:"\f3c5"}.ion-erlenmeyer-flask-bubbles:before{content:"\f3c4"}.ion-eye:before{content:"\f133"}.ion-eye-disabled:before{content:"\f306"}.ion-female:before{content:"\f278"}.ion-filing:before{content:"\f134"}.ion-film-marker:before{content:"\f135"}.ion-fireball:before{content:"\f319"}.ion-flag:before{content:"\f279"}.ion-flame:before{content:"\f31a"}.ion-flash:before{content:"\f137"}.ion-flash-off:before{content:"\f136"}.ion-folder:before{content:"\f139"}.ion-fork:before{content:"\f27a"}.ion-fork-repo:before{content:"\f2c0"}.ion-forward:before{content:"\f13a"}.ion-funnel:before{content:"\f31b"}.ion-gear-a:before{content:"\f13d"}.ion-gear-b:before{content:"\f13e"}.ion-grid:before{content:"\f13f"}.ion-hammer:before{content:"\f27b"}.ion-happy:before{content:"\f31c"}.ion-happy-outline:before{content:"\f3c6"}.ion-headphone:before{content:"\f140"}.ion-heart:before{content:"\f141"}.ion-heart-broken:before{content:"\f31d"}.ion-help:before{content:"\f143"}.ion-help-buoy:before{content:"\f27c"}.ion-help-circled:before{content:"\f142"}.ion-home:before{content:"\f144"}.ion-icecream:before{content:"\f27d"}.ion-image:before{content:"\f147"}.ion-images:before{content:"\f148"}.ion-information:before{content:"\f14a"}.ion-information-circled:before{content:"\f149"}.ion-ionic:before{content:"\f14b"}.ion-ios-alarm:before{content:"\f3c8"}.ion-ios-alarm-outline:before{content:"\f3c7"}.ion-ios-albums:before{content:"\f3ca"}.ion-ios-albums-outline:before{content:"\f3c9"}.ion-ios-americanfootball:before{content:"\f3cc"}.ion-ios-americanfootball-outline:before{content:"\f3cb"}.ion-ios-analytics:before{content:"\f3ce"}.ion-ios-analytics-outline:before{content:"\f3cd"}.ion-ios-arrow-back:before{content:"\f3cf"}.ion-ios-arrow-down:before{content:"\f3d0"}.ion-ios-arrow-forward:before{content:"\f3d1"}.ion-ios-arrow-left:before{content:"\f3d2"}.ion-ios-arrow-right:before{content:"\f3d3"}.ion-ios-arrow-thin-down:before{content:"\f3d4"}.ion-ios-arrow-thin-left:before{content:"\f3d5"}.ion-ios-arrow-thin-right:before{content:"\f3d6"}.ion-ios-arrow-thin-up:before{content:"\f3d7"}.ion-ios-arrow-up:before{content:"\f3d8"}.ion-ios-at:before{content:"\f3da"}.ion-ios-at-outline:before{content:"\f3d9"}.ion-ios-barcode:before{content:"\f3dc"}.ion-ios-barcode-outline:before{content:"\f3db"}.ion-ios-baseball:before{content:"\f3de"}.ion-ios-baseball-outline:before{content:"\f3dd"}.ion-ios-basketball:before{content:"\f3e0"}.ion-ios-basketball-outline:before{content:"\f3df"}.ion-ios-bell:before{content:"\f3e2"}.ion-ios-bell-outline:before{content:"\f3e1"}.ion-ios-body:before{content:"\f3e4"}.ion-ios-body-outline:before{content:"\f3e3"}.ion-ios-bolt:before{content:"\f3e6"}.ion-ios-bolt-outline:before{content:"\f3e5"}.ion-ios-book:before{content:"\f3e8"}.ion-ios-book-outline:before{content:"\f3e7"}.ion-ios-bookmarks:before{content:"\f3ea"}.ion-ios-bookmarks-outline:before{content:"\f3e9"}.ion-ios-box:before{content:"\f3ec"}.ion-ios-box-outline:before{content:"\f3eb"}.ion-ios-briefcase:before{content:"\f3ee"}.ion-ios-briefcase-outline:before{content:"\f3ed"}.ion-ios-browsers:before{content:"\f3f0"}.ion-ios-browsers-outline:before{content:"\f3ef"}.ion-ios-calculator:before{content:"\f3f2"}.ion-ios-calculator-outline:before{content:"\f3f1"}.ion-ios-calendar:before{content:"\f3f4"}.ion-ios-calendar-outline:before{content:"\f3f3"}.ion-ios-camera:before{content:"\f3f6"}.ion-ios-camera-outline:before{content:"\f3f5"}.ion-ios-cart:before{content:"\f3f8"}.ion-ios-cart-outline:before{content:"\f3f7"}.ion-ios-chatboxes:before{content:"\f3fa"}.ion-ios-chatboxes-outline:before{content:"\f3f9"}.ion-ios-chatbubble:before{content:"\f3fc"}.ion-ios-chatbubble-outline:before{content:"\f3fb"}.ion-ios-checkmark:before{content:"\f3ff"}.ion-ios-checkmark-empty:before{content:"\f3fd"}.ion-ios-checkmark-outline:before{content:"\f3fe"}.ion-ios-circle-filled:before{content:"\f400"}.ion-ios-circle-outline:before{content:"\f401"}.ion-ios-clock:before{content:"\f403"}.ion-ios-clock-outline:before{content:"\f402"}.ion-ios-close:before{content:"\f406"}.ion-ios-close-empty:before{content:"\f404"}.ion-ios-close-outline:before{content:"\f405"}.ion-ios-cloud:before{content:"\f40c"}.ion-ios-cloud-download:before{content:"\f408"}.ion-ios-cloud-download-outline:before{content:"\f407"}.ion-ios-cloud-outline:before{content:"\f409"}.ion-ios-cloud-upload:before{content:"\f40b"}.ion-ios-cloud-upload-outline:before{content:"\f40a"}.ion-ios-cloudy:before{content:"\f410"}.ion-ios-cloudy-night:before{content:"\f40e"}.ion-ios-cloudy-night-outline:before{content:"\f40d"}.ion-ios-cloudy-outline:before{content:"\f40f"}.ion-ios-cog:before{content:"\f412"}.ion-ios-cog-outline:before{content:"\f411"}.ion-ios-color-filter:before{content:"\f414"}.ion-ios-color-filter-outline:before{content:"\f413"}.ion-ios-color-wand:before{content:"\f416"}.ion-ios-color-wand-outline:before{content:"\f415"}.ion-ios-compose:before{content:"\f418"}.ion-ios-compose-outline:before{content:"\f417"}.ion-ios-contact:before{content:"\f41a"}.ion-ios-contact-outline:before{content:"\f419"}.ion-ios-copy:before{content:"\f41c"}.ion-ios-copy-outline:before{content:"\f41b"}.ion-ios-crop:before{content:"\f41e"}.ion-ios-crop-strong:before{content:"\f41d"}.ion-ios-download:before{content:"\f420"}.ion-ios-download-outline:before{content:"\f41f"}.ion-ios-drag:before{content:"\f421"}.ion-ios-email:before{content:"\f423"}.ion-ios-email-outline:before{content:"\f422"}.ion-ios-eye:before{content:"\f425"}.ion-ios-eye-outline:before{content:"\f424"}.ion-ios-fastforward:before{content:"\f427"}.ion-ios-fastforward-outline:before{content:"\f426"}.ion-ios-filing:before{content:"\f429"}.ion-ios-filing-outline:before{content:"\f428"}.ion-ios-film:before{content:"\f42b"}.ion-ios-film-outline:before{content:"\f42a"}.ion-ios-flag:before{content:"\f42d"}.ion-ios-flag-outline:before{content:"\f42c"}.ion-ios-flame:before{content:"\f42f"}.ion-ios-flame-outline:before{content:"\f42e"}.ion-ios-flask:before{content:"\f431"}.ion-ios-flask-outline:before{content:"\f430"}.ion-ios-flower:before{content:"\f433"}.ion-ios-flower-outline:before{content:"\f432"}.ion-ios-folder:before{content:"\f435"}.ion-ios-folder-outline:before{content:"\f434"}.ion-ios-football:before{content:"\f437"}.ion-ios-football-outline:before{content:"\f436"}.ion-ios-game-controller-a:before{content:"\f439"}.ion-ios-game-controller-a-outline:before{content:"\f438"}.ion-ios-game-controller-b:before{content:"\f43b"}.ion-ios-game-controller-b-outline:before{content:"\f43a"}.ion-ios-gear:before{content:"\f43d"}.ion-ios-gear-outline:before{content:"\f43c"}.ion-ios-glasses:before{content:"\f43f"}.ion-ios-glasses-outline:before{content:"\f43e"}.ion-ios-grid-view:before{content:"\f441"}.ion-ios-grid-view-outline:before{content:"\f440"}.ion-ios-heart:before{content:"\f443"}.ion-ios-heart-outline:before{content:"\f442"}.ion-ios-help:before{content:"\f446"}.ion-ios-help-empty:before{content:"\f444"}.ion-ios-help-outline:before{content:"\f445"}.ion-ios-home:before{content:"\f448"}.ion-ios-home-outline:before{content:"\f447"}.ion-ios-infinite:before{content:"\f44a"}.ion-ios-infinite-outline:before{content:"\f449"}.ion-ios-information:before{content:"\f44d"}.ion-ios-information-empty:before{content:"\f44b"}.ion-ios-information-outline:before{content:"\f44c"}.ion-ios-ionic-outline:before{content:"\f44e"}.ion-ios-keypad:before{content:"\f450"}.ion-ios-keypad-outline:before{content:"\f44f"}.ion-ios-lightbulb:before{content:"\f452"}.ion-ios-lightbulb-outline:before{content:"\f451"}.ion-ios-list:before{content:"\f454"}.ion-ios-list-outline:before{content:"\f453"}.ion-ios-location:before{content:"\f456"}.ion-ios-location-outline:before{content:"\f455"}.ion-ios-locked:before{content:"\f458"}.ion-ios-locked-outline:before{content:"\f457"}.ion-ios-loop:before{content:"\f45a"}.ion-ios-loop-strong:before{content:"\f459"}.ion-ios-medical:before{content:"\f45c"}.ion-ios-medical-outline:before{content:"\f45b"}.ion-ios-medkit:before{content:"\f45e"}.ion-ios-medkit-outline:before{content:"\f45d"}.ion-ios-mic:before{content:"\f461"}.ion-ios-mic-off:before{content:"\f45f"}.ion-ios-mic-outline:before{content:"\f460"}.ion-ios-minus:before{content:"\f464"}.ion-ios-minus-empty:before{content:"\f462"}.ion-ios-minus-outline:before{content:"\f463"}.ion-ios-monitor:before{content:"\f466"}.ion-ios-monitor-outline:before{content:"\f465"}.ion-ios-moon:before{content:"\f468"}.ion-ios-moon-outline:before{content:"\f467"}.ion-ios-more:before{content:"\f46a"}.ion-ios-more-outline:before{content:"\f469"}.ion-ios-musical-note:before{content:"\f46b"}.ion-ios-musical-notes:before{content:"\f46c"}.ion-ios-navigate:before{content:"\f46e"}.ion-ios-navigate-outline:before{content:"\f46d"}.ion-ios-nutrition:before{content:"\f470"}.ion-ios-nutrition-outline:before{content:"\f46f"}.ion-ios-paper:before{content:"\f472"}.ion-ios-paper-outline:before{content:"\f471"}.ion-ios-paperplane:before{content:"\f474"}.ion-ios-paperplane-outline:before{content:"\f473"}.ion-ios-partlysunny:before{content:"\f476"}.ion-ios-partlysunny-outline:before{content:"\f475"}.ion-ios-pause:before{content:"\f478"}.ion-ios-pause-outline:before{content:"\f477"}.ion-ios-paw:before{content:"\f47a"}.ion-ios-paw-outline:before{content:"\f479"}.ion-ios-people:before{content:"\f47c"}.ion-ios-people-outline:before{content:"\f47b"}.ion-ios-person:before{content:"\f47e"}.ion-ios-person-outline:before{content:"\f47d"}.ion-ios-personadd:before{content:"\f480"}.ion-ios-personadd-outline:before{content:"\f47f"}.ion-ios-photos:before{content:"\f482"}.ion-ios-photos-outline:before{content:"\f481"}.ion-ios-pie:before{content:"\f484"}.ion-ios-pie-outline:before{content:"\f483"}.ion-ios-pint:before{content:"\f486"}.ion-ios-pint-outline:before{content:"\f485"}.ion-ios-play:before{content:"\f488"}.ion-ios-play-outline:before{content:"\f487"}.ion-ios-plus:before{content:"\f48b"}.ion-ios-plus-empty:before{content:"\f489"}.ion-ios-plus-outline:before{content:"\f48a"}.ion-ios-pricetag:before{content:"\f48d"}.ion-ios-pricetag-outline:before{content:"\f48c"}.ion-ios-pricetags:before{content:"\f48f"}.ion-ios-pricetags-outline:before{content:"\f48e"}.ion-ios-printer:before{content:"\f491"}.ion-ios-printer-outline:before{content:"\f490"}.ion-ios-pulse:before{content:"\f493"}.ion-ios-pulse-strong:before{content:"\f492"}.ion-ios-rainy:before{content:"\f495"}.ion-ios-rainy-outline:before{content:"\f494"}.ion-ios-recording:before{content:"\f497"}.ion-ios-recording-outline:before{content:"\f496"}.ion-ios-redo:before{content:"\f499"}.ion-ios-redo-outline:before{content:"\f498"}.ion-ios-refresh:before{content:"\f49c"}.ion-ios-refresh-empty:before{content:"\f49a"}.ion-ios-refresh-outline:before{content:"\f49b"}.ion-ios-reload:before{content:"\f49d"}.ion-ios-reverse-camera:before{content:"\f49f"}.ion-ios-reverse-camera-outline:before{content:"\f49e"}.ion-ios-rewind:before{content:"\f4a1"}.ion-ios-rewind-outline:before{content:"\f4a0"}.ion-ios-rose:before{content:"\f4a3"}.ion-ios-rose-outline:before{content:"\f4a2"}.ion-ios-search:before{content:"\f4a5"}.ion-ios-search-strong:before{content:"\f4a4"}.ion-ios-settings:before{content:"\f4a7"}.ion-ios-settings-strong:before{content:"\f4a6"}.ion-ios-shuffle:before{content:"\f4a9"}.ion-ios-shuffle-strong:before{content:"\f4a8"}.ion-ios-skipbackward:before{content:"\f4ab"}.ion-ios-skipbackward-outline:before{content:"\f4aa"}.ion-ios-skipforward:before{content:"\f4ad"}.ion-ios-skipforward-outline:before{content:"\f4ac"}.ion-ios-snowy:before{content:"\f4ae"}.ion-ios-speedometer:before{content:"\f4b0"}.ion-ios-speedometer-outline:before{content:"\f4af"}.ion-ios-star:before{content:"\f4b3"}.ion-ios-star-half:before{content:"\f4b1"}.ion-ios-star-outline:before{content:"\f4b2"}.ion-ios-stopwatch:before{content:"\f4b5"}.ion-ios-stopwatch-outline:before{content:"\f4b4"}.ion-ios-sunny:before{content:"\f4b7"}.ion-ios-sunny-outline:before{content:"\f4b6"}.ion-ios-telephone:before{content:"\f4b9"}.ion-ios-telephone-outline:before{content:"\f4b8"}.ion-ios-tennisball:before{content:"\f4bb"}.ion-ios-tennisball-outline:before{content:"\f4ba"}.ion-ios-thunderstorm:before{content:"\f4bd"}.ion-ios-thunderstorm-outline:before{content:"\f4bc"}.ion-ios-time:before{content:"\f4bf"}.ion-ios-time-outline:before{content:"\f4be"}.ion-ios-timer:before{content:"\f4c1"}.ion-ios-timer-outline:before{content:"\f4c0"}.ion-ios-toggle:before{content:"\f4c3"}.ion-ios-toggle-outline:before{content:"\f4c2"}.ion-ios-trash:before{content:"\f4c5"}.ion-ios-trash-outline:before{content:"\f4c4"}.ion-ios-undo:before{content:"\f4c7"}.ion-ios-undo-outline:before{content:"\f4c6"}.ion-ios-unlocked:before{content:"\f4c9"}.ion-ios-unlocked-outline:before{content:"\f4c8"}.ion-ios-upload:before{content:"\f4cb"}.ion-ios-upload-outline:before{content:"\f4ca"}.ion-ios-videocam:before{content:"\f4cd"}.ion-ios-videocam-outline:before{content:"\f4cc"}.ion-ios-volume-high:before{content:"\f4ce"}.ion-ios-volume-low:before{content:"\f4cf"}.ion-ios-wineglass:before{content:"\f4d1"}.ion-ios-wineglass-outline:before{content:"\f4d0"}.ion-ios-world:before{content:"\f4d3"}.ion-ios-world-outline:before{content:"\f4d2"}.ion-ipad:before{content:"\f1f9"}.ion-iphone:before{content:"\f1fa"}.ion-ipod:before{content:"\f1fb"}.ion-jet:before{content:"\f295"}.ion-key:before{content:"\f296"}.ion-knife:before{content:"\f297"}.ion-laptop:before{content:"\f1fc"}.ion-leaf:before{content:"\f1fd"}.ion-levels:before{content:"\f298"}.ion-lightbulb:before{content:"\f299"}.ion-link:before{content:"\f1fe"}.ion-load-a:before{content:"\f29a"}.ion-load-b:before{content:"\f29b"}.ion-load-c:before{content:"\f29c"}.ion-load-d:before{content:"\f29d"}.ion-location:before{content:"\f1ff"}.ion-lock-combination:before{content:"\f4d4"}.ion-locked:before{content:"\f200"}.ion-log-in:before{content:"\f29e"}.ion-log-out:before{content:"\f29f"}.ion-loop:before{content:"\f201"}.ion-magnet:before{content:"\f2a0"}.ion-male:before{content:"\f2a1"}.ion-man:before{content:"\f202"}.ion-map:before{content:"\f203"}.ion-medkit:before{content:"\f2a2"}.ion-merge:before{content:"\f33f"}.ion-mic-a:before{content:"\f204"}.ion-mic-b:before{content:"\f205"}.ion-mic-c:before{content:"\f206"}.ion-minus:before{content:"\f209"}.ion-minus-circled:before{content:"\f207"}.ion-minus-round:before{content:"\f208"}.ion-model-s:before{content:"\f2c1"}.ion-monitor:before{content:"\f20a"}.ion-more:before{content:"\f20b"}.ion-mouse:before{content:"\f340"}.ion-music-note:before{content:"\f20c"}.ion-navicon:before{content:"\f20e"}.ion-navicon-round:before{content:"\f20d"}.ion-navigate:before{content:"\f2a3"}.ion-network:before{content:"\f341"}.ion-no-smoking:before{content:"\f2c2"}.ion-nuclear:before{content:"\f2a4"}.ion-outlet:before{content:"\f342"}.ion-paintbrush:before{content:"\f4d5"}.ion-paintbucket:before{content:"\f4d6"}.ion-paper-airplane:before{content:"\f2c3"}.ion-paperclip:before{content:"\f20f"}.ion-pause:before{content:"\f210"}.ion-person:before{content:"\f213"}.ion-person-add:before{content:"\f211"}.ion-person-stalker:before{content:"\f212"}.ion-pie-graph:before{content:"\f2a5"}.ion-pin:before{content:"\f2a6"}.ion-pinpoint:before{content:"\f2a7"}.ion-pizza:before{content:"\f2a8"}.ion-plane:before{content:"\f214"}.ion-planet:before{content:"\f343"}.ion-play:before{content:"\f215"}.ion-playstation:before{content:"\f30a"}.ion-plus:before{content:"\f218"}.ion-plus-circled:before{content:"\f216"}.ion-plus-round:before{content:"\f217"}.ion-podium:before{content:"\f344"}.ion-pound:before{content:"\f219"}.ion-power:before{content:"\f2a9"}.ion-pricetag:before{content:"\f2aa"}.ion-pricetags:before{content:"\f2ab"}.ion-printer:before{content:"\f21a"}.ion-pull-request:before{content:"\f345"}.ion-qr-scanner:before{content:"\f346"}.ion-quote:before{content:"\f347"}.ion-radio-waves:before{content:"\f2ac"}.ion-record:before{content:"\f21b"}.ion-refresh:before{content:"\f21c"}.ion-reply:before{content:"\f21e"}.ion-reply-all:before{content:"\f21d"}.ion-ribbon-a:before{content:"\f348"}.ion-ribbon-b:before{content:"\f349"}.ion-sad:before{content:"\f34a"}.ion-sad-outline:before{content:"\f4d7"}.ion-scissors:before{content:"\f34b"}.ion-search:before{content:"\f21f"}.ion-settings:before{content:"\f2ad"}.ion-share:before{content:"\f220"}.ion-shuffle:before{content:"\f221"}.ion-skip-backward:before{content:"\f222"}.ion-skip-forward:before{content:"\f223"}.ion-social-android:before{content:"\f225"}.ion-social-android-outline:before{content:"\f224"}.ion-social-angular:before{content:"\f4d9"}.ion-social-angular-outline:before{content:"\f4d8"}.ion-social-apple:before{content:"\f227"}.ion-social-apple-outline:before{content:"\f226"}.ion-social-bitcoin:before{content:"\f2af"}.ion-social-bitcoin-outline:before{content:"\f2ae"}.ion-social-buffer:before{content:"\f229"}.ion-social-buffer-outline:before{content:"\f228"}.ion-social-chrome:before{content:"\f4db"}.ion-social-chrome-outline:before{content:"\f4da"}.ion-social-codepen:before{content:"\f4dd"}.ion-social-codepen-outline:before{content:"\f4dc"}.ion-social-css3:before{content:"\f4df"}.ion-social-css3-outline:before{content:"\f4de"}.ion-social-designernews:before{content:"\f22b"}.ion-social-designernews-outline:before{content:"\f22a"}.ion-social-dribbble:before{content:"\f22d"}.ion-social-dribbble-outline:before{content:"\f22c"}.ion-social-dropbox:before{content:"\f22f"}.ion-social-dropbox-outline:before{content:"\f22e"}.ion-social-euro:before{content:"\f4e1"}.ion-social-euro-outline:before{content:"\f4e0"}.ion-social-facebook:before{content:"\f231"}.ion-social-facebook-outline:before{content:"\f230"}.ion-social-foursquare:before{content:"\f34d"}.ion-social-foursquare-outline:before{content:"\f34c"}.ion-social-freebsd-devil:before{content:"\f2c4"}.ion-social-github:before{content:"\f233"}.ion-social-github-outline:before{content:"\f232"}.ion-social-google:before{content:"\f34f"}.ion-social-google-outline:before{content:"\f34e"}.ion-social-googleplus:before{content:"\f235"}.ion-social-googleplus-outline:before{content:"\f234"}.ion-social-hackernews:before{content:"\f237"}.ion-social-hackernews-outline:before{content:"\f236"}.ion-social-html5:before{content:"\f4e3"}.ion-social-html5-outline:before{content:"\f4e2"}.ion-social-instagram:before{content:"\f351"}.ion-social-instagram-outline:before{content:"\f350"}.ion-social-javascript:before{content:"\f4e5"}.ion-social-javascript-outline:before{content:"\f4e4"}.ion-social-linkedin:before{content:"\f239"}.ion-social-linkedin-outline:before{content:"\f238"}.ion-social-markdown:before{content:"\f4e6"}.ion-social-nodejs:before{content:"\f4e7"}.ion-social-octocat:before{content:"\f4e8"}.ion-social-pinterest:before{content:"\f2b1"}.ion-social-pinterest-outline:before{content:"\f2b0"}.ion-social-python:before{content:"\f4e9"}.ion-social-reddit:before{content:"\f23b"}.ion-social-reddit-outline:before{content:"\f23a"}.ion-social-rss:before{content:"\f23d"}.ion-social-rss-outline:before{content:"\f23c"}.ion-social-sass:before{content:"\f4ea"}.ion-social-skype:before{content:"\f23f"}.ion-social-skype-outline:before{content:"\f23e"}.ion-social-snapchat:before{content:"\f4ec"}.ion-social-snapchat-outline:before{content:"\f4eb"}.ion-social-tumblr:before{content:"\f241"}.ion-social-tumblr-outline:before{content:"\f240"}.ion-social-tux:before{content:"\f2c5"}.ion-social-twitch:before{content:"\f4ee"}.ion-social-twitch-outline:before{content:"\f4ed"}.ion-social-twitter:before{content:"\f243"}.ion-social-twitter-outline:before{content:"\f242"}.ion-social-usd:before{content:"\f353"}.ion-social-usd-outline:before{content:"\f352"}.ion-social-vimeo:before{content:"\f245"}.ion-social-vimeo-outline:before{content:"\f244"}.ion-social-whatsapp:before{content:"\f4f0"}.ion-social-whatsapp-outline:before{content:"\f4ef"}.ion-social-windows:before{content:"\f247"}.ion-social-windows-outline:before{content:"\f246"}.ion-social-wordpress:before{content:"\f249"}.ion-social-wordpress-outline:before{content:"\f248"}.ion-social-yahoo:before{content:"\f24b"}.ion-social-yahoo-outline:before{content:"\f24a"}.ion-social-yen:before{content:"\f4f2"}.ion-social-yen-outline:before{content:"\f4f1"}.ion-social-youtube:before{content:"\f24d"}.ion-social-youtube-outline:before{content:"\f24c"}.ion-soup-can:before{content:"\f4f4"}.ion-soup-can-outline:before{content:"\f4f3"}.ion-speakerphone:before{content:"\f2b2"}.ion-speedometer:before{content:"\f2b3"}.ion-spoon:before{content:"\f2b4"}.ion-star:before{content:"\f24e"}.ion-stats-bars:before{content:"\f2b5"}.ion-steam:before{content:"\f30b"}.ion-stop:before{content:"\f24f"}.ion-thermometer:before{content:"\f2b6"}.ion-thumbsdown:before{content:"\f250"}.ion-thumbsup:before{content:"\f251"}.ion-toggle:before{content:"\f355"}.ion-toggle-filled:before{content:"\f354"}.ion-transgender:before{content:"\f4f5"}.ion-trash-a:before{content:"\f252"}.ion-trash-b:before{content:"\f253"}.ion-trophy:before{content:"\f356"}.ion-tshirt:before{content:"\f4f7"}.ion-tshirt-outline:before{content:"\f4f6"}.ion-umbrella:before{content:"\f2b7"}.ion-university:before{content:"\f357"}.ion-unlocked:before{content:"\f254"}.ion-upload:before{content:"\f255"}.ion-usb:before{content:"\f2b8"}.ion-videocamera:before{content:"\f256"}.ion-volume-high:before{content:"\f257"}.ion-volume-low:before{content:"\f258"}.ion-volume-medium:before{content:"\f259"}.ion-volume-mute:before{content:"\f25a"}.ion-wand:before{content:"\f358"}.ion-waterdrop:before{content:"\f25b"}.ion-wifi:before{content:"\f25c"}.ion-wineglass:before{content:"\f2b9"}.ion-woman:before{content:"\f25d"}.ion-wrench:before{content:"\f2ba"}.ion-xbox:before{content:"\f30c"} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot new file mode 100644 index 0000000..92a3f20 Binary files /dev/null and b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot differ diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.svg b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.svg new file mode 100644 index 0000000..49fc8f3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.svg @@ -0,0 +1,2230 @@ + + + + + +Created by FontForge 20120731 at Thu Dec 4 09:51:48 2014 + By Adam Bradley +Created by Adam Bradley with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf new file mode 100644 index 0000000..c4e4632 Binary files /dev/null and b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf differ diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff new file mode 100644 index 0000000..5f3a14e Binary files /dev/null and b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff differ diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js new file mode 100644 index 0000000..234f9b3 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js @@ -0,0 +1,2 @@ +/*! pace 1.0.2 */ +(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cQ;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='
\n
\n
\n
',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e;e=[];for(d in b.prototype)try{e.push(null==a[d]&&"function"!=typeof b[d]?"function"==typeof Object.defineProperty?Object.defineProperty(a,d,{get:function(){return b.prototype[d]},configurable:!0,enumerable:!0}):a[d]=b.prototype[d]:void 0)}catch(f){c=f}return e},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(["pace"],function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css new file mode 100644 index 0000000..d9bca46 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css @@ -0,0 +1,77 @@ +/* This is a compiled file, you should be editing the file in the templates directory */ +.pace { + -webkit-pointer-events: none; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background: #2299dd; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 2px; +} + +.pace .pace-progress-inner { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #2299dd, 0 0 5px #2299dd; + opacity: 1.0; + -webkit-transform: rotate(3deg) translate(0px, -4px); + -moz-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + -o-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +.pace .pace-activity { + display: block; + position: fixed; + z-index: 2000; + top: 15px; + right: 15px; + width: 14px; + height: 14px; + border: solid 2px transparent; + border-top-color: #2299dd; + border-left-color: #2299dd; + border-radius: 10px; + -webkit-animation: pace-spinner 400ms linear infinite; + -moz-animation: pace-spinner 400ms linear infinite; + -ms-animation: pace-spinner 400ms linear infinite; + -o-animation: pace-spinner 400ms linear infinite; + animation: pace-spinner 400ms linear infinite; +} + +@-webkit-keyframes pace-spinner { + 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } +} +@-moz-keyframes pace-spinner { + 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } +} +@-o-keyframes pace-spinner { + 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } +} +@-ms-keyframes pace-spinner { + 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } +} +@keyframes pace-spinner { + 0% { transform: rotate(0deg); transform: rotate(0deg); } + 100% { transform: rotate(360deg); transform: rotate(360deg); } +} diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css new file mode 100644 index 0000000..86f4b77 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css @@ -0,0 +1,269 @@ +.daterangepicker { + position: absolute; + color: inherit; + background-color: #fff; + border-radius: 4px; + width: 278px; + padding: 4px; + margin-top: 1px; + top: 100px; + left: 20px; + /* Calendars */ } + .daterangepicker:before, .daterangepicker:after { + position: absolute; + display: inline-block; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; } + .daterangepicker:before { + top: -7px; + border-right: 7px solid transparent; + border-left: 7px solid transparent; + border-bottom: 7px solid #ccc; } + .daterangepicker:after { + top: -6px; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; } + .daterangepicker.opensleft:before { + right: 9px; } + .daterangepicker.opensleft:after { + right: 10px; } + .daterangepicker.openscenter:before { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; } + .daterangepicker.openscenter:after { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; } + .daterangepicker.opensright:before { + left: 9px; } + .daterangepicker.opensright:after { + left: 10px; } + .daterangepicker.dropup { + margin-top: -5px; } + .daterangepicker.dropup:before { + top: initial; + bottom: -7px; + border-bottom: initial; + border-top: 7px solid #ccc; } + .daterangepicker.dropup:after { + top: initial; + bottom: -6px; + border-bottom: initial; + border-top: 6px solid #fff; } + .daterangepicker.dropdown-menu { + max-width: none; + z-index: 3001; } + .daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; } + .daterangepicker.show-calendar .calendar { + display: block; } + .daterangepicker .calendar { + display: none; + max-width: 270px; + margin: 4px; } + .daterangepicker .calendar.single .calendar-table { + border: none; } + .daterangepicker .calendar th, .daterangepicker .calendar td { + white-space: nowrap; + text-align: center; + min-width: 32px; } + .daterangepicker .calendar-table { + border: 1px solid #fff; + padding: 4px; + border-radius: 4px; + background-color: #fff; } + .daterangepicker table { + width: 100%; + margin: 0; } + .daterangepicker td, .daterangepicker th { + text-align: center; + width: 20px; + height: 20px; + border-radius: 4px; + border: 1px solid transparent; + white-space: nowrap; + cursor: pointer; } + .daterangepicker td.available:hover, .daterangepicker th.available:hover { + background-color: #eee; + border-color: transparent; + color: inherit; } + .daterangepicker td.week, .daterangepicker th.week { + font-size: 80%; + color: #ccc; } + .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { + background-color: #fff; + border-color: transparent; + color: #999; } + .daterangepicker td.in-range { + background-color: #ebf4f8; + border-color: transparent; + color: #000; + border-radius: 0; } + .daterangepicker td.start-date { + border-radius: 4px 0 0 4px; } + .daterangepicker td.end-date { + border-radius: 0 4px 4px 0; } + .daterangepicker td.start-date.end-date { + border-radius: 4px; } + .daterangepicker td.active, .daterangepicker td.active:hover { + background-color: #357ebd; + border-color: transparent; + color: #fff; } + .daterangepicker th.month { + width: auto; } + .daterangepicker td.disabled, .daterangepicker option.disabled { + color: #999; + cursor: not-allowed; + text-decoration: line-through; } + .daterangepicker select.monthselect, .daterangepicker select.yearselect { + font-size: 12px; + padding: 1px; + height: auto; + margin: 0; + cursor: default; } + .daterangepicker select.monthselect { + margin-right: 2%; + width: 56%; } + .daterangepicker select.yearselect { + width: 40%; } + .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { + width: 50px; + margin-bottom: 0; } + .daterangepicker .input-mini { + border: 1px solid #ccc; + border-radius: 4px; + color: #555; + height: 30px; + line-height: 30px; + display: block; + vertical-align: middle; + margin: 0 0 5px 0; + padding: 0 6px 0 28px; + width: 100%; } + .daterangepicker .input-mini.active { + border: 1px solid #08c; + border-radius: 4px; } + .daterangepicker .daterangepicker_input { + position: relative; } + .daterangepicker .daterangepicker_input i { + position: absolute; + left: 8px; + top: 8px; } + .daterangepicker.rtl .input-mini { + padding-right: 28px; + padding-left: 6px; } + .daterangepicker.rtl .daterangepicker_input i { + left: auto; + right: 8px; } + .daterangepicker .calendar-time { + text-align: center; + margin: 5px auto; + line-height: 30px; + position: relative; + padding-left: 28px; } + .daterangepicker .calendar-time select.disabled { + color: #ccc; + cursor: not-allowed; } + +.ranges { + font-size: 11px; + float: none; + margin: 4px; + text-align: left; } + .ranges ul { + list-style: none; + margin: 0 auto; + padding: 0; + width: 100%; } + .ranges li { + font-size: 13px; + background-color: #f5f5f5; + border: 1px solid #f5f5f5; + border-radius: 4px; + color: #08c; + padding: 3px 12px; + margin-bottom: 8px; + cursor: pointer; } + .ranges li:hover { + background-color: #08c; + border: 1px solid #08c; + color: #fff; } + .ranges li.active { + background-color: #08c; + border: 1px solid #08c; + color: #fff; } + +/* Larger Screen Styling */ +@media (min-width: 564px) { + .daterangepicker { + width: auto; } + .daterangepicker .ranges ul { + width: 160px; } + .daterangepicker.single .ranges ul { + width: 100%; } + .daterangepicker.single .calendar.left { + clear: none; } + .daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .calendar { + float: left; } + .daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .calendar { + float: right; } + .daterangepicker.ltr { + direction: ltr; + text-align: left; } + .daterangepicker.ltr .calendar.left { + clear: left; + margin-right: 0; } + .daterangepicker.ltr .calendar.left .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + .daterangepicker.ltr .calendar.right { + margin-left: 0; } + .daterangepicker.ltr .calendar.right .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + .daterangepicker.ltr .left .daterangepicker_input { + padding-right: 12px; } + .daterangepicker.ltr .calendar.left .calendar-table { + padding-right: 12px; } + .daterangepicker.ltr .ranges, .daterangepicker.ltr .calendar { + float: left; } + .daterangepicker.rtl { + direction: rtl; + text-align: right; } + .daterangepicker.rtl .calendar.left { + clear: right; + margin-left: 0; } + .daterangepicker.rtl .calendar.left .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + .daterangepicker.rtl .calendar.right { + margin-right: 0; } + .daterangepicker.rtl .calendar.right .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + .daterangepicker.rtl .left .daterangepicker_input { + padding-left: 12px; } + .daterangepicker.rtl .calendar.left .calendar-table { + padding-left: 12px; } + .daterangepicker.rtl .ranges, .daterangepicker.rtl .calendar { + text-align: right; + float: right; } } +@media (min-width: 730px) { + .daterangepicker .ranges { + width: auto; } + .daterangepicker.ltr .ranges { + float: left; } + .daterangepicker.rtl .ranges { + float: right; } + .daterangepicker .calendar.left { + clear: none !important; } } diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js new file mode 100644 index 0000000..079cde6 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js @@ -0,0 +1,1653 @@ +/** +* @version: 2.1.27 +* @author: Dan Grossman http://www.dangrossman.info/ +* @copyright: Copyright (c) 2012-2017 Dan Grossman. All rights reserved. +* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php +* @website: http://www.daterangepicker.com/ +*/ +// Follow the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Make globaly available as well + define(['moment', 'jquery'], function (moment, jquery) { + if (!jquery.fn) jquery.fn = {}; // webpack server rendering + return factory(moment, jquery); + }); + } else if (typeof module === 'object' && module.exports) { + // Node / Browserify + //isomorphic issue + var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined; + if (!jQuery) { + jQuery = require('jquery'); + if (!jQuery.fn) jQuery.fn = {}; + } + var moment = (typeof window != 'undefined' && typeof window.moment != 'undefined') ? window.moment : require('moment'); + module.exports = factory(moment, jQuery); + } else { + // Browser globals + root.daterangepicker = factory(root.moment, root.jQuery); + } +}(this, function(moment, $) { + var DateRangePicker = function(element, options, cb) { + + //default settings for options + this.parentEl = 'body'; + this.element = $(element); + this.startDate = moment().startOf('day'); + this.endDate = moment().endOf('day'); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + this.autoApply = false; + this.singleDatePicker = false; + this.showDropdowns = false; + this.showWeekNumbers = false; + this.showISOWeekNumbers = false; + this.showCustomRangeLabel = true; + this.timePicker = false; + this.timePicker24Hour = false; + this.timePickerIncrement = 1; + this.timePickerSeconds = false; + this.linkedCalendars = true; + this.autoUpdateInput = true; + this.alwaysShowCalendars = false; + this.ranges = {}; + + this.opens = 'right'; + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + this.drops = 'down'; + if (this.element.hasClass('dropup')) + this.drops = 'up'; + + this.buttonClasses = 'btn btn-sm'; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.locale = { + direction: 'ltr', + format: moment.localeData().longDateFormat('L'), + separator: ' - ', + applyLabel: 'Apply', + cancelLabel: 'Cancel', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment.weekdaysMin(), + monthNames: moment.monthsShort(), + firstDay: moment.localeData().firstDayOfWeek() + }; + + this.callback = function() { }; + + //some state information + this.isShowing = false; + this.leftCalendar = {}; + this.rightCalendar = {}; + + //custom options from user + if (typeof options !== 'object' || options === null) + options = {}; + + //allow setting options with data attributes + //data-api options will be overwritten with custom javascript options + options = $.extend(this.element.data(), options); + + //html template for the picker UI + if (typeof options.template !== 'string' && !(options.template instanceof $)) + options.template = ''; + + this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); + this.container = $(options.template).appendTo(this.parentEl); + + // + // handle all the possible options overriding defaults + // + + if (typeof options.locale === 'object') { + + if (typeof options.locale.direction === 'string') + this.locale.direction = options.locale.direction; + + if (typeof options.locale.format === 'string') + this.locale.format = options.locale.format; + + if (typeof options.locale.separator === 'string') + this.locale.separator = options.locale.separator; + + if (typeof options.locale.daysOfWeek === 'object') + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + + if (typeof options.locale.monthNames === 'object') + this.locale.monthNames = options.locale.monthNames.slice(); + + if (typeof options.locale.firstDay === 'number') + this.locale.firstDay = options.locale.firstDay; + + if (typeof options.locale.applyLabel === 'string') + this.locale.applyLabel = options.locale.applyLabel; + + if (typeof options.locale.cancelLabel === 'string') + this.locale.cancelLabel = options.locale.cancelLabel; + + if (typeof options.locale.weekLabel === 'string') + this.locale.weekLabel = options.locale.weekLabel; + + if (typeof options.locale.customRangeLabel === 'string'){ + //Support unicode chars in the custom range name. + var elem = document.createElement('textarea'); + elem.innerHTML = options.locale.customRangeLabel; + var rangeHtml = elem.value; + this.locale.customRangeLabel = rangeHtml; + } + } + this.container.addClass(this.locale.direction); + + if (typeof options.startDate === 'string') + this.startDate = moment(options.startDate, this.locale.format); + + if (typeof options.endDate === 'string') + this.endDate = moment(options.endDate, this.locale.format); + + if (typeof options.minDate === 'string') + this.minDate = moment(options.minDate, this.locale.format); + + if (typeof options.maxDate === 'string') + this.maxDate = moment(options.maxDate, this.locale.format); + + if (typeof options.startDate === 'object') + this.startDate = moment(options.startDate); + + if (typeof options.endDate === 'object') + this.endDate = moment(options.endDate); + + if (typeof options.minDate === 'object') + this.minDate = moment(options.minDate); + + if (typeof options.maxDate === 'object') + this.maxDate = moment(options.maxDate); + + // sanity check for bad options + if (this.minDate && this.startDate.isBefore(this.minDate)) + this.startDate = this.minDate.clone(); + + // sanity check for bad options + if (this.maxDate && this.endDate.isAfter(this.maxDate)) + this.endDate = this.maxDate.clone(); + + if (typeof options.applyClass === 'string') + this.applyClass = options.applyClass; + + if (typeof options.cancelClass === 'string') + this.cancelClass = options.cancelClass; + + if (typeof options.dateLimit === 'object') + this.dateLimit = options.dateLimit; + + if (typeof options.opens === 'string') + this.opens = options.opens; + + if (typeof options.drops === 'string') + this.drops = options.drops; + + if (typeof options.showWeekNumbers === 'boolean') + this.showWeekNumbers = options.showWeekNumbers; + + if (typeof options.showISOWeekNumbers === 'boolean') + this.showISOWeekNumbers = options.showISOWeekNumbers; + + if (typeof options.buttonClasses === 'string') + this.buttonClasses = options.buttonClasses; + + if (typeof options.buttonClasses === 'object') + this.buttonClasses = options.buttonClasses.join(' '); + + if (typeof options.showDropdowns === 'boolean') + this.showDropdowns = options.showDropdowns; + + if (typeof options.showCustomRangeLabel === 'boolean') + this.showCustomRangeLabel = options.showCustomRangeLabel; + + if (typeof options.singleDatePicker === 'boolean') { + this.singleDatePicker = options.singleDatePicker; + if (this.singleDatePicker) + this.endDate = this.startDate.clone(); + } + + if (typeof options.timePicker === 'boolean') + this.timePicker = options.timePicker; + + if (typeof options.timePickerSeconds === 'boolean') + this.timePickerSeconds = options.timePickerSeconds; + + if (typeof options.timePickerIncrement === 'number') + this.timePickerIncrement = options.timePickerIncrement; + + if (typeof options.timePicker24Hour === 'boolean') + this.timePicker24Hour = options.timePicker24Hour; + + if (typeof options.autoApply === 'boolean') + this.autoApply = options.autoApply; + + if (typeof options.autoUpdateInput === 'boolean') + this.autoUpdateInput = options.autoUpdateInput; + + if (typeof options.linkedCalendars === 'boolean') + this.linkedCalendars = options.linkedCalendars; + + if (typeof options.isInvalidDate === 'function') + this.isInvalidDate = options.isInvalidDate; + + if (typeof options.isCustomDate === 'function') + this.isCustomDate = options.isCustomDate; + + if (typeof options.alwaysShowCalendars === 'boolean') + this.alwaysShowCalendars = options.alwaysShowCalendars; + + // update day names order to firstDay + if (this.locale.firstDay != 0) { + var iterator = this.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + + var start, end, range; + + //if no start/end dates set, check if an input element contains initial values + if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(), + split = val.split(this.locale.separator); + + start = end = null; + + if (split.length == 2) { + start = moment(split[0], this.locale.format); + end = moment(split[1], this.locale.format); + } else if (this.singleDatePicker && val !== "") { + start = moment(val, this.locale.format); + end = moment(val, this.locale.format); + } + if (start !== null && end !== null) { + this.setStartDate(start); + this.setEndDate(end); + } + } + } + + if (typeof options.ranges === 'object') { + for (range in options.ranges) { + + if (typeof options.ranges[range][0] === 'string') + start = moment(options.ranges[range][0], this.locale.format); + else + start = moment(options.ranges[range][0]); + + if (typeof options.ranges[range][1] === 'string') + end = moment(options.ranges[range][1], this.locale.format); + else + end = moment(options.ranges[range][1]); + + // If the start or end date exceed those allowed by the minDate or dateLimit + // options, shorten the range to the allowable period. + if (this.minDate && start.isBefore(this.minDate)) + start = this.minDate.clone(); + + var maxDate = this.maxDate; + if (this.dateLimit && maxDate && start.clone().add(this.dateLimit).isAfter(maxDate)) + maxDate = start.clone().add(this.dateLimit); + if (maxDate && end.isAfter(maxDate)) + end = maxDate.clone(); + + // If the end of the range is before the minimum or the start of the range is + // after the maximum, don't display this range option at all. + if ((this.minDate && end.isBefore(this.minDate, this.timepicker ? 'minute' : 'day')) + || (maxDate && start.isAfter(maxDate, this.timepicker ? 'minute' : 'day'))) + continue; + + //Support unicode chars in the range names. + var elem = document.createElement('textarea'); + elem.innerHTML = range; + var rangeHtml = elem.value; + + this.ranges[rangeHtml] = [start, end]; + } + + var list = '
    '; + for (range in this.ranges) { + list += '
  • ' + range + '
  • '; + } + if (this.showCustomRangeLabel) { + list += '
  • ' + this.locale.customRangeLabel + '
  • '; + } + list += '
'; + this.container.find('.ranges').prepend(list); + } + + if (typeof cb === 'function') { + this.callback = cb; + } + + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.endOf('day'); + this.container.find('.calendar-time').hide(); + } + + //can't be used together for now + if (this.timePicker && this.autoApply) + this.autoApply = false; + + if (this.autoApply && typeof options.ranges !== 'object') { + this.container.find('.ranges').hide(); + } else if (this.autoApply) { + this.container.find('.applyBtn, .cancelBtn').addClass('hide'); + } + + if (this.singleDatePicker) { + this.container.addClass('single'); + this.container.find('.calendar.left').addClass('single'); + this.container.find('.calendar.left').show(); + this.container.find('.calendar.right').hide(); + this.container.find('.daterangepicker_input input, .daterangepicker_input > i').hide(); + if (this.timePicker) { + this.container.find('.ranges ul').hide(); + } else { + this.container.find('.ranges').hide(); + } + } + + if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) { + this.container.addClass('show-calendar'); + } + + this.container.addClass('opens' + this.opens); + + //swap the position of the predefined ranges if opens right + if (typeof options.ranges !== 'undefined' && this.opens == 'right') { + this.container.find('.ranges').prependTo( this.container.find('.calendar.left').parent() ); + } + + //apply CSS classes and labels to buttons + this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses); + if (this.applyClass.length) + this.container.find('.applyBtn').addClass(this.applyClass); + if (this.cancelClass.length) + this.container.find('.cancelBtn').addClass(this.cancelClass); + this.container.find('.applyBtn').html(this.locale.applyLabel); + this.container.find('.cancelBtn').html(this.locale.cancelLabel); + + // + // event listeners + // + + this.container.find('.calendar') + .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) + .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) + .on('mousedown.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) + .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this)) + .on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this)) + .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this)) + .on('click.daterangepicker', '.daterangepicker_input input', $.proxy(this.showCalendars, this)) + .on('focus.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsFocused, this)) + .on('blur.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsBlurred, this)) + .on('change.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this)) + .on('keydown.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsKeydown, this)); + + this.container.find('.ranges') + .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter.daterangepicker', 'li', $.proxy(this.hoverRange, this)) + .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); + + if (this.element.is('input') || this.element.is('button')) { + this.element.on({ + 'click.daterangepicker': $.proxy(this.show, this), + 'focus.daterangepicker': $.proxy(this.show, this), + 'keyup.daterangepicker': $.proxy(this.elementChanged, this), + 'keydown.daterangepicker': $.proxy(this.keydown, this) //IE 11 compatibility + }); + } else { + this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); + this.element.on('keydown.daterangepicker', $.proxy(this.toggle, this)); + } + + // + // if attached to a text input, set the initial value + // + + if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); + this.element.trigger('change'); + } else if (this.element.is('input') && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format)); + this.element.trigger('change'); + } + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + setStartDate: function(startDate) { + if (typeof startDate === 'string') + this.startDate = moment(startDate, this.locale.format); + + if (typeof startDate === 'object') + this.startDate = moment(startDate); + + if (!this.timePicker) + this.startDate = this.startDate.startOf('day'); + + if (this.timePicker && this.timePickerIncrement) + this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + + if (this.minDate && this.startDate.isBefore(this.minDate)) { + this.startDate = this.minDate.clone(); + if (this.timePicker && this.timePickerIncrement) + this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + } + + if (this.maxDate && this.startDate.isAfter(this.maxDate)) { + this.startDate = this.maxDate.clone(); + if (this.timePicker && this.timePickerIncrement) + this.startDate.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + } + + if (!this.isShowing) + this.updateElement(); + + this.updateMonthsInView(); + }, + + setEndDate: function(endDate) { + if (typeof endDate === 'string') + this.endDate = moment(endDate, this.locale.format); + + if (typeof endDate === 'object') + this.endDate = moment(endDate); + + if (!this.timePicker) + this.endDate = this.endDate.add(1,'d').startOf('day').subtract(1,'second'); + + if (this.timePicker && this.timePickerIncrement) + this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + + if (this.endDate.isBefore(this.startDate)) + this.endDate = this.startDate.clone(); + + if (this.maxDate && this.endDate.isAfter(this.maxDate)) + this.endDate = this.maxDate.clone(); + + if (this.dateLimit && this.startDate.clone().add(this.dateLimit).isBefore(this.endDate)) + this.endDate = this.startDate.clone().add(this.dateLimit); + + this.previousRightTime = this.endDate.clone(); + + if (!this.isShowing) + this.updateElement(); + + this.updateMonthsInView(); + }, + + isInvalidDate: function() { + return false; + }, + + isCustomDate: function() { + return false; + }, + + updateView: function() { + if (this.timePicker) { + this.renderTimePicker('left'); + this.renderTimePicker('right'); + if (!this.endDate) { + this.container.find('.right .calendar-time select').attr('disabled', 'disabled').addClass('disabled'); + } else { + this.container.find('.right .calendar-time select').removeAttr('disabled').removeClass('disabled'); + } + } + if (this.endDate) { + this.container.find('input[name="daterangepicker_end"]').removeClass('active'); + this.container.find('input[name="daterangepicker_start"]').addClass('active'); + } else { + this.container.find('input[name="daterangepicker_end"]').addClass('active'); + this.container.find('input[name="daterangepicker_start"]').removeClass('active'); + } + this.updateMonthsInView(); + this.updateCalendars(); + this.updateFormInputs(); + }, + + updateMonthsInView: function() { + if (this.endDate) { + + //if both dates are visible already, do nothing + if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month && + (this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) + && + (this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) + ) { + return; + } + + this.leftCalendar.month = this.startDate.clone().date(2); + if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) { + this.rightCalendar.month = this.endDate.clone().date(2); + } else { + this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); + } + + } else { + if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) { + this.leftCalendar.month = this.startDate.clone().date(2); + this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); + } + } + if (this.maxDate && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.maxDate) { + this.rightCalendar.month = this.maxDate.clone().date(2); + this.leftCalendar.month = this.maxDate.clone().date(2).subtract(1, 'month'); + } + }, + + updateCalendars: function() { + + if (this.timePicker) { + var hour, minute, second; + if (this.endDate) { + hour = parseInt(this.container.find('.left .hourselect').val(), 10); + minute = parseInt(this.container.find('.left .minuteselect').val(), 10); + second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; + if (!this.timePicker24Hour) { + var ampm = this.container.find('.left .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + } else { + hour = parseInt(this.container.find('.right .hourselect').val(), 10); + minute = parseInt(this.container.find('.right .minuteselect').val(), 10); + second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; + if (!this.timePicker24Hour) { + var ampm = this.container.find('.right .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + } + this.leftCalendar.month.hour(hour).minute(minute).second(second); + this.rightCalendar.month.hour(hour).minute(minute).second(second); + } + + this.renderCalendar('left'); + this.renderCalendar('right'); + + //highlight any predefined range matching the current start and end dates + this.container.find('.ranges li').removeClass('active'); + if (this.endDate == null) return; + + this.calculateChosenLabel(); + }, + + renderCalendar: function(side) { + + // + // Build the matrix of dates that will populate the calendar + // + + var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar; + var month = calendar.month.month(); + var year = calendar.month.year(); + var hour = calendar.month.hour(); + var minute = calendar.month.minute(); + var second = calendar.month.second(); + var daysInMonth = moment([year, month]).daysInMonth(); + var firstDay = moment([year, month, 1]); + var lastDay = moment([year, month, daysInMonth]); + var lastMonth = moment(firstDay).subtract(1, 'month').month(); + var lastYear = moment(firstDay).subtract(1, 'month').year(); + var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); + var dayOfWeek = firstDay.day(); + + //initialize a 6 rows x 7 columns array for the calendar + var calendar = []; + calendar.firstDay = firstDay; + calendar.lastDay = lastDay; + + for (var i = 0; i < 6; i++) { + calendar[i] = []; + } + + //populate the calendar with date objects + var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; + if (startDay > daysInLastMonth) + startDay -= 7; + + if (dayOfWeek == this.locale.firstDay) + startDay = daysInLastMonth - 6; + + var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]); + + var col, row; + for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { + if (i > 0 && col % 7 === 0) { + col = 0; + row++; + } + calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second); + curDate.hour(12); + + if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { + calendar[row][col] = this.minDate.clone(); + } + + if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { + calendar[row][col] = this.maxDate.clone(); + } + + } + + //make the calendar object available to hoverDate/clickDate + if (side == 'left') { + this.leftCalendar.calendar = calendar; + } else { + this.rightCalendar.calendar = calendar; + } + + // + // Display the calendar + // + + var minDate = side == 'left' ? this.minDate : this.startDate; + var maxDate = this.maxDate; + var selected = side == 'left' ? this.startDate : this.endDate; + var arrow = this.locale.direction == 'ltr' ? {left: 'chevron-left', right: 'chevron-right'} : {left: 'chevron-right', right: 'chevron-left'}; + + var html = ''; + html += ''; + html += ''; + + // add empty cell for week number + if (this.showWeekNumbers || this.showISOWeekNumbers) + html += ''; + + if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side == 'left')) { + html += ''; + } else { + html += ''; + } + + var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); + + if (this.showDropdowns) { + var currentMonth = calendar[1][1].month(); + var currentYear = calendar[1][1].year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + var inMinYear = currentYear == minYear; + var inMaxYear = currentYear == maxYear; + + var monthHtml = '"; + + var yearHtml = ''; + + dateHtml = monthHtml + yearHtml; + } + + html += ''; + if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side == 'right' || this.singleDatePicker)) { + html += ''; + } else { + html += ''; + } + + html += ''; + html += ''; + + // add week number label + if (this.showWeekNumbers || this.showISOWeekNumbers) + html += ''; + + $.each(this.locale.daysOfWeek, function(index, dayOfWeek) { + html += ''; + }); + + html += ''; + html += ''; + html += ''; + + //adjust maxDate to reflect the dateLimit setting in order to + //grey out end dates beyond the dateLimit + if (this.endDate == null && this.dateLimit) { + var maxLimit = this.startDate.clone().add(this.dateLimit).endOf('day'); + if (!maxDate || maxLimit.isBefore(maxDate)) { + maxDate = maxLimit; + } + } + + for (var row = 0; row < 6; row++) { + html += ''; + + // add week number + if (this.showWeekNumbers) + html += ''; + else if (this.showISOWeekNumbers) + html += ''; + + for (var col = 0; col < 7; col++) { + + var classes = []; + + //highlight today's date + if (calendar[row][col].isSame(new Date(), "day")) + classes.push('today'); + + //highlight weekends + if (calendar[row][col].isoWeekday() > 5) + classes.push('weekend'); + + //grey out the dates in other months displayed at beginning and end of this calendar + if (calendar[row][col].month() != calendar[1][1].month()) + classes.push('off'); + + //don't allow selection of dates before the minimum date + if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day')) + classes.push('off', 'disabled'); + + //don't allow selection of dates after the maximum date + if (maxDate && calendar[row][col].isAfter(maxDate, 'day')) + classes.push('off', 'disabled'); + + //don't allow selection of date if a custom function decides it's invalid + if (this.isInvalidDate(calendar[row][col])) + classes.push('off', 'disabled'); + + //highlight the currently selected start date + if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) + classes.push('active', 'start-date'); + + //highlight the currently selected end date + if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) + classes.push('active', 'end-date'); + + //highlight dates in-between the selected dates + if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate) + classes.push('in-range'); + + //apply custom classes for this date + var isCustom = this.isCustomDate(calendar[row][col]); + if (isCustom !== false) { + if (typeof isCustom === 'string') + classes.push(isCustom); + else + Array.prototype.push.apply(classes, isCustom); + } + + var cname = '', disabled = false; + for (var i = 0; i < classes.length; i++) { + cname += classes[i] + ' '; + if (classes[i] == 'disabled') + disabled = true; + } + if (!disabled) + cname += 'available'; + + html += ''; + + } + html += ''; + } + + html += ''; + html += '
' + dateHtml + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].week() + '' + calendar[row][0].isoWeek() + '' + calendar[row][col].date() + '
'; + + this.container.find('.calendar.' + side + ' .calendar-table').html(html); + + }, + + renderTimePicker: function(side) { + + // Don't bother updating the time picker if it's currently disabled + // because an end date hasn't been clicked yet + if (side == 'right' && !this.endDate) return; + + var html, selected, minDate, maxDate = this.maxDate; + + if (this.dateLimit && (!this.maxDate || this.startDate.clone().add(this.dateLimit).isAfter(this.maxDate))) + maxDate = this.startDate.clone().add(this.dateLimit); + + if (side == 'left') { + selected = this.startDate.clone(); + minDate = this.minDate; + } else if (side == 'right') { + selected = this.endDate.clone(); + minDate = this.startDate; + + //Preserve the time already selected + var timeSelector = this.container.find('.calendar.right .calendar-time div'); + if (timeSelector.html() != '') { + + selected.hour(timeSelector.find('.hourselect option:selected').val() || selected.hour()); + selected.minute(timeSelector.find('.minuteselect option:selected').val() || selected.minute()); + selected.second(timeSelector.find('.secondselect option:selected').val() || selected.second()); + + if (!this.timePicker24Hour) { + var ampm = timeSelector.find('.ampmselect option:selected').val(); + if (ampm === 'PM' && selected.hour() < 12) + selected.hour(selected.hour() + 12); + if (ampm === 'AM' && selected.hour() === 12) + selected.hour(0); + } + + } + + if (selected.isBefore(this.startDate)) + selected = this.startDate.clone(); + + if (maxDate && selected.isAfter(maxDate)) + selected = maxDate.clone(); + + } + + // + // hours + // + + html = ' '; + + // + // minutes + // + + html += ': '; + + // + // seconds + // + + if (this.timePickerSeconds) { + html += ': '; + } + + // + // AM/PM + // + + if (!this.timePicker24Hour) { + html += ''; + } + + this.container.find('.calendar.' + side + ' .calendar-time div').html(html); + + }, + + updateFormInputs: function() { + + //ignore mouse movements while an above-calendar text input has focus + if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + return; + + this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.locale.format)); + if (this.endDate) + this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.locale.format)); + + if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) { + this.container.find('button.applyBtn').removeAttr('disabled'); + } else { + this.container.find('button.applyBtn').attr('disabled', 'disabled'); + } + + }, + + move: function() { + var parentOffset = { top: 0, left: 0 }, + containerTop; + var parentRightEdge = $(window).width(); + if (!this.parentEl.is('body')) { + parentOffset = { + top: this.parentEl.offset().top - this.parentEl.scrollTop(), + left: this.parentEl.offset().left - this.parentEl.scrollLeft() + }; + parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; + } + + if (this.drops == 'up') + containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top; + else + containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top; + this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('dropup'); + + if (this.opens == 'left') { + this.container.css({ + top: containerTop, + right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), + left: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else if (this.opens == 'center') { + this.container.css({ + top: containerTop, + left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 + - this.container.outerWidth() / 2, + right: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else { + this.container.css({ + top: containerTop, + left: this.element.offset().left - parentOffset.left, + right: 'auto' + }); + if (this.container.offset().left + this.container.outerWidth() > $(window).width()) { + this.container.css({ + left: 'auto', + right: 0 + }); + } + } + }, + + show: function(e) { + if (this.isShowing) return; + + // Create a click proxy that is private to this instance of datepicker, for unbinding + this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this); + + // Bind global datepicker mousedown for hiding and + $(document) + .on('mousedown.daterangepicker', this._outsideClickProxy) + // also support mobile devices + .on('touchend.daterangepicker', this._outsideClickProxy) + // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them + .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) + // and also close when focus changes to outside the picker (eg. tabbing between controls) + .on('focusin.daterangepicker', this._outsideClickProxy); + + // Reposition the picker if the window is resized while it's open + $(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this)); + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + this.previousRightTime = this.endDate.clone(); + + this.updateView(); + this.container.show(); + this.move(); + this.element.trigger('show.daterangepicker', this); + this.isShowing = true; + }, + + hide: function(e) { + if (!this.isShowing) return; + + //incomplete date selection, revert to last values + if (!this.endDate) { + this.startDate = this.oldStartDate.clone(); + this.endDate = this.oldEndDate.clone(); + } + + //if a new date range was selected, invoke the user callback function + if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) + this.callback(this.startDate, this.endDate, this.chosenLabel); + + //if picker is attached to a text input, update it + this.updateElement(); + + $(document).off('.daterangepicker'); + $(window).off('.daterangepicker'); + this.container.hide(); + this.element.trigger('hide.daterangepicker', this); + this.isShowing = false; + }, + + toggle: function(e) { + if (this.isShowing) { + this.hide(); + } else { + this.show(); + } + }, + + outsideClick: function(e) { + var target = $(e.target); + // if the page is clicked anywhere except within the daterangerpicker/button + // itself then call this.hide() + if ( + // ie modal dialog fix + e.type == "focusin" || + target.closest(this.element).length || + target.closest(this.container).length || + target.closest('.calendar-table').length + ) return; + this.hide(); + this.element.trigger('outsideClick.daterangepicker', this); + }, + + showCalendars: function() { + this.container.addClass('show-calendar'); + this.move(); + this.element.trigger('showCalendar.daterangepicker', this); + }, + + hideCalendars: function() { + this.container.removeClass('show-calendar'); + this.element.trigger('hideCalendar.daterangepicker', this); + }, + + hoverRange: function(e) { + + //ignore mouse movements while an above-calendar text input has focus + if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + return; + + var label = e.target.getAttribute('data-range-key'); + + if (label == this.locale.customRangeLabel) { + this.updateView(); + } else { + var dates = this.ranges[label]; + this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.locale.format)); + this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.locale.format)); + } + + }, + + clickRange: function(e) { + var label = e.target.getAttribute('data-range-key'); + this.chosenLabel = label; + if (label == this.locale.customRangeLabel) { + this.showCalendars(); + } else { + var dates = this.ranges[label]; + this.startDate = dates[0]; + this.endDate = dates[1]; + + if (!this.timePicker) { + this.startDate.startOf('day'); + this.endDate.endOf('day'); + } + + if (!this.alwaysShowCalendars) + this.hideCalendars(); + this.clickApply(); + } + }, + + clickPrev: function(e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.subtract(1, 'month'); + if (this.linkedCalendars) + this.rightCalendar.month.subtract(1, 'month'); + } else { + this.rightCalendar.month.subtract(1, 'month'); + } + this.updateCalendars(); + }, + + clickNext: function(e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.add(1, 'month'); + } else { + this.rightCalendar.month.add(1, 'month'); + if (this.linkedCalendars) + this.leftCalendar.month.add(1, 'month'); + } + this.updateCalendars(); + }, + + hoverDate: function(e) { + + //ignore mouse movements while an above-calendar text input has focus + //if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + // return; + + //ignore dates that can't be selected + if (!$(e.target).hasClass('available')) return; + + //have the text inputs above calendars reflect the date being hovered over + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; + + if (this.endDate && !this.container.find('input[name=daterangepicker_start]').is(":focus")) { + this.container.find('input[name=daterangepicker_start]').val(date.format(this.locale.format)); + } else if (!this.endDate && !this.container.find('input[name=daterangepicker_end]').is(":focus")) { + this.container.find('input[name=daterangepicker_end]').val(date.format(this.locale.format)); + } + + //highlight the dates between the start date and the date being hovered as a potential end date + var leftCalendar = this.leftCalendar; + var rightCalendar = this.rightCalendar; + var startDate = this.startDate; + if (!this.endDate) { + this.container.find('.calendar tbody td').each(function(index, el) { + + //skip week numbers, only look at dates + if ($(el).hasClass('week')) return; + + var title = $(el).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(el).parents('.calendar'); + var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col]; + + if ((dt.isAfter(startDate) && dt.isBefore(date)) || dt.isSame(date, 'day')) { + $(el).addClass('in-range'); + } else { + $(el).removeClass('in-range'); + } + + }); + } + + }, + + clickDate: function(e) { + + if (!$(e.target).hasClass('available')) return; + + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; + + // + // this function needs to do a few things: + // * alternate between selecting a start and end date for the range, + // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date + // * if autoapply is enabled, and an end date was chosen, apply the selection + // * if single date picker mode, and time picker isn't enabled, apply the selection immediately + // * if one of the inputs above the calendars was focused, cancel that manual input + // + + if (this.endDate || date.isBefore(this.startDate, 'day')) { //picking start + if (this.timePicker) { + var hour = parseInt(this.container.find('.left .hourselect').val(), 10); + if (!this.timePicker24Hour) { + var ampm = this.container.find('.left .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + var minute = parseInt(this.container.find('.left .minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; + date = date.clone().hour(hour).minute(minute).second(second); + } + this.endDate = null; + this.setStartDate(date.clone()); + } else if (!this.endDate && date.isBefore(this.startDate)) { + //special case: clicking the same date for start/end, + //but the time of the end date is before the start date + this.setEndDate(this.startDate.clone()); + } else { // picking end + if (this.timePicker) { + var hour = parseInt(this.container.find('.right .hourselect').val(), 10); + if (!this.timePicker24Hour) { + var ampm = this.container.find('.right .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + var minute = parseInt(this.container.find('.right .minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; + date = date.clone().hour(hour).minute(minute).second(second); + } + this.setEndDate(date.clone()); + if (this.autoApply) { + this.calculateChosenLabel(); + this.clickApply(); + } + } + + if (this.singleDatePicker) { + this.setEndDate(this.startDate); + if (!this.timePicker) + this.clickApply(); + } + + this.updateView(); + + //This is to cancel the blur event handler if the mouse was in one of the inputs + e.stopPropagation(); + + }, + + calculateChosenLabel: function () { + var customRange = true; + var i = 0; + for (var range in this.ranges) { + if (this.timePicker) { + var format = this.timePickerSeconds ? "YYYY-MM-DD hh:mm:ss" : "YYYY-MM-DD hh:mm"; + //ignore times when comparing dates if time picker seconds is not enabled + if (this.startDate.format(format) == this.ranges[range][0].format(format) && this.endDate.format(format) == this.ranges[range][1].format(format)) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); + break; + } + } else { + //ignore times when comparing dates if time picker is not enabled + if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); + break; + } + } + i++; + } + if (customRange) { + if (this.showCustomRangeLabel) { + this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); + } else { + this.chosenLabel = null; + } + this.showCalendars(); + } + }, + + clickApply: function(e) { + this.hide(); + this.element.trigger('apply.daterangepicker', this); + }, + + clickCancel: function(e) { + this.startDate = this.oldStartDate; + this.endDate = this.oldEndDate; + this.hide(); + this.element.trigger('cancel.daterangepicker', this); + }, + + monthOrYearChanged: function(e) { + var isLeft = $(e.target).closest('.calendar').hasClass('left'), + leftOrRight = isLeft ? 'left' : 'right', + cal = this.container.find('.calendar.'+leftOrRight); + + // Month must be Number for new moment versions + var month = parseInt(cal.find('.monthselect').val(), 10); + var year = cal.find('.yearselect').val(); + + if (!isLeft) { + if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) { + month = this.startDate.month(); + year = this.startDate.year(); + } + } + + if (this.minDate) { + if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) { + month = this.minDate.month(); + year = this.minDate.year(); + } + } + + if (this.maxDate) { + if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) { + month = this.maxDate.month(); + year = this.maxDate.year(); + } + } + + if (isLeft) { + this.leftCalendar.month.month(month).year(year); + if (this.linkedCalendars) + this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month'); + } else { + this.rightCalendar.month.month(month).year(year); + if (this.linkedCalendars) + this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month'); + } + this.updateCalendars(); + }, + + timeChanged: function(e) { + + var cal = $(e.target).closest('.calendar'), + isLeft = cal.hasClass('left'); + + var hour = parseInt(cal.find('.hourselect').val(), 10); + var minute = parseInt(cal.find('.minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0; + + if (!this.timePicker24Hour) { + var ampm = cal.find('.ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + + if (isLeft) { + var start = this.startDate.clone(); + start.hour(hour); + start.minute(minute); + start.second(second); + this.setStartDate(start); + if (this.singleDatePicker) { + this.endDate = this.startDate.clone(); + } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) { + this.setEndDate(start.clone()); + } + } else if (this.endDate) { + var end = this.endDate.clone(); + end.hour(hour); + end.minute(minute); + end.second(second); + this.setEndDate(end); + } + + //update the calendars so all clickable dates reflect the new time component + this.updateCalendars(); + + //update the form inputs above the calendars with the new time + this.updateFormInputs(); + + //re-render the time pickers because changing one selection can affect what's enabled in another + this.renderTimePicker('left'); + this.renderTimePicker('right'); + + }, + + formInputsChanged: function(e) { + var isRight = $(e.target).closest('.calendar').hasClass('right'); + var start = moment(this.container.find('input[name="daterangepicker_start"]').val(), this.locale.format); + var end = moment(this.container.find('input[name="daterangepicker_end"]').val(), this.locale.format); + + if (start.isValid() && end.isValid()) { + + if (isRight && end.isBefore(start)) + start = end.clone(); + + this.setStartDate(start); + this.setEndDate(end); + + if (isRight) { + this.container.find('input[name="daterangepicker_start"]').val(this.startDate.format(this.locale.format)); + } else { + this.container.find('input[name="daterangepicker_end"]').val(this.endDate.format(this.locale.format)); + } + + } + + this.updateView(); + }, + + formInputsFocused: function(e) { + + // Highlight the focused input + this.container.find('input[name="daterangepicker_start"], input[name="daterangepicker_end"]').removeClass('active'); + $(e.target).addClass('active'); + + // Set the state such that if the user goes back to using a mouse, + // the calendars are aware we're selecting the end of the range, not + // the start. This allows someone to edit the end of a date range without + // re-selecting the beginning, by clicking on the end date input then + // using the calendar. + var isRight = $(e.target).closest('.calendar').hasClass('right'); + if (isRight) { + this.endDate = null; + this.setStartDate(this.startDate.clone()); + this.updateView(); + } + + }, + + formInputsBlurred: function(e) { + + // this function has one purpose right now: if you tab from the first + // text input to the second in the UI, the endDate is nulled so that + // you can click another, but if you tab out without clicking anything + // or changing the input value, the old endDate should be retained + + if (!this.endDate) { + var val = this.container.find('input[name="daterangepicker_end"]').val(); + var end = moment(val, this.locale.format); + if (end.isValid()) { + this.setEndDate(end); + this.updateView(); + } + } + + }, + + formInputsKeydown: function(e) { + // This function ensures that if the 'enter' key was pressed in the input, then the calendars + // are updated with the startDate and endDate. + // This behaviour is automatic in Chrome/Firefox/Edge but not in IE 11 hence why this exists. + // Other browsers and versions of IE are untested and the behaviour is unknown. + if (e.keyCode === 13) { + // Prevent the calendar from being updated twice on Chrome/Firefox/Edge + e.preventDefault(); + this.formInputsChanged(e); + } + }, + + + elementChanged: function() { + if (!this.element.is('input')) return; + if (!this.element.val().length) return; + + var dateString = this.element.val().split(this.locale.separator), + start = null, + end = null; + + if (dateString.length === 2) { + start = moment(dateString[0], this.locale.format); + end = moment(dateString[1], this.locale.format); + } + + if (this.singleDatePicker || start === null || end === null) { + start = moment(this.element.val(), this.locale.format); + end = start; + } + + if (!start.isValid() || !end.isValid()) return; + + this.setStartDate(start); + this.setEndDate(end); + this.updateView(); + }, + + keydown: function(e) { + //hide on tab or enter + if ((e.keyCode === 9) || (e.keyCode === 13)) { + this.hide(); + } + + //hide on esc and prevent propagation + if (e.keyCode === 27) { + e.preventDefault(); + e.stopPropagation(); + + this.hide(); + } + }, + + updateElement: function() { + if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); + this.element.trigger('change'); + } else if (this.element.is('input') && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format)); + this.element.trigger('change'); + } + }, + + remove: function() { + this.container.remove(); + this.element.off('.daterangepicker'); + this.element.removeData(); + } + + }; + + $.fn.daterangepicker = function(options, callback) { + var implementOptions = $.extend(true, {}, $.fn.daterangepicker.defaultOptions, options); + this.each(function() { + var el = $(this); + if (el.data('daterangepicker')) + el.data('daterangepicker').remove(); + el.data('daterangepicker', new DateRangePicker(el, implementOptions, callback)); + }); + return this; + }; + + return DateRangePicker; + +})); diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css new file mode 100644 index 0000000..5b96335 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map new file mode 100644 index 0000000..0ae3de5 --- /dev/null +++ b/test-server-cloud/test-visual/test-cloud-xxljob/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","dist/css/bootstrap.css","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA;;;;AAKA,4ECKA,KACE,YAAA,WACA,qBAAA,KACA,yBAAA,KAOF,KACE,OAAA,EAaF,QCnBA,MACA,QACA,WACA,OACA,OACA,OACA,OACA,KACA,KACA,IACA,QACA,QDqBE,QAAA,MAQF,MCzBA,OACA,SACA,MD2BE,QAAA,aACA,eAAA,SAQF,sBACE,QAAA,KACA,OAAA,EAQF,SCrCA,SDuCE,QAAA,KAUF,EACE,iBAAA,YAQF,SCnDA,QDqDE,QAAA,EAWF,YACE,cAAA,KACA,gBAAA,UACA,wBAAA,UAAA,OAAA,qBAAA,UAAA,OAAA,gBAAA,UAAA,OAOF,EC/DA,ODiEE,YAAA,IAOF,IACE,WAAA,OAQF,GACE,UAAA,IACA,OAAA,MAAA,EAOF,KACE,WAAA,KACA,MAAA,KAOF,MACE,UAAA,IAOF,ICzFA,ID2FE,UAAA,IACA,YAAA,EACA,SAAA,SACA,eAAA,SAGF,IACE,IAAA,MAGF,IACE,OAAA,OAUF,IACE,OAAA,EAOF,eACE,SAAA,OAUF,OACE,OAAA,IAAA,KAOF,GACE,mBAAA,YAAA,gBAAA,YAAA,WAAA,YACA,OAAA,EAOF,IACE,SAAA,KAOF,KC7HA,IACA,IACA,KD+HE,YAAA,SAAA,CAAA,UACA,UAAA,IAkBF,OC7IA,MACA,SACA,OACA,SD+IE,MAAA,QACA,KAAA,QACA,OAAA,EAOF,OACE,SAAA,QAUF,OC1JA,OD4JE,eAAA,KAWF,OCnKA,wBACA,kBACA,mBDqKE,mBAAA,OACA,OAAA,QAOF,iBCxKA,qBD0KE,OAAA,QAOF,yBC7KA,wBD+KE,OAAA,EACA,QAAA,EAQF,MACE,YAAA,OAWF,qBC5LA,kBD8LE,mBAAA,WAAA,gBAAA,WAAA,WAAA,WACA,QAAA,EASF,8CCjMA,8CDmME,OAAA,KAQF,mBACE,mBAAA,UACA,mBAAA,YAAA,gBAAA,YAAA,WAAA,YASF,iDC5MA,8CD8ME,mBAAA,KAOF,SACE,OAAA,IAAA,MAAA,OACA,OAAA,EAAA,IACA,QAAA,MAAA,OAAA,MAQF,OACE,OAAA,EACA,QAAA,EAOF,SACE,SAAA,KAQF,SACE,YAAA,IAUF,MACE,gBAAA,SACA,eAAA,EAGF,GC3OA,GD6OE,QAAA,EDlPF,qFGhLA,aACE,ED2LA,OADA,QCvLE,MAAA,eACA,YAAA,eACA,WAAA,cACA,mBAAA,eAAA,WAAA,eAGF,ED0LA,UCxLE,gBAAA,UAGF,cACE,QAAA,KAAA,WAAA,IAGF,kBACE,QAAA,KAAA,YAAA,IAKF,mBDqLA,6BCnLE,QAAA,GDuLF,WCpLA,IAEE,OAAA,IAAA,MAAA,KACA,kBAAA,MAGF,MACE,QAAA,mBDqLF,IClLA,GAEE,kBAAA,MAGF,IACE,UAAA,eDmLF,GACA,GCjLA,EAGE,QAAA,EACA,OAAA,EAGF,GD+KA,GC7KE,iBAAA,MAMF,QACE,QAAA,KAEF,YD2KA,oBCxKI,iBAAA,eAGJ,OACE,OAAA,IAAA,MAAA,KAGF,OACE,gBAAA,mBADF,UD2KA,UCtKI,iBAAA,eD0KJ,mBCvKA,mBAGI,OAAA,IAAA,MAAA,gBCrFN,WACE,YAAA,uBACA,IAAA,+CACA,IAAA,sDAAA,2BAAA,CAAA,iDAAA,eAAA,CAAA,gDAAA,cAAA,CAAA,+CAAA,kBAAA,CAAA,2EAAA,cAQF,WACE,SAAA,SACA,IAAA,IACA,QAAA,aACA,YAAA,uBACA,WAAA,OACA,YAAA,IACA,YAAA,EACA,uBAAA,YACA,wBAAA,UAIkC,2BAAW,QAAA,QACX,uBAAW,QAAA,QF2P/C,sBEzPoC,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,2BAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,6BAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,2BAAW,QAAA,QACX,qBAAW,QAAA,QACX,0BAAW,QAAA,QACX,qBAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,2BAAW,QAAA,QACX,sBAAW,QAAA,QACX,yBAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,+BAAW,QAAA,QACX,2BAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,8BAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,6BAAW,QAAA,QACX,6BAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,sBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,2BAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,yBAAW,QAAA,QACX,8BAAW,QAAA,QACX,6BAAW,QAAA,QACX,6BAAW,QAAA,QACX,+BAAW,QAAA,QACX,8BAAW,QAAA,QACX,gCAAW,QAAA,QACX,uBAAW,QAAA,QACX,8BAAW,QAAA,QACX,+BAAW,QAAA,QACX,iCAAW,QAAA,QACX,0BAAW,QAAA,QACX,6BAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,gCAAW,QAAA,QACX,gCAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,0BAAW,QAAA,QACX,+BAAW,QAAA,QACX,+BAAW,QAAA,QACX,wBAAW,QAAA,QACX,+BAAW,QAAA,QACX,gCAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,8BAAW,QAAA,QACX,0BAAW,QAAA,QACX,gCAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,gCAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,6BAAW,QAAA,QACX,8BAAW,QAAA,QACX,2BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,8BAAW,QAAA,QACX,+BAAW,QAAA,QACX,mCAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,2BAAW,QAAA,QACX,4BAAW,QAAA,QACX,+BAAW,QAAA,QACX,wBAAW,QAAA,QACX,2BAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,yBAAW,QAAA,QACX,6BAAW,QAAA,QACX,+BAAW,QAAA,QACX,0BAAW,QAAA,QACX,gCAAW,QAAA,QACX,+BAAW,QAAA,QACX,8BAAW,QAAA,QACX,kCAAW,QAAA,QACX,oCAAW,QAAA,QACX,sBAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,8BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,0BAAW,QAAA,QACX,4BAAW,QAAA,QACX,qCAAW,QAAA,QACX,oCAAW,QAAA,QACX,kCAAW,QAAA,QACX,oCAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,8BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,0BAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,uBAAW,QAAA,QACX,mCAAW,QAAA,QACX,uCAAW,QAAA,QACX,gCAAW,QAAA,QACX,oCAAW,QAAA,QACX,qCAAW,QAAA,QACX,yCAAW,QAAA,QACX,4BAAW,QAAA,QACX,yBAAW,QAAA,QACX,gCAAW,QAAA,QACX,8BAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,0BAAW,QAAA,QACX,6BAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,8BAAW,QAAA,QACX,+BAAW,QAAA,QACX,gCAAW,QAAA,QACX,8BAAW,QAAA,QACX,8BAAW,QAAA,QACX,8BAAW,QAAA,QACX,2BAAW,QAAA,QACX,0BAAW,QAAA,QACX,yBAAW,QAAA,QACX,6BAAW,QAAA,QACX,2BAAW,QAAA,QACX,4BAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,2BAAW,QAAA,QACX,2BAAW,QAAA,QACX,4BAAW,QAAA,QACX,+BAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,iCAAW,QAAA,QACX,oCAAW,QAAA,QACX,iCAAW,QAAA,QACX,+BAAW,QAAA,QACX,+BAAW,QAAA,QACX,iCAAW,QAAA,QACX,qBAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QASX,wBAAW,QAAA,QACX,4BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,yBAAW,QAAA,QACX,yBAAW,QAAA,QACX,+BAAW,QAAA,QACX,uBAAW,QAAA,QACX,6BAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,4BAAW,QAAA,QACX,uBAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,2BAAW,QAAA,QACX,0BAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,4BAAW,QAAA,QACX,mCAAW,QAAA,QACX,4BAAW,QAAA,QACX,oCAAW,QAAA,QACX,kCAAW,QAAA,QACX,iCAAW,QAAA,QACX,+BAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,kCAAW,QAAA,QACX,mCAAW,QAAA,QACX,sCAAW,QAAA,QACX,0CAAW,QAAA,QACX,oCAAW,QAAA,QACX,wCAAW,QAAA,QACX,qCAAW,QAAA,QACX,iCAAW,QAAA,QACX,gCAAW,QAAA,QACX,kCAAW,QAAA,QACX,+BAAW,QAAA,QACX,0BAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,0BAAW,QAAA,QCxS/C,ECkEE,mBAAA,WACG,gBAAA,WACK,WAAA,WJo+BV,OGriCA,QC+DE,mBAAA,WACG,gBAAA,WACK,WAAA,WDzDV,KACE,UAAA,KACA,4BAAA,cAGF,KACE,YAAA,gBAAA,CAAA,SAAA,CAAA,KAAA,CAAA,WACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,iBAAA,KHoiCF,OGhiCA,MHiiCA,OACA,SG9hCE,YAAA,QACA,UAAA,QACA,YAAA,QAMF,EACE,MAAA,QACA,gBAAA,KH8hCF,QG5hCE,QAEE,MAAA,QACA,gBAAA,UAGF,QEnDA,QAAA,IAAA,KAAA,yBACA,eAAA,KF6DF,OACE,OAAA,EAMF,IACE,eAAA,OHqhCF,4BADA,0BGhhCA,gBH+gCA,iBADA,eMxlCE,QAAA,MACA,UAAA,KACA,OAAA,KH6EF,aACE,cAAA,IAMF,eACE,QAAA,IACA,YAAA,WACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,IC+FA,mBAAA,IAAA,IAAA,YACK,cAAA,IAAA,IAAA,YACG,WAAA,IAAA,IAAA,YE5LR,QAAA,aACA,UAAA,KACA,OAAA,KHiGF,YACE,cAAA,IAMF,GACE,WAAA,KACA,cAAA,KACA,OAAA,EACA,WAAA,IAAA,MAAA,KAQF,SACE,SAAA,SACA,MAAA,IACA,OAAA,IACA,QAAA,EACA,OAAA,KACA,SAAA,OACA,KAAA,cACA,OAAA,EAQA,0BH8/BF,yBG5/BI,SAAA,OACA,MAAA,KACA,OAAA,KACA,OAAA,EACA,SAAA,QACA,KAAA,KAWJ,cACE,OAAA,QH4/BF,IACA,IACA,IACA,IACA,IACA,IOtpCA,GP4oCA,GACA,GACA,GACA,GACA,GO9oCE,YAAA,QACA,YAAA,IACA,YAAA,IACA,MAAA,QPyqCF,WAZA,UAaA,WAZA,UAaA,WAZA,UAaA,WAZA,UAaA,WAZA,UAaA,WAZA,UACA,UOxqCA,SPyqCA,UAZA,SAaA,UAZA,SAaA,UAZA,SAaA,UAZA,SAaA,UAZA,SOxpCI,YAAA,IACA,YAAA,EACA,MAAA,KP8qCJ,IAEA,IAEA,IO9qCA,GP2qCA,GAEA,GO1qCE,WAAA,KACA,cAAA,KPqrCF,WANA,UAQA,WANA,UAQA,WANA,UACA,UOxrCA,SP0rCA,UANA,SAQA,UANA,SO9qCI,UAAA,IPyrCJ,IAEA,IAEA,IO1rCA,GPurCA,GAEA,GOtrCE,WAAA,KACA,cAAA,KPisCF,WANA,UAQA,WANA,UAQA,WANA,UACA,UOpsCA,SPssCA,UANA,SAQA,UANA,SO1rCI,UAAA,IPqsCJ,IOjsCA,GAAU,UAAA,KPqsCV,IOpsCA,GAAU,UAAA,KPwsCV,IOvsCA,GAAU,UAAA,KP2sCV,IO1sCA,GAAU,UAAA,KP8sCV,IO7sCA,GAAU,UAAA,KPitCV,IOhtCA,GAAU,UAAA,KAMV,EACE,OAAA,EAAA,EAAA,KAGF,MACE,cAAA,KACA,UAAA,KACA,YAAA,IACA,YAAA,IAEA,yBAAA,MACE,UAAA,MPitCJ,OOxsCA,MAEE,UAAA,IP0sCF,MOvsCA,KAEE,QAAA,KACA,iBAAA,QAIF,WAAuB,WAAA,KACvB,YAAuB,WAAA,MACvB,aAAuB,WAAA,OACvB,cAAuB,WAAA,QACvB,aAAuB,YAAA,OAGvB,gBAAuB,eAAA,UACvB,gBAAuB,eAAA,UACvB,iBAAuB,eAAA,WAGvB,YACE,MAAA,KAEF,cCvGE,MAAA,QR2zCF,qBQ1zCE,qBAEE,MAAA,QDuGJ,cC1GE,MAAA,QRk0CF,qBQj0CE,qBAEE,MAAA,QD0GJ,WC7GE,MAAA,QRy0CF,kBQx0CE,kBAEE,MAAA,QD6GJ,cChHE,MAAA,QRg1CF,qBQ/0CE,qBAEE,MAAA,QDgHJ,aCnHE,MAAA,QRu1CF,oBQt1CE,oBAEE,MAAA,QDuHJ,YAGE,MAAA,KE7HA,iBAAA,QT+1CF,mBS91CE,mBAEE,iBAAA,QF6HJ,YEhIE,iBAAA,QTs2CF,mBSr2CE,mBAEE,iBAAA,QFgIJ,SEnIE,iBAAA,QT62CF,gBS52CE,gBAEE,iBAAA,QFmIJ,YEtIE,iBAAA,QTo3CF,mBSn3CE,mBAEE,iBAAA,QFsIJ,WEzIE,iBAAA,QT23CF,kBS13CE,kBAEE,iBAAA,QF8IJ,aACE,eAAA,IACA,OAAA,KAAA,EAAA,KACA,cAAA,IAAA,MAAA,KPgvCF,GOxuCA,GAEE,WAAA,EACA,cAAA,KP4uCF,MAFA,MACA,MO9uCA,MAMI,cAAA,EAOJ,eACE,aAAA,EACA,WAAA,KAIF,aALE,aAAA,EACA,WAAA,KAMA,YAAA,KAFF,gBAKI,QAAA,aACA,cAAA,IACA,aAAA,IAKJ,GACE,WAAA,EACA,cAAA,KPouCF,GOluCA,GAEE,YAAA,WAEF,GACE,YAAA,IAEF,GACE,YAAA,EAaA,yBAAA,kBAEI,MAAA,KACA,MAAA,MACA,MAAA,KACA,WAAA,MGxNJ,SAAA,OACA,cAAA,SACA,YAAA,OHiNA,kBASI,YAAA,OP4tCN,0BOjtCA,YAEE,OAAA,KAGF,YACE,UAAA,IA9IqB,eAAA,UAmJvB,WACE,QAAA,KAAA,KACA,OAAA,EAAA,EAAA,KACA,UAAA,OACA,YAAA,IAAA,MAAA,KPitCF,yBO5sCI,wBP2sCJ,yBO1sCM,cAAA,EPgtCN,kBO1tCA,kBPytCA,iBOtsCI,QAAA,MACA,UAAA,IACA,YAAA,WACA,MAAA,KP4sCJ,yBO1sCI,yBPysCJ,wBOxsCM,QAAA,cAQN,oBPqsCA,sBOnsCE,cAAA,KACA,aAAA,EACA,WAAA,MACA,aAAA,IAAA,MAAA,KACA,YAAA,EP0sCF,kCOpsCI,kCPksCJ,iCAGA,oCAJA,oCAEA,mCOnsCe,QAAA,GP4sCf,iCO3sCI,iCPysCJ,gCAGA,mCAJA,mCAEA,kCOzsCM,QAAA,cAMN,QACE,cAAA,KACA,WAAA,OACA,YAAA,WIxSF,KXm/CA,IACA,IACA,KWj/CE,YAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,aAAA,CAAA,UAIF,KACE,QAAA,IAAA,IACA,UAAA,IACA,MAAA,QACA,iBAAA,QACA,cAAA,IAIF,IACE,QAAA,IAAA,IACA,UAAA,IACA,MAAA,KACA,iBAAA,KACA,cAAA,IACA,mBAAA,MAAA,EAAA,KAAA,EAAA,gBAAA,WAAA,MAAA,EAAA,KAAA,EAAA,gBANF,QASI,QAAA,EACA,UAAA,KACA,YAAA,IACA,mBAAA,KAAA,WAAA,KAKJ,IACE,QAAA,MACA,QAAA,MACA,OAAA,EAAA,EAAA,KACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,WAAA,UACA,UAAA,WACA,iBAAA,QACA,OAAA,IAAA,MAAA,KACA,cAAA,IAXF,SAeI,QAAA,EACA,UAAA,QACA,MAAA,QACA,YAAA,SACA,iBAAA,YACA,cAAA,EAKJ,gBACE,WAAA,MACA,WAAA,OC1DF,WCHE,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KDGA,yBAAA,WACE,MAAA,OAEF,yBAAA,WACE,MAAA,OAEF,0BAAA,WACE,MAAA,QAUJ,iBCvBE,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KD6BF,KCvBE,aAAA,MACA,YAAA,MD0BF,gBACE,aAAA,EACA,YAAA,EAFF,8BAKI,cAAA,EACA,aAAA,EZwiDJ,UAoCA,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAjCA,UAoCA,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAjCA,UAoCA,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UatnDC,UbynDD,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UcpmDM,SAAA,SAEA,WAAA,IAEA,cAAA,KACA,aAAA,KDtBL,UbmpDD,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,Uc3mDM,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,EFCJ,yBCzEC,Ub2zDC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UcnxDI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GFUJ,yBClFC,Ubo+DC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,Uc57DI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GFmBJ,0BC3FC,Ub6oEC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UcrmEI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GCjEJ,MACE,iBAAA,YADF,uBAQI,SAAA,OACA,QAAA,aACA,MAAA,KAKA,sBf+xEJ,sBe9xEM,SAAA,OACA,QAAA,WACA,MAAA,KAKN,QACE,YAAA,IACA,eAAA,IACA,MAAA,KACA,WAAA,KAGF,GACE,WAAA,KAMF,OACE,MAAA,KACA,UAAA,KACA,cAAA,Kf6xEF,mBAHA,mBAIA,mBAHA,mBACA,mBe/xEA,mBAWQ,QAAA,IACA,YAAA,WACA,eAAA,IACA,WAAA,IAAA,MAAA,KAdR,mBAoBI,eAAA,OACA,cAAA,IAAA,MAAA,KfyxEJ,uCe9yEA,uCf+yEA,wCAHA,wCAIA,2CAHA,2Ce/wEQ,WAAA,EA9BR,mBAoCI,WAAA,IAAA,MAAA,KApCJ,cAyCI,iBAAA,KfoxEJ,6BAHA,6BAIA,6BAHA,6BACA,6Be5wEA,6BAOQ,QAAA,IAWR,gBACE,OAAA,IAAA,MAAA,KfqwEF,4BAHA,4BAIA,4BAHA,4BACA,4BerwEA,4BAQQ,OAAA,IAAA,MAAA,KfmwER,4Be3wEA,4BAeM,oBAAA,IAUN,yCAEI,iBAAA,QASJ,4BAEI,iBAAA,QfqvEJ,0BAGA,0BATA,0BAGA,0BAIA,0BAGA,0BATA,0BAGA,0BACA,0BAGA,0BgBt4EE,0BhBg4EF,0BgBz3EM,iBAAA,QhBs4EN,sCAEA,sCADA,oCgBj4EE,sChB+3EF,sCgBz3EM,iBAAA,QhBs4EN,2BAGA,2BATA,2BAGA,2BAIA,2BAGA,2BATA,2BAGA,2BACA,2BAGA,2BgB35EE,2BhBq5EF,2BgB94EM,iBAAA,QhB25EN,uCAEA,uCADA,qCgBt5EE,uChBo5EF,uCgB94EM,iBAAA,QhB25EN,wBAGA,wBATA,wBAGA,wBAIA,wBAGA,wBATA,wBAGA,wBACA,wBAGA,wBgBh7EE,wBhB06EF,wBgBn6EM,iBAAA,QhBg7EN,oCAEA,oCADA,kCgB36EE,oChBy6EF,oCgBn6EM,iBAAA,QhBg7EN,2BAGA,2BATA,2BAGA,2BAIA,2BAGA,2BATA,2BAGA,2BACA,2BAGA,2BgBr8EE,2BhB+7EF,2BgBx7EM,iBAAA,QhBq8EN,uCAEA,uCADA,qCgBh8EE,uChB87EF,uCgBx7EM,iBAAA,QhBq8EN,0BAGA,0BATA,0BAGA,0BAIA,0BAGA,0BATA,0BAGA,0BACA,0BAGA,0BgB19EE,0BhBo9EF,0BgB78EM,iBAAA,QhB09EN,sCAEA,sCADA,oCgBr9EE,sChBm9EF,sCgB78EM,iBAAA,QDoJN,kBACE,WAAA,KACA,WAAA,KAEA,oCAAA,kBACE,MAAA,KACA,cAAA,KACA,WAAA,OACA,mBAAA,yBACA,OAAA,IAAA,MAAA,KALF,yBASI,cAAA,Efq0EJ,qCAHA,qCAIA,qCAHA,qCACA,qCe70EA,qCAkBU,YAAA,OAlBV,kCA0BI,OAAA,Ef+zEJ,0DAHA,0DAIA,0DAHA,0DACA,0Dex1EA,0DAmCU,YAAA,Ef8zEV,yDAHA,yDAIA,yDAHA,yDACA,yDeh2EA,yDAuCU,aAAA,Efg0EV,yDev2EA,yDfw2EA,yDAFA,yDelzEU,cAAA,GEzNZ,SAIE,UAAA,EACA,QAAA,EACA,OAAA,EACA,OAAA,EAGF,OACE,QAAA,MACA,MAAA,KACA,QAAA,EACA,cAAA,KACA,UAAA,KACA,YAAA,QACA,MAAA,KACA,OAAA,EACA,cAAA,IAAA,MAAA,QAGF,MACE,QAAA,aACA,UAAA,KACA,cAAA,IACA,YAAA,IAUF,mBb6BE,mBAAA,WACG,gBAAA,WACK,WAAA,WarBR,mBAAA,KACA,gBAAA,KAAA,WAAA,KjBkgFF,qBiB9/EA,kBAEE,OAAA,IAAA,EAAA,EACA,WAAA,MACA,YAAA,OjBogFF,wCADA,qCADA,8BAFA,+BACA,2BiB3/EE,4BAGE,OAAA,YAIJ,iBACE,QAAA,MAIF,kBACE,QAAA,MACA,MAAA,KAIF,iBjBu/EA,aiBr/EE,OAAA,KjB0/EF,2BiBt/EA,uBjBq/EA,wBK/kFE,QAAA,IAAA,KAAA,yBACA,eAAA,KYgGF,OACE,QAAA,MACA,YAAA,IACA,UAAA,KACA,YAAA,WACA,MAAA,KA0BF,cACE,QAAA,MACA,MAAA,KACA,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,iBAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,Ib3EA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBAyHR,mBAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,KACK,cAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,KACG,mBAAA,aAAA,YAAA,IAAA,CAAA,mBAAA,YAAA,KAAA,WAAA,aAAA,YAAA,IAAA,CAAA,mBAAA,YAAA,KAAA,WAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,KAAA,WAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,IAAA,CAAA,mBAAA,YAAA,Kc1IR,oBACE,aAAA,QACA,QAAA,EdYF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,qBAiCR,gCACE,MAAA,KACA,QAAA,EAEF,oCAA0B,MAAA,KAC1B,yCAAgC,MAAA,Ka+ChC,0BACE,iBAAA,YACA,OAAA,EAQF,wBjBq+EF,wBACA,iCiBn+EI,iBAAA,KACA,QAAA,EAGF,wBjBo+EF,iCiBl+EI,OAAA,YAIF,sBACE,OAAA,KAcJ,qDAKI,8BjBm9EF,wCACA,+BAFA,8BiBj9EI,YAAA,KjB09EJ,iCAEA,2CACA,kCAFA,iCiBx9EE,0BjBq9EF,oCACA,2BAFA,0BiBl9EI,YAAA,KjB+9EJ,iCAEA,2CACA,kCAFA,iCiB79EE,0BjB09EF,oCACA,2BAFA,0BiBv9EI,YAAA,MAWN,YACE,cAAA,KjBy9EF,UiBj9EA,OAEE,SAAA,SACA,QAAA,MACA,WAAA,KACA,cAAA,KjBm9EF,yBiBh9EE,sBjBk9EF,mCADA,gCiB98EM,OAAA,YjBm9EN,gBiB99EA,aAgBI,WAAA,KACA,aAAA,KACA,cAAA,EACA,YAAA,IACA,OAAA,QjBm9EJ,+BACA,sCiBj9EA,yBjB+8EA,gCiB38EE,SAAA,SACA,WAAA,MACA,YAAA,MjBi9EF,oBiB98EA,cAEE,WAAA,KjBg9EF,iBiB58EA,cAEE,SAAA,SACA,QAAA,aACA,aAAA,KACA,cAAA,EACA,YAAA,IACA,eAAA,OACA,OAAA,QjB88EF,0BiB38EE,uBjB68EF,oCADA,iCiB18EI,OAAA,YjB+8EJ,kCiB58EA,4BAEE,WAAA,EACA,YAAA,KASF,qBACE,WAAA,KAEA,YAAA,IACA,eAAA,IAEA,cAAA,EAEA,8BjBm8EF,8BiBj8EI,cAAA,EACA,aAAA,EAaJ,UC3PE,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IAEA,gBACE,OAAA,KACA,YAAA,KlBsrFJ,0BkBnrFE,kBAEE,OAAA,KDiPJ,6BAEI,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IANJ,mCASI,OAAA,KACA,YAAA,KjBq8EJ,6CiB/8EA,qCAcI,OAAA,KAdJ,oCAiBI,OAAA,KACA,WAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IAIJ,UCvRE,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IAEA,gBACE,OAAA,KACA,YAAA,KlB2tFJ,0BkBxtFE,kBAEE,OAAA,KD6QJ,6BAEI,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IANJ,mCASI,OAAA,KACA,YAAA,KjB88EJ,6CiBx9EA,qCAcI,OAAA,KAdJ,oCAiBI,OAAA,KACA,WAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UASJ,cAEE,SAAA,SAFF,4BAMI,cAAA,OAIJ,uBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,QAAA,EACA,QAAA,MACA,MAAA,KACA,OAAA,KACA,YAAA,KACA,WAAA,OACA,eAAA,KjBo8EF,oDADA,uCiBj8EA,iCAGE,MAAA,KACA,OAAA,KACA,YAAA,KjBo8EF,oDADA,uCiBj8EA,iCAGE,MAAA,KACA,OAAA,KACA,YAAA,KjBq8EF,uBAEA,8BAJA,4BiB/7EA,yBjBg8EA,oBAEA,2BAGA,4BAEA,mCAHA,yBAEA,gCkBx1FI,MAAA,QDkZJ,2BC9YI,aAAA,QdiDF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBchDN,iCACE,aAAA,Qd8CJ,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,Qa4VV,gCCpYI,MAAA,QACA,iBAAA,QACA,aAAA,QDkYJ,oCC9XI,MAAA,QlB61FJ,uBAEA,8BAJA,4BiB19EA,yBjB29EA,oBAEA,2BAGA,4BAEA,mCAHA,yBAEA,gCkBt3FI,MAAA,QDqZJ,2BCjZI,aAAA,QdiDF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBchDN,iCACE,aAAA,Qd8CJ,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,Qa+VV,gCCvYI,MAAA,QACA,iBAAA,QACA,aAAA,QDqYJ,oCCjYI,MAAA,QlB23FJ,qBAEA,4BAJA,0BiBr/EA,uBjBs/EA,kBAEA,yBAGA,0BAEA,iCAHA,uBAEA,8BkBp5FI,MAAA,QDwZJ,yBCpZI,aAAA,QdiDF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBchDN,+BACE,aAAA,Qd8CJ,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QakWV,8BC1YI,MAAA,QACA,iBAAA,QACA,aAAA,QDwYJ,kCCpYI,MAAA,QD2YF,2CACE,IAAA,KAEF,mDACE,IAAA,EAUJ,YACE,QAAA,MACA,WAAA,IACA,cAAA,KACA,MAAA,QAkBA,yBAAA,yBAGI,QAAA,aACA,cAAA,EACA,eAAA,OALJ,2BAUI,QAAA,aACA,MAAA,KACA,eAAA,OAZJ,kCAiBI,QAAA,aAjBJ,0BAqBI,QAAA,aACA,eAAA,OjBi/EJ,wCiBvgFA,6CjBsgFA,2CiB3+EM,MAAA,KA3BN,wCAiCI,MAAA,KAjCJ,4BAqCI,cAAA,EACA,eAAA,OjB4+EJ,uBiBlhFA,oBA6CI,QAAA,aACA,WAAA,EACA,cAAA,EACA,eAAA,OjBy+EJ,6BiBzhFA,0BAmDM,aAAA,EjB0+EN,4CiB7hFA,sCAwDI,SAAA,SACA,YAAA,EAzDJ,kDA8DI,IAAA,GjBw+EN,2BAEA,kCiB/9EA,wBjB89EA,+BiBr9EI,YAAA,IACA,WAAA,EACA,cAAA,EjB09EJ,2BiBr+EA,wBAiBI,WAAA,KAjBJ,6BJ9gBE,aAAA,MACA,YAAA,MIwiBA,yBAAA,gCAEI,YAAA,IACA,cAAA,EACA,WAAA,OA/BN,sDAwCI,MAAA,KAQA,yBAAA,+CAEI,YAAA,KACA,UAAA,MAKJ,yBAAA,+CAEI,YAAA,IACA,UAAA,ME9kBR,KACE,QAAA,aACA,cAAA,EACA,YAAA,IACA,WAAA,OACA,YAAA,OACA,eAAA,OACA,iBAAA,aAAA,aAAA,aACA,OAAA,QACA,iBAAA,KACA,OAAA,IAAA,MAAA,YCoCA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,WACA,cAAA,IhBqKA,oBAAA,KACG,iBAAA,KACC,gBAAA,KACI,YAAA,KJs1FV,kBAHA,kBACA,WACA,kBAHA,kBmB1hGI,WdrBF,QAAA,IAAA,KAAA,yBACA,eAAA,KLwjGF,WADA,WmB7hGE,WAGE,MAAA,KACA,gBAAA,KnB+hGJ,YmB5hGE,YAEE,iBAAA,KACA,QAAA,Ef2BF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBexBR,cnB4hGF,eACA,wBmB1hGI,OAAA,YE9CF,OAAA,kBACA,QAAA,IjBiEA,mBAAA,KACQ,WAAA,KefN,enB4hGJ,yBmB1hGM,eAAA,KASN,aC7DE,MAAA,KACA,iBAAA,KACA,aAAA,KpBqlGF,mBoBnlGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpBqlGJ,oBoBnlGE,oBpBolGF,mCoBjlGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpB2lGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoBrlGI,0BpB0lGJ,yCAHA,yCAHA,yCoBjlGM,MAAA,KACA,iBAAA,QACA,aAAA,QpBgmGN,4BAHA,4BoBvlGI,4BpB2lGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoBnlGM,iBAAA,KACA,aAAA,KDuBN,oBClBI,MAAA,KACA,iBAAA,KDoBJ,aChEE,MAAA,KACA,iBAAA,QACA,aAAA,QpB0oGF,mBoBxoGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpB0oGJ,oBoBxoGE,oBpByoGF,mCoBtoGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpBgpGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoB1oGI,0BpB+oGJ,yCAHA,yCAHA,yCoBtoGM,MAAA,KACA,iBAAA,QACA,aAAA,QpBqpGN,4BAHA,4BoB5oGI,4BpBgpGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoBxoGM,iBAAA,QACA,aAAA,QD0BN,oBCrBI,MAAA,QACA,iBAAA,KDwBJ,aCpEE,MAAA,KACA,iBAAA,QACA,aAAA,QpB+rGF,mBoB7rGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpB+rGJ,oBoB7rGE,oBpB8rGF,mCoB3rGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpBqsGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoB/rGI,0BpBosGJ,yCAHA,yCAHA,yCoB3rGM,MAAA,KACA,iBAAA,QACA,aAAA,QpB0sGN,4BAHA,4BoBjsGI,4BpBqsGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoB7rGM,iBAAA,QACA,aAAA,QD8BN,oBCzBI,MAAA,QACA,iBAAA,KD4BJ,UCxEE,MAAA,KACA,iBAAA,QACA,aAAA,QpBovGF,gBoBlvGE,gBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,gBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpBovGJ,iBoBlvGE,iBpBmvGF,gCoBhvGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpB0vGJ,uBAHA,uBAHA,uBAKA,uBAHA,uBoBpvGI,uBpByvGJ,sCAHA,sCAHA,sCoBhvGM,MAAA,KACA,iBAAA,QACA,aAAA,QpB+vGN,yBAHA,yBoBtvGI,yBpB0vGJ,0BAHA,0BAHA,0BAOA,mCAHA,mCAHA,mCoBlvGM,iBAAA,QACA,aAAA,QDkCN,iBC7BI,MAAA,QACA,iBAAA,KDgCJ,aC5EE,MAAA,KACA,iBAAA,QACA,aAAA,QpByyGF,mBoBvyGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpByyGJ,oBoBvyGE,oBpBwyGF,mCoBryGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpB+yGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoBzyGI,0BpB8yGJ,yCAHA,yCAHA,yCoBryGM,MAAA,KACA,iBAAA,QACA,aAAA,QpBozGN,4BAHA,4BoB3yGI,4BpB+yGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoBvyGM,iBAAA,QACA,aAAA,QDsCN,oBCjCI,MAAA,QACA,iBAAA,KDoCJ,YChFE,MAAA,KACA,iBAAA,QACA,aAAA,QpB81GF,kBoB51GE,kBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,kBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpB81GJ,mBoB51GE,mBpB61GF,kCoB11GI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpBo2GJ,yBAHA,yBAHA,yBAKA,yBAHA,yBoB91GI,yBpBm2GJ,wCAHA,wCAHA,wCoB11GM,MAAA,KACA,iBAAA,QACA,aAAA,QpBy2GN,2BAHA,2BoBh2GI,2BpBo2GJ,4BAHA,4BAHA,4BAOA,qCAHA,qCAHA,qCoB51GM,iBAAA,QACA,aAAA,QD0CN,mBCrCI,MAAA,QACA,iBAAA,KD6CJ,UACE,YAAA,IACA,MAAA,QACA,cAAA,EAEA,UnBwzGF,iBADA,iBAEA,oBACA,6BmBrzGI,iBAAA,YfnCF,mBAAA,KACQ,WAAA,KeqCR,UnB0zGF,iBADA,gBADA,gBmBpzGI,aAAA,YnB0zGJ,gBmBxzGE,gBAEE,MAAA,QACA,gBAAA,UACA,iBAAA,YnB2zGJ,0BmBvzGI,0BnBwzGJ,mCAFA,mCmBpzGM,MAAA,KACA,gBAAA,KnB0zGN,mBmBjzGA,QC9EE,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IpBm4GF,mBmBpzGA,QClFE,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IpB04GF,mBmBvzGA,QCtFE,QAAA,IAAA,IACA,UAAA,KACA,YAAA,IACA,cAAA,ID2FF,WACE,QAAA,MACA,MAAA,KAIF,sBACE,WAAA,InBuzGF,6BADA,4BmB/yGE,6BACE,MAAA,KG1JJ,MACE,QAAA,ElBoLA,mBAAA,QAAA,KAAA,OACK,cAAA,QAAA,KAAA,OACG,WAAA,QAAA,KAAA,OkBnLR,SACE,QAAA,EAIJ,UACE,QAAA,KAEA,aAAY,QAAA,MACZ,eAAY,QAAA,UACZ,kBAAY,QAAA,gBAGd,YACE,SAAA,SACA,OAAA,EACA,SAAA,OlBsKA,4BAAA,MAAA,CAAA,WACQ,uBAAA,MAAA,CAAA,WAAA,oBAAA,MAAA,CAAA,WAOR,4BAAA,KACQ,uBAAA,KAAA,oBAAA,KAGR,mCAAA,KACQ,8BAAA,KAAA,2BAAA,KmB5MV,OACE,QAAA,aACA,MAAA,EACA,OAAA,EACA,YAAA,IACA,eAAA,OACA,WAAA,IAAA,OACA,WAAA,IAAA,QACA,aAAA,IAAA,MAAA,YACA,YAAA,IAAA,MAAA,YvBu/GF,UuBn/GA,QAEE,SAAA,SAIF,uBACE,QAAA,EAIF,eACE,SAAA,SACA,IAAA,KACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,MAAA,KACA,UAAA,MACA,QAAA,IAAA,EACA,OAAA,IAAA,EAAA,EACA,UAAA,KACA,WAAA,KACA,WAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,KACA,OAAA,IAAA,MAAA,gBACA,cAAA,InBuBA,mBAAA,EAAA,IAAA,KAAA,iBACQ,WAAA,EAAA,IAAA,KAAA,iBmBlBR,0BACE,MAAA,EACA,KAAA,KAzBJ,wBCzBE,OAAA,IACA,OAAA,IAAA,EACA,SAAA,OACA,iBAAA,QDsBF,oBAmCI,QAAA,MACA,QAAA,IAAA,KACA,MAAA,KACA,YAAA,IACA,YAAA,WACA,MAAA,KACA,YAAA,OvB8+GJ,0BuB5+GI,0BAEE,MAAA,QACA,gBAAA,KACA,iBAAA,QAOJ,yBvBw+GF,+BADA,+BuBp+GI,MAAA,KACA,gBAAA,KACA,iBAAA,QACA,QAAA,EASF,2BvBi+GF,iCADA,iCuB79GI,MAAA,KvBk+GJ,iCuB99GE,iCAEE,gBAAA,KACA,OAAA,YACA,iBAAA,YACA,iBAAA,KEzGF,OAAA,0DF+GF,qBAGI,QAAA,MAHJ,QAQI,QAAA,EAQJ,qBACE,MAAA,EACA,KAAA,KAQF,oBACE,MAAA,KACA,KAAA,EAIF,iBACE,QAAA,MACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,YAAA,OAIF,mBACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,IAIF,2BACE,MAAA,EACA,KAAA,KAQF,evB+7GA,sCuB37GI,QAAA,GACA,WAAA,EACA,cAAA,IAAA,OACA,cAAA,IAAA,QAPJ,uBvBs8GA,8CuB37GI,IAAA,KACA,OAAA,KACA,cAAA,IASJ,yBACE,6BApEA,MAAA,EACA,KAAA,KAmEA,kCA1DA,MAAA,KACA,KAAA,GG1IF,W1BkoHA,oB0BhoHE,SAAA,SACA,QAAA,aACA,eAAA,O1BooHF,yB0BxoHA,gBAMI,SAAA,SACA,MAAA,K1B4oHJ,gCAFA,gCAFA,+BAFA,+BAKA,uBAFA,uBAFA,sB0BroHI,sBAIE,QAAA,EAMN,qB1BooHA,2BACA,2BACA,iC0BjoHI,YAAA,KAKJ,aACE,YAAA,KADF,kB1BmoHA,wBACA,0B0B7nHI,MAAA,KAPJ,kB1BwoHA,wBACA,0B0B7nHI,YAAA,IAIJ,yEACE,cAAA,EAIF,4BACE,YAAA,EACA,mECpDA,wBAAA,EACA,2BAAA,EDwDF,6C1B2nHA,8C2B5qHE,uBAAA,EACA,0BAAA,EDsDF,sBACE,MAAA,KAEF,8DACE,cAAA,EAEF,mE1B0nHA,oE2B/rHE,wBAAA,EACA,2BAAA,ED0EF,oECnEE,uBAAA,EACA,0BAAA,EDuEF,mC1BwnHA,iC0BtnHE,QAAA,EAiBF,iCACE,cAAA,IACA,aAAA,IAEF,oCACE,cAAA,KACA,aAAA,KAKF,iCtB/CE,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBsBkDR,0CtBnDA,mBAAA,KACQ,WAAA,KsByDV,YACE,YAAA,EAGF,eACE,aAAA,IAAA,IAAA,EACA,oBAAA,EAGF,uBACE,aAAA,EAAA,IAAA,IAOF,yB1B4lHA,+BACA,oC0BzlHI,QAAA,MACA,MAAA,KACA,MAAA,KACA,UAAA,KAPJ,oCAcM,MAAA,KAdN,8B1BumHA,oCACA,oCACA,0C0BnlHI,WAAA,KACA,YAAA,EAKF,4DACE,cAAA,EAEF,sDC7KA,uBAAA,IACA,wBAAA,IAOA,2BAAA,EACA,0BAAA,EDwKA,sDCjLA,uBAAA,EACA,wBAAA,EAOA,2BAAA,IACA,0BAAA,ID6KF,uEACE,cAAA,EAEF,4E1BqlHA,6E2BtwHE,2BAAA,EACA,0BAAA,EDsLF,6EC/LE,uBAAA,EACA,wBAAA,EDsMF,qBACE,QAAA,MACA,MAAA,KACA,aAAA,MACA,gBAAA,SAJF,0B1BslHA,gC0B/kHI,QAAA,WACA,MAAA,KACA,MAAA,GATJ,qCAYI,MAAA,KAZJ,+CAgBI,KAAA,K1BmlHJ,gD0BlkHA,6C1BmkHA,2DAFA,wD0B5jHM,SAAA,SACA,KAAA,cACA,eAAA,KE1ON,aACE,SAAA,SACA,QAAA,MACA,gBAAA,SAGA,0BACE,MAAA,KACA,cAAA,EACA,aAAA,EATJ,2BAeI,SAAA,SACA,QAAA,EAKA,MAAA,KAEA,MAAA,KACA,cAAA,EAEA,iCACE,QAAA,EAUN,8B5B2xHA,mCACA,sCkBpwHE,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IAEA,oClBswHF,yCACA,4CkBtwHI,OAAA,KACA,YAAA,KlB4wHJ,8CACA,mDACA,sDkB3wHE,sClBuwHF,2CACA,8CkBtwHI,OAAA,KUhCJ,8B5B6yHA,mCACA,sCkB3xHE,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IAEA,oClB6xHF,yCACA,4CkB7xHI,OAAA,KACA,YAAA,KlBmyHJ,8CACA,mDACA,sDkBlyHE,sClB8xHF,2CACA,8CkB7xHI,OAAA,KlBqyHJ,2B4B5zHA,mB5B2zHA,iB4BxzHE,QAAA,W5B8zHF,8D4B5zHE,sD5B2zHF,oD4B1zHI,cAAA,EAIJ,mB5B2zHA,iB4BzzHE,MAAA,GACA,YAAA,OACA,eAAA,OAKF,mBACE,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,WAAA,OACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,IAGA,4BACE,QAAA,IAAA,KACA,UAAA,KACA,cAAA,IAEF,4BACE,QAAA,KAAA,KACA,UAAA,KACA,cAAA,I5ByzHJ,wC4B70HA,qCA0BI,WAAA,EAKJ,uC5BkzHA,+BACA,kCACA,6CACA,8CAEA,6DADA,wE2B55HE,wBAAA,EACA,2BAAA,EC8GF,+BACE,aAAA,EAEF,sC5BmzHA,8BAKA,+DADA,oDAHA,iCACA,4CACA,6C2Bh6HE,uBAAA,EACA,0BAAA,ECkHF,8BACE,YAAA,EAKF,iBACE,SAAA,SAGA,UAAA,EACA,YAAA,OALF,sBAUI,SAAA,SAVJ,2BAYM,YAAA,K5BizHN,6BADA,4B4B7yHI,4BAGE,QAAA,EAKJ,kC5B0yHF,wC4BvyHM,aAAA,KAGJ,iC5BwyHF,uC4BryHM,QAAA,EACA,YAAA,KC/JN,KACE,aAAA,EACA,cAAA,EACA,WAAA,KAHF,QAOI,SAAA,SACA,QAAA,MARJ,UAWM,SAAA,SACA,QAAA,MACA,QAAA,KAAA,K7By8HN,gB6Bx8HM,gBAEE,gBAAA,KACA,iBAAA,KAKJ,mBACE,MAAA,K7Bu8HN,yB6Br8HM,yBAEE,MAAA,KACA,gBAAA,KACA,OAAA,YACA,iBAAA,YAOJ,a7Bi8HJ,mBADA,mB6B77HM,iBAAA,KACA,aAAA,QAzCN,kBLLE,OAAA,IACA,OAAA,IAAA,EACA,SAAA,OACA,iBAAA,QKEF,cA0DI,UAAA,KASJ,UACE,cAAA,IAAA,MAAA,KADF,aAGI,MAAA,KAEA,cAAA,KALJ,eASM,aAAA,IACA,YAAA,WACA,OAAA,IAAA,MAAA,YACA,cAAA,IAAA,IAAA,EAAA,EACA,qBACE,aAAA,KAAA,KAAA,KAMF,sB7B86HN,4BADA,4B6B16HQ,MAAA,KACA,OAAA,QACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,oBAAA,YAKN,wBAqDA,MAAA,KA8BA,cAAA,EAnFA,2BAwDE,MAAA,KAxDF,6BA0DI,cAAA,IACA,WAAA,OA3DJ,iDAgEE,IAAA,KACA,KAAA,KAGF,yBAAA,2BAEI,QAAA,WACA,MAAA,GAHJ,6BAKM,cAAA,GAzEN,6BAuFE,aAAA,EACA,cAAA,IAxFF,kC7Bu8HF,wCADA,wC6Bx2HI,OAAA,IAAA,MAAA,KAGF,yBAAA,6BAEI,cAAA,IAAA,MAAA,KACA,cAAA,IAAA,IAAA,EAAA,EAHJ,kC7Bg3HA,wCADA,wC6Bv2HI,oBAAA,MAhGN,cAEI,MAAA,KAFJ,gBAMM,cAAA,IANN,iBASM,YAAA,IAKA,uB7By8HN,6BADA,6B6Br8HQ,MAAA,KACA,iBAAA,QAQR,gBAEI,MAAA,KAFJ,mBAIM,WAAA,IACA,YAAA,EAYN,eACE,MAAA,KADF,kBAII,MAAA,KAJJ,oBAMM,cAAA,IACA,WAAA,OAPN,wCAYI,IAAA,KACA,KAAA,KAGF,yBAAA,kBAEI,QAAA,WACA,MAAA,GAHJ,oBAKM,cAAA,GASR,oBACE,cAAA,EADF,yBAKI,aAAA,EACA,cAAA,IANJ,8B7By7HA,oCADA,oC6B56HI,OAAA,IAAA,MAAA,KAGF,yBAAA,yBAEI,cAAA,IAAA,MAAA,KACA,cAAA,IAAA,IAAA,EAAA,EAHJ,8B7Bo7HA,oCADA,oC6B36HI,oBAAA,MAUN,uBAEI,QAAA,KAFJ,qBAKI,QAAA,MASJ,yBAEE,WAAA,KF7OA,uBAAA,EACA,wBAAA,EGQF,QACE,SAAA,SACA,WAAA,KACA,cAAA,KACA,OAAA,IAAA,MAAA,YAKA,yBAAA,QACE,cAAA,KAaF,yBAAA,eACE,MAAA,MAeJ,iBACE,cAAA,KACA,aAAA,KACA,WAAA,QACA,WAAA,IAAA,MAAA,YACA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,WAAA,MAAA,EAAA,IAAA,EAAA,qBAEA,2BAAA,MAEA,oBACE,WAAA,KAGF,yBAAA,iBACE,MAAA,KACA,WAAA,EACA,mBAAA,KAAA,WAAA,KAEA,0BACE,QAAA,gBACA,OAAA,eACA,eAAA,EACA,SAAA,kBAGF,oBACE,WAAA,Q9BknIJ,sC8B7mIE,mC9B4mIF,oC8BzmII,cAAA,EACA,aAAA,G9B+mIN,qB8B1mIA,kBAWE,SAAA,MACA,MAAA,EACA,KAAA,EACA,QAAA,K9BmmIF,sC8BjnIA,mCAGI,WAAA,MAEA,4D9BinIF,sC8BjnIE,mCACE,WAAA,OAWJ,yB9B2mIA,qB8B3mIA,kBACE,cAAA,GAIJ,kBACE,IAAA,EACA,aAAA,EAAA,EAAA,IAEF,qBACE,OAAA,EACA,cAAA,EACA,aAAA,IAAA,EAAA,E9B+mIF,kCAFA,gCACA,4B8BtmIA,0BAII,aAAA,MACA,YAAA,MAEA,yB9BwmIF,kCAFA,gCACA,4B8BvmIE,0BACE,aAAA,EACA,YAAA,GAaN,mBACE,QAAA,KACA,aAAA,EAAA,EAAA,IAEA,yBAAA,mBACE,cAAA,GAOJ,cACE,MAAA,KACA,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,K9B8lIF,oB8B5lIE,oBAEE,gBAAA,KATJ,kBAaI,QAAA,MAGF,yBACE,iC9B0lIF,uC8BxlII,YAAA,OAWN,eACE,SAAA,SACA,MAAA,MACA,QAAA,IAAA,KACA,aAAA,KC9LA,WAAA,IACA,cAAA,ID+LA,iBAAA,YACA,iBAAA,KACA,OAAA,IAAA,MAAA,YACA,cAAA,IAIA,qBACE,QAAA,EAdJ,yBAmBI,QAAA,MACA,MAAA,KACA,OAAA,IACA,cAAA,IAtBJ,mCAyBI,WAAA,IAGF,yBAAA,eACE,QAAA,MAUJ,YACE,OAAA,MAAA,MADF,iBAII,YAAA,KACA,eAAA,KACA,YAAA,KAGF,yBAAA,iCAGI,SAAA,OACA,MAAA,KACA,MAAA,KACA,WAAA,EACA,iBAAA,YACA,OAAA,EACA,mBAAA,KAAA,WAAA,K9BykIJ,kD8BllIA,sCAYM,QAAA,IAAA,KAAA,IAAA,KAZN,sCAeM,YAAA,K9B0kIN,4C8BzkIM,4CAEE,iBAAA,MAOR,yBAAA,YACE,MAAA,KACA,OAAA,EAFF,eAKI,MAAA,KALJ,iBAOM,YAAA,KACA,eAAA,MAYR,aACE,QAAA,KAAA,KACA,aAAA,MACA,YAAA,MACA,WAAA,IAAA,MAAA,YACA,cAAA,IAAA,MAAA,Y1B5NA,mBAAA,MAAA,EAAA,IAAA,EAAA,oBAAA,CAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,oBAAA,CAAA,EAAA,IAAA,EAAA,qB2BjER,WAAA,IACA,cAAA,Id6cA,yBAAA,yBAGI,QAAA,aACA,cAAA,EACA,eAAA,OALJ,2BAUI,QAAA,aACA,MAAA,KACA,eAAA,OAZJ,kCAiBI,QAAA,aAjBJ,0BAqBI,QAAA,aACA,eAAA,OjB+4HJ,wCiBr6HA,6CjBo6HA,2CiBz4HM,MAAA,KA3BN,wCAiCI,MAAA,KAjCJ,4BAqCI,cAAA,EACA,eAAA,OjB04HJ,uBiBh7HA,oBA6CI,QAAA,aACA,WAAA,EACA,cAAA,EACA,eAAA,OjBu4HJ,6BiBv7HA,0BAmDM,aAAA,EjBw4HN,4CiB37HA,sCAwDI,SAAA,SACA,YAAA,EAzDJ,kDA8DI,IAAA,GaxOF,yBAAA,yBACE,cAAA,IAEA,oCACE,cAAA,GASN,yBAAA,aACE,MAAA,KACA,YAAA,EACA,eAAA,EACA,aAAA,EACA,YAAA,EACA,OAAA,E1BvPF,mBAAA,KACQ,WAAA,M0B+PV,8BACE,WAAA,EHpUA,uBAAA,EACA,wBAAA,EGuUF,mDACE,cAAA,EHzUA,uBAAA,IACA,wBAAA,IAOA,2BAAA,EACA,0BAAA,EG0UF,YChVE,WAAA,IACA,cAAA,IDkVA,mBCnVA,WAAA,KACA,cAAA,KDqVA,mBCtVA,WAAA,KACA,cAAA,KD+VF,aChWE,WAAA,KACA,cAAA,KDkWA,yBAAA,aACE,MAAA,KACA,aAAA,KACA,YAAA,MAaJ,yBACE,aEtWA,MAAA,eFuWA,cE1WA,MAAA,gBF4WE,aAAA,MAFF,4BAKI,aAAA,GAUN,gBACE,iBAAA,QACA,aAAA,QAFF,8BAKI,MAAA,K9BmlIJ,oC8BllII,oCAEE,MAAA,QACA,iBAAA,YATN,6BAcI,MAAA,KAdJ,iCAmBM,MAAA,K9BglIN,uC8B9kIM,uCAEE,MAAA,KACA,iBAAA,YAIF,sC9B6kIN,4CADA,4C8BzkIQ,MAAA,KACA,iBAAA,QAIF,wC9B2kIN,8CADA,8C8BvkIQ,MAAA,KACA,iBAAA,YAOF,oC9BskIN,0CADA,0C8BlkIQ,MAAA,KACA,iBAAA,QAIJ,yBAAA,sDAIM,MAAA,K9BmkIR,4D8BlkIQ,4DAEE,MAAA,KACA,iBAAA,YAIF,2D9BikIR,iEADA,iE8B7jIU,MAAA,KACA,iBAAA,QAIF,6D9B+jIR,mEADA,mE8B3jIU,MAAA,KACA,iBAAA,aA/EZ,+BAuFI,aAAA,K9B4jIJ,qC8B3jII,qCAEE,iBAAA,KA1FN,yCA6FM,iBAAA,KA7FN,iC9B0pIA,6B8BvjII,aAAA,QAnGJ,6BA4GI,MAAA,KACA,mCACE,MAAA,KA9GN,0BAmHI,MAAA,K9BojIJ,gC8BnjII,gCAEE,MAAA,K9BsjIN,0C8BljIM,0C9BmjIN,mDAFA,mD8B/iIQ,MAAA,KAQR,gBACE,iBAAA,KACA,aAAA,QAFF,8BAKI,MAAA,Q9B+iIJ,oC8B9iII,oCAEE,MAAA,KACA,iBAAA,YATN,6BAcI,MAAA,QAdJ,iCAmBM,MAAA,Q9B4iIN,uC8B1iIM,uCAEE,MAAA,KACA,iBAAA,YAIF,sC9ByiIN,4CADA,4C8BriIQ,MAAA,KACA,iBAAA,QAIF,wC9BuiIN,8CADA,8C8BniIQ,MAAA,KACA,iBAAA,YAMF,oC9BmiIN,0CADA,0C8B/hIQ,MAAA,KACA,iBAAA,QAIJ,yBAAA,kEAIM,aAAA,QAJN,0DAOM,iBAAA,QAPN,sDAUM,MAAA,Q9BgiIR,4D8B/hIQ,4DAEE,MAAA,KACA,iBAAA,YAIF,2D9B8hIR,iEADA,iE8B1hIU,MAAA,KACA,iBAAA,QAIF,6D9B4hIR,mEADA,mE8BxhIU,MAAA,KACA,iBAAA,aApFZ,+BA6FI,aAAA,K9BwhIJ,qC8BvhII,qCAEE,iBAAA,KAhGN,yCAmGM,iBAAA,KAnGN,iC9B4nIA,6B8BnhII,aAAA,QAzGJ,6BA6GI,MAAA,QACA,mCACE,MAAA,KA/GN,0BAoHI,MAAA,Q9BqhIJ,gC8BphII,gCAEE,MAAA,K9BuhIN,0C8BnhIM,0C9BohIN,mDAFA,mD8BhhIQ,MAAA,KGtoBR,YACE,QAAA,IAAA,KACA,cAAA,KACA,WAAA,KACA,iBAAA,QACA,cAAA,IALF,eAQI,QAAA,aARJ,yBAWM,QAAA,EAAA,IACA,MAAA,KACA,QAAA,SAbN,oBAkBI,MAAA,KCpBJ,YACE,QAAA,aACA,aAAA,EACA,OAAA,KAAA,EACA,cAAA,IAJF,eAOI,QAAA,OAPJ,iBlCyrJA,oBkC/qJM,SAAA,SACA,MAAA,KACA,QAAA,IAAA,KACA,YAAA,KACA,YAAA,WACA,MAAA,QACA,gBAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KlCorJN,uBkClrJM,uBlCmrJN,0BAFA,0BkC/qJQ,QAAA,EACA,MAAA,QACA,iBAAA,KACA,aAAA,KAGJ,6BlCkrJJ,gCkC/qJQ,YAAA,EPnBN,uBAAA,IACA,0BAAA,IOsBE,4BlCirJJ,+B2BhtJE,wBAAA,IACA,2BAAA,IOwCE,sBlC+qJJ,4BAFA,4BADA,yBAIA,+BAFA,+BkC3qJM,QAAA,EACA,MAAA,KACA,OAAA,QACA,iBAAA,QACA,aAAA,QlCmrJN,wBAEA,8BADA,8BkCxuJA,2BlCsuJA,iCADA,iCkCtqJM,MAAA,KACA,OAAA,YACA,iBAAA,KACA,aAAA,KASN,oBlCqqJA,uBmC7uJM,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UAEF,gCnC+uJJ,mC2B1uJE,uBAAA,IACA,0BAAA,IQAE,+BnC8uJJ,kC2BvvJE,wBAAA,IACA,2BAAA,IO2EF,oBlCgrJA,uBmC7vJM,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IAEF,gCnC+vJJ,mC2B1vJE,uBAAA,IACA,0BAAA,IQAE,+BnC8vJJ,kC2BvwJE,wBAAA,IACA,2BAAA,ISHF,OACE,aAAA,EACA,OAAA,KAAA,EACA,WAAA,OACA,WAAA,KAJF,UAOI,QAAA,OAPJ,YpCuxJA,eoC7wJM,QAAA,aACA,QAAA,IAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,KpCixJN,kBoC/xJA,kBAmBM,gBAAA,KACA,iBAAA,KApBN,epCoyJA,kBoCzwJM,MAAA,MA3BN,mBpCwyJA,sBoCtwJM,MAAA,KAlCN,mBpC6yJA,yBADA,yBAEA,sBoCnwJM,MAAA,KACA,OAAA,YACA,iBAAA,KC9CN,OACE,QAAA,OACA,QAAA,KAAA,KAAA,KACA,UAAA,IACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,eAAA,SACA,cAAA,MrCuzJF,cqCnzJI,cAEE,MAAA,KACA,gBAAA,KACA,OAAA,QAKJ,aACE,QAAA,KAIF,YACE,SAAA,SACA,IAAA,KAOJ,eCtCE,iBAAA,KtCk1JF,2BsC/0JI,2BAEE,iBAAA,QDqCN,eC1CE,iBAAA,QtCy1JF,2BsCt1JI,2BAEE,iBAAA,QDyCN,eC9CE,iBAAA,QtCg2JF,2BsC71JI,2BAEE,iBAAA,QD6CN,YClDE,iBAAA,QtCu2JF,wBsCp2JI,wBAEE,iBAAA,QDiDN,eCtDE,iBAAA,QtC82JF,2BsC32JI,2BAEE,iBAAA,QDqDN,cC1DE,iBAAA,QtCq3JF,0BsCl3JI,0BAEE,iBAAA,QCFN,OACE,QAAA,aACA,UAAA,KACA,QAAA,IAAA,IACA,UAAA,KACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,eAAA,OACA,iBAAA,KACA,cAAA,KAGA,aACE,QAAA,KAIF,YACE,SAAA,SACA,IAAA,KvCq3JJ,0BuCl3JE,eAEE,IAAA,EACA,QAAA,IAAA,IvCo3JJ,cuC/2JI,cAEE,MAAA,KACA,gBAAA,KACA,OAAA,QAKJ,+BvC42JF,4BuC12JI,MAAA,QACA,iBAAA,KAGF,wBACE,MAAA,MAGF,+BACE,aAAA,IAGF,uBACE,YAAA,IC1DJ,WACE,YAAA,KACA,eAAA,KACA,cAAA,KACA,MAAA,QACA,iBAAA,KxCu6JF,ewC56JA,cASI,MAAA,QATJ,aAaI,cAAA,KACA,UAAA,KACA,YAAA,IAfJ,cAmBI,iBAAA,QAGF,sBxCk6JF,4BwCh6JI,cAAA,KACA,aAAA,KACA,cAAA,IA1BJ,sBA8BI,UAAA,KAGF,oCAAA,WACE,YAAA,KACA,eAAA,KAEA,sBxCi6JF,4BwC/5JI,cAAA,KACA,aAAA,KxCm6JJ,ewC16JA,cAYI,UAAA,MC1CN,WACE,QAAA,MACA,QAAA,IACA,cAAA,KACA,YAAA,WACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,IrCiLA,mBAAA,OAAA,IAAA,YACK,cAAA,OAAA,IAAA,YACG,WAAA,OAAA,IAAA,YJ+xJV,iByCz9JA,eAaI,aAAA,KACA,YAAA,KzCi9JJ,mBADA,kByC58JE,kBAGE,aAAA,QArBJ,oBA0BI,QAAA,IACA,MAAA,KC3BJ,OACE,QAAA,KACA,cAAA,KACA,OAAA,IAAA,MAAA,YACA,cAAA,IAJF,UAQI,WAAA,EACA,MAAA,QATJ,mBAcI,YAAA,IAdJ,S1Co/JA,U0Ch+JI,cAAA,EApBJ,WAwBI,WAAA,IASJ,mB1C09JA,mB0Cx9JE,cAAA,KAFF,0B1C89JA,0B0Cx9JI,SAAA,SACA,IAAA,KACA,MAAA,MACA,MAAA,QAQJ,eCvDE,MAAA,QACA,iBAAA,QACA,aAAA,QDqDF,kBClDI,iBAAA,QDkDJ,2BC9CI,MAAA,QDkDJ,YC3DE,MAAA,QACA,iBAAA,QACA,aAAA,QDyDF,eCtDI,iBAAA,QDsDJ,wBClDI,MAAA,QDsDJ,eC/DE,MAAA,QACA,iBAAA,QACA,aAAA,QD6DF,kBC1DI,iBAAA,QD0DJ,2BCtDI,MAAA,QD0DJ,cCnEE,MAAA,QACA,iBAAA,QACA,aAAA,QDiEF,iBC9DI,iBAAA,QD8DJ,0BC1DI,MAAA,QCDJ,wCACE,KAAQ,oBAAA,KAAA,EACR,GAAQ,oBAAA,EAAA,GAIV,mCACE,KAAQ,oBAAA,KAAA,EACR,GAAQ,oBAAA,EAAA,GAFV,gCACE,KAAQ,oBAAA,KAAA,EACR,GAAQ,oBAAA,EAAA,GAQV,UACE,OAAA,KACA,cAAA,KACA,SAAA,OACA,iBAAA,QACA,cAAA,IxCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,eACQ,WAAA,MAAA,EAAA,IAAA,IAAA,ewClCV,cACE,MAAA,KACA,MAAA,GACA,OAAA,KACA,UAAA,KACA,YAAA,KACA,MAAA,KACA,WAAA,OACA,iBAAA,QxCyBA,mBAAA,MAAA,EAAA,KAAA,EAAA,gBACQ,WAAA,MAAA,EAAA,KAAA,EAAA,gBAyHR,mBAAA,MAAA,IAAA,KACK,cAAA,MAAA,IAAA,KACG,WAAA,MAAA,IAAA,KJw6JV,sB4CnjKA,gCCDI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKDEF,wBAAA,KAAA,KAAA,gBAAA,KAAA,K5CwjKF,qB4CjjKA,+BxC5CE,kBAAA,qBAAA,GAAA,OAAA,SACK,aAAA,qBAAA,GAAA,OAAA,SACG,UAAA,qBAAA,GAAA,OAAA,SwCmDV,sBEvEE,iBAAA,QAGA,wCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKDsBJ,mBE3EE,iBAAA,QAGA,qCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKD0BJ,sBE/EE,iBAAA,QAGA,wCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKD8BJ,qBEnFE,iBAAA,QAGA,uCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKExDJ,OAEE,WAAA,KAEA,mBACE,WAAA,EAIJ,O/CqpKA,Y+CnpKE,SAAA,OACA,KAAA,EAGF,YACE,MAAA,QAGF,cACE,QAAA,MAGA,4BACE,UAAA,KAIJ,a/CgpKA,mB+C9oKE,aAAA,KAGF,Y/C+oKA,kB+C7oKE,cAAA,K/CkpKF,Y+C/oKA,Y/C8oKA,a+C3oKE,QAAA,WACA,eAAA,IAGF,cACE,eAAA,OAGF,cACE,eAAA,OAIF,eACE,WAAA,EACA,cAAA,IAMF,YACE,aAAA,EACA,WAAA,KCrDF,YAEE,aAAA,EACA,cAAA,KAQF,iBACE,SAAA,SACA,QAAA,MACA,QAAA,KAAA,KAEA,cAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KAGA,6BrB7BA,uBAAA,IACA,wBAAA,IqB+BA,4BACE,cAAA,ErBzBF,2BAAA,IACA,0BAAA,IqB6BA,0BhDqrKF,gCADA,gCgDjrKI,MAAA,KACA,OAAA,YACA,iBAAA,KALF,mDhD4rKF,yDADA,yDgDlrKM,MAAA,QATJ,gDhDisKF,sDADA,sDgDprKM,MAAA,KAKJ,wBhDqrKF,8BADA,8BgDjrKI,QAAA,EACA,MAAA,KACA,iBAAA,QACA,aAAA,QANF,iDhDisKF,wDAHA,uDADA,uDAMA,8DAHA,6DAJA,uDAMA,8DAHA,6DgDnrKM,MAAA,QAZJ,8ChDwsKF,oDADA,oDgDxrKM,MAAA,QAWN,kBhDkrKA,uBgDhrKE,MAAA,KAFF,2ChDsrKA,gDgDjrKI,MAAA,KhDsrKJ,wBgDlrKE,wBhDmrKF,6BAFA,6BgD/qKI,MAAA,KACA,gBAAA,KACA,iBAAA,QAIJ,uBACE,MAAA,KACA,WAAA,KnCvGD,yBoCIG,MAAA,QACA,iBAAA,QAEA,0BjDuxKJ,+BiDrxKM,MAAA,QAFF,mDjD2xKJ,wDiDtxKQ,MAAA,QjD2xKR,gCiDxxKM,gCjDyxKN,qCAFA,qCiDrxKQ,MAAA,QACA,iBAAA,QAEF,iCjD4xKN,uCAFA,uCADA,sCAIA,4CAFA,4CiDxxKQ,MAAA,KACA,iBAAA,QACA,aAAA,QpCzBP,sBoCIG,MAAA,QACA,iBAAA,QAEA,uBjDozKJ,4BiDlzKM,MAAA,QAFF,gDjDwzKJ,qDiDnzKQ,MAAA,QjDwzKR,6BiDrzKM,6BjDszKN,kCAFA,kCiDlzKQ,MAAA,QACA,iBAAA,QAEF,8BjDyzKN,oCAFA,oCADA,mCAIA,yCAFA,yCiDrzKQ,MAAA,KACA,iBAAA,QACA,aAAA,QpCzBP,yBoCIG,MAAA,QACA,iBAAA,QAEA,0BjDi1KJ,+BiD/0KM,MAAA,QAFF,mDjDq1KJ,wDiDh1KQ,MAAA,QjDq1KR,gCiDl1KM,gCjDm1KN,qCAFA,qCiD/0KQ,MAAA,QACA,iBAAA,QAEF,iCjDs1KN,uCAFA,uCADA,sCAIA,4CAFA,4CiDl1KQ,MAAA,KACA,iBAAA,QACA,aAAA,QpCzBP,wBoCIG,MAAA,QACA,iBAAA,QAEA,yBjD82KJ,8BiD52KM,MAAA,QAFF,kDjDk3KJ,uDiD72KQ,MAAA,QjDk3KR,+BiD/2KM,+BjDg3KN,oCAFA,oCiD52KQ,MAAA,QACA,iBAAA,QAEF,gCjDm3KN,sCAFA,sCADA,qCAIA,2CAFA,2CiD/2KQ,MAAA,KACA,iBAAA,QACA,aAAA,QDiGR,yBACE,WAAA,EACA,cAAA,IAEF,sBACE,cAAA,EACA,YAAA,IExHF,OACE,cAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,YACA,cAAA,I9C0DA,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gB8CtDV,YACE,QAAA,KAKF,eACE,QAAA,KAAA,KACA,cAAA,IAAA,MAAA,YvBtBA,uBAAA,IACA,wBAAA,IuBmBF,0CAMI,MAAA,QAKJ,aACE,WAAA,EACA,cAAA,EACA,UAAA,KACA,MAAA,QlD24KF,oBAEA,sBkDj5KA,elD84KA,mBAEA,qBkDr4KI,MAAA,QAKJ,cACE,QAAA,KAAA,KACA,iBAAA,QACA,WAAA,IAAA,MAAA,KvB1CA,2BAAA,IACA,0BAAA,IuBmDF,mBlD+3KA,mCkD53KI,cAAA,EAHJ,oClDm4KA,oDkD73KM,aAAA,IAAA,EACA,cAAA,EAIF,4DlD63KJ,4EkD33KQ,WAAA,EvBzEN,uBAAA,IACA,wBAAA,IuB8EE,0DlD23KJ,0EkDz3KQ,cAAA,EvBzEN,2BAAA,IACA,0BAAA,IuBmDF,+EvB5DE,uBAAA,EACA,wBAAA,EuB4FF,wDAEI,iBAAA,EAGJ,0BACE,iBAAA,ElDw3KF,8BkDh3KA,clD+2KA,gCkD32KI,cAAA,ElDi3KJ,sCkDr3KA,sBlDo3KA,wCkD72KM,cAAA,KACA,aAAA,KlDk3KN,wDkD13KA,0BvB3GE,uBAAA,IACA,wBAAA,I3B2+KF,yFAFA,yFACA,2DkDh4KA,2DAmBQ,uBAAA,IACA,wBAAA,IlDo3KR,wGAIA,wGANA,wGAIA,wGAHA,0EAIA,0EkD34KA,0ElDy4KA,0EkDj3KU,uBAAA,IlD03KV,uGAIA,uGANA,uGAIA,uGAHA,yEAIA,yEkDr5KA,yElDm5KA,yEkDv3KU,wBAAA,IlD83KV,sDkD15KA,yBvBnGE,2BAAA,IACA,0BAAA,I3BigLF,qFAEA,qFkDj6KA,wDlDg6KA,wDkDv3KQ,2BAAA,IACA,0BAAA,IlD43KR,oGAIA,oGAFA,oGAIA,oGkD56KA,uElDy6KA,uEAFA,uEAIA,uEkD73KU,0BAAA,IlDk4KV,mGAIA,mGAFA,mGAIA,mGkDt7KA,sElDm7KA,sEAFA,sEAIA,sEkDn4KU,2BAAA,IAlDV,0BlD07KA,qCACA,0BACA,qCkDj4KI,WAAA,IAAA,MAAA,KlDq4KJ,kDkDh8KA,kDA+DI,WAAA,EA/DJ,uBlDo8KA,yCkDj4KI,OAAA,ElD44KJ,+CANA,+CAQA,+CANA,+CAEA,+CkD78KA,+ClDg9KA,iEANA,iEAQA,iEANA,iEAEA,iEANA,iEkD93KU,YAAA,ElDm5KV,8CANA,8CAQA,8CANA,8CAEA,8CkD39KA,8ClD89KA,gEANA,gEAQA,gEANA,gEAEA,gEANA,gEkDx4KU,aAAA,ElDu5KV,+CAIA,+CkDz+KA,+ClDu+KA,+CADA,iEAIA,iEANA,iEAIA,iEkDj5KU,cAAA,EAvFV,8ClDi/KA,8CAFA,8CAIA,8CALA,gEAIA,gEAFA,gEAIA,gEkDp5KU,cAAA,EAhGV,yBAsGI,cAAA,EACA,OAAA,EAUJ,aACE,cAAA,KADF,oBAKI,cAAA,EACA,cAAA,IANJ,2BASM,WAAA,IATN,4BAcI,cAAA,ElD04KJ,wDkDx5KA,wDAkBM,WAAA,IAAA,MAAA,KAlBN,2BAuBI,WAAA,EAvBJ,uDAyBM,cAAA,IAAA,MAAA,KAON,eC5PE,aAAA,KAEA,8BACE,MAAA,KACA,iBAAA,QACA,aAAA,KAHF,0DAMI,iBAAA,KANJ,qCASI,MAAA,QACA,iBAAA,KAGJ,yDAEI,oBAAA,KD8ON,eC/PE,aAAA,QAEA,8BACE,MAAA,KACA,iBAAA,QACA,aAAA,QAHF,0DAMI,iBAAA,QANJ,qCASI,MAAA,QACA,iBAAA,KAGJ,yDAEI,oBAAA,QDiPN,eClQE,aAAA,QAEA,8BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,0DAMI,iBAAA,QANJ,qCASI,MAAA,QACA,iBAAA,QAGJ,yDAEI,oBAAA,QDoPN,YCrQE,aAAA,QAEA,2BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,uDAMI,iBAAA,QANJ,kCASI,MAAA,QACA,iBAAA,QAGJ,sDAEI,oBAAA,QDuPN,eCxQE,aAAA,QAEA,8BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,0DAMI,iBAAA,QANJ,qCASI,MAAA,QACA,iBAAA,QAGJ,yDAEI,oBAAA,QD0PN,cC3QE,aAAA,QAEA,6BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,yDAMI,iBAAA,QANJ,oCASI,MAAA,QACA,iBAAA,QAGJ,wDAEI,oBAAA,QChBN,kBACE,SAAA,SACA,QAAA,MACA,OAAA,EACA,QAAA,EACA,SAAA,OALF,yCpDivLA,wBADA,yBAEA,yBACA,wBoDvuLI,SAAA,SACA,IAAA,EACA,OAAA,EACA,KAAA,EACA,MAAA,KACA,OAAA,KACA,OAAA,EAKJ,wBACE,eAAA,OAIF,uBACE,eAAA,IC3BF,MACE,WAAA,KACA,QAAA,KACA,cAAA,KACA,iBAAA,QACA,OAAA,IAAA,MAAA,QACA,cAAA,IjD0DA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBiDjEV,iBASI,aAAA,KACA,aAAA,gBAKJ,SACE,QAAA,KACA,cAAA,IAEF,SACE,QAAA,IACA,cAAA,ICpBF,OACE,MAAA,MACA,UAAA,KACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,YAAA,EAAA,IAAA,EAAA,KjCTA,OAAA,kBACA,QAAA,GrBkyLF,asDvxLE,aAEE,MAAA,KACA,gBAAA,KACA,OAAA,QjChBF,OAAA,kBACA,QAAA,GiCuBA,aACE,QAAA,EACA,OAAA,QACA,WAAA,IACA,OAAA,EACA,mBAAA,KACA,gBAAA,KAAA,WAAA,KCxBJ,YACE,SAAA,OAIF,OACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,SAAA,OACA,2BAAA,MAIA,QAAA,EAGA,0BnDiHA,kBAAA,kBACI,cAAA,kBACC,aAAA,kBACG,UAAA,kBAkER,mBAAA,kBAAA,IAAA,SAEK,cAAA,aAAA,IAAA,SACG,WAAA,kBAAA,IAAA,SAAA,WAAA,UAAA,IAAA,SAAA,WAAA,UAAA,IAAA,QAAA,CAAA,kBAAA,IAAA,QAAA,CAAA,aAAA,IAAA,SmDrLR,wBnD6GA,kBAAA,eACI,cAAA,eACC,aAAA,eACG,UAAA,emD9GV,mBACE,WAAA,OACA,WAAA,KAIF,cACE,SAAA,SACA,MAAA,KACA,OAAA,KAIF,eACE,SAAA,SACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,KACA,OAAA,IAAA,MAAA,eACA,cAAA,InDcA,mBAAA,EAAA,IAAA,IAAA,eACQ,WAAA,EAAA,IAAA,IAAA,emDZR,QAAA,EAIF,gBACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,KACA,iBAAA,KAEA,qBlCpEA,OAAA,iBACA,QAAA,EkCoEA,mBlCrEA,OAAA,kBACA,QAAA,GkCyEF,cACE,QAAA,KACA,cAAA,IAAA,MAAA,QAIF,qBACE,WAAA,KAIF,aACE,OAAA,EACA,YAAA,WAKF,YACE,SAAA,SACA,QAAA,KAIF,cACE,QAAA,KACA,WAAA,MACA,WAAA,IAAA,MAAA,QAHF,wBAQI,cAAA,EACA,YAAA,IATJ,mCAaI,YAAA,KAbJ,oCAiBI,YAAA,EAKJ,yBACE,SAAA,SACA,IAAA,QACA,MAAA,KACA,OAAA,KACA,SAAA,OAIF,yBAEE,cACE,MAAA,MACA,OAAA,KAAA,KAEF,enDrEA,mBAAA,EAAA,IAAA,KAAA,eACQ,WAAA,EAAA,IAAA,KAAA,emDyER,UAAY,MAAA,OAGd,yBACE,UAAY,MAAA,OC9Id,SACE,SAAA,SACA,QAAA,KACA,QAAA,MCRA,YAAA,gBAAA,CAAA,SAAA,CAAA,KAAA,CAAA,WAEA,WAAA,OACA,YAAA,IACA,YAAA,WACA,WAAA,KACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,UAAA,OACA,YAAA,ODHA,UAAA,KnCTA,OAAA,iBACA,QAAA,EmCYA,YnCbA,OAAA,kBACA,QAAA,GmCaA,aACE,QAAA,IAAA,EACA,WAAA,KAEF,eACE,QAAA,EAAA,IACA,YAAA,IAEF,gBACE,QAAA,IAAA,EACA,WAAA,IAEF,cACE,QAAA,EAAA,IACA,YAAA,KAIF,4BACE,OAAA,EACA,KAAA,IACA,YAAA,KACA,aAAA,IAAA,IAAA,EACA,iBAAA,KAEF,iCACE,MAAA,IACA,OAAA,EACA,cAAA,KACA,aAAA,IAAA,IAAA,EACA,iBAAA,KAEF,kCACE,OAAA,EACA,KAAA,IACA,cAAA,KACA,aAAA,IAAA,IAAA,EACA,iBAAA,KAEF,8BACE,IAAA,IACA,KAAA,EACA,WAAA,KACA,aAAA,IAAA,IAAA,IAAA,EACA,mBAAA,KAEF,6BACE,IAAA,IACA,MAAA,EACA,WAAA,KACA,aAAA,IAAA,EAAA,IAAA,IACA,kBAAA,KAEF,+BACE,IAAA,EACA,KAAA,IACA,YAAA,KACA,aAAA,EAAA,IAAA,IACA,oBAAA,KAEF,oCACE,IAAA,EACA,MAAA,IACA,WAAA,KACA,aAAA,EAAA,IAAA,IACA,oBAAA,KAEF,qCACE,IAAA,EACA,KAAA,IACA,WAAA,KACA,aAAA,EAAA,IAAA,IACA,oBAAA,KAKJ,eACE,UAAA,MACA,QAAA,IAAA,IACA,MAAA,KACA,WAAA,OACA,iBAAA,KACA,cAAA,IAIF,eACE,SAAA,SACA,MAAA,EACA,OAAA,EACA,aAAA,YACA,aAAA,MEzGF,SACE,SAAA,SACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,UAAA,MACA,QAAA,IDXA,YAAA,gBAAA,CAAA,SAAA,CAAA,KAAA,CAAA,WAEA,WAAA,OACA,YAAA,IACA,YAAA,WACA,WAAA,KACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,UAAA,OACA,YAAA,OCAA,UAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,KACA,OAAA,IAAA,MAAA,eACA,cAAA,ItDiDA,mBAAA,EAAA,IAAA,KAAA,eACQ,WAAA,EAAA,IAAA,KAAA,esD9CR,aAAQ,WAAA,MACR,eAAU,YAAA,KACV,gBAAW,WAAA,KACX,cAAS,YAAA,MAvBX,gBA4BI,aAAA,KAEA,gB1DkjMJ,sB0DhjMM,SAAA,SACA,QAAA,MACA,MAAA,EACA,OAAA,EACA,aAAA,YACA,aAAA,MAGF,sBACE,QAAA,GACA,aAAA,KAIJ,oBACE,OAAA,MACA,KAAA,IACA,YAAA,MACA,iBAAA,KACA,iBAAA,gBACA,oBAAA,EACA,0BACE,OAAA,IACA,YAAA,MACA,QAAA,IACA,iBAAA,KACA,oBAAA,EAGJ,sBACE,IAAA,IACA,KAAA,MACA,WAAA,MACA,mBAAA,KACA,mBAAA,gBACA,kBAAA,EACA,4BACE,OAAA,MACA,KAAA,IACA,QAAA,IACA,mBAAA,KACA,kBAAA,EAGJ,uBACE,IAAA,MACA,KAAA,IACA,YAAA,MACA,iBAAA,EACA,oBAAA,KACA,oBAAA,gBACA,6BACE,IAAA,IACA,YAAA,MACA,QAAA,IACA,iBAAA,EACA,oBAAA,KAIJ,qBACE,IAAA,IACA,MAAA,MACA,WAAA,MACA,mBAAA,EACA,kBAAA,KACA,kBAAA,gBACA,2BACE,MAAA,IACA,OAAA,MACA,QAAA,IACA,mBAAA,EACA,kBAAA,KAKN,eACE,QAAA,IAAA,KACA,OAAA,EACA,UAAA,KACA,iBAAA,QACA,cAAA,IAAA,MAAA,QACA,cAAA,IAAA,IAAA,EAAA,EAGF,iBACE,QAAA,IAAA,KCpHF,UACE,SAAA,SAGF,gBACE,SAAA,SACA,MAAA,KACA,SAAA,OAHF,sBAMI,SAAA,SACA,QAAA,KvD6KF,mBAAA,IAAA,YAAA,KACK,cAAA,IAAA,YAAA,KACG,WAAA,IAAA,YAAA,KJs/LV,4B2D5qMA,0BAcM,YAAA,EAIF,8BAAA,uBAAA,sBvDuLF,mBAAA,kBAAA,IAAA,YAEK,cAAA,aAAA,IAAA,YACG,WAAA,kBAAA,IAAA,YAAA,WAAA,UAAA,IAAA,YAAA,WAAA,UAAA,IAAA,WAAA,CAAA,kBAAA,IAAA,WAAA,CAAA,aAAA,IAAA,YA7JR,4BAAA,OAEQ,oBAAA,OA+GR,oBAAA,OAEQ,YAAA,OJ0hMR,mC2DrqMI,2BvDmHJ,kBAAA,sBACQ,UAAA,sBuDjHF,KAAA,E3DwqMN,kC2DtqMI,2BvD8GJ,kBAAA,uBACQ,UAAA,uBuD5GF,KAAA,E3D0qMN,6B2DxqMI,gC3DuqMJ,iCI9jMA,kBAAA,mBACQ,UAAA,mBuDtGF,KAAA,GArCR,wB3DgtMA,sBACA,sB2DpqMI,QAAA,MA7CJ,wBAiDI,KAAA,EAjDJ,sB3DwtMA,sB2DlqMI,SAAA,SACA,IAAA,EACA,MAAA,KAxDJ,sBA4DI,KAAA,KA5DJ,sBA+DI,KAAA,MA/DJ,2B3DouMA,4B2DjqMI,KAAA,EAnEJ,6BAuEI,KAAA,MAvEJ,8BA0EI,KAAA,KAQJ,kBACE,SAAA,SACA,IAAA,EACA,OAAA,EACA,KAAA,EACA,MAAA,IACA,UAAA,KACA,MAAA,KACA,WAAA,OACA,YAAA,EAAA,IAAA,IAAA,eACA,iBAAA,ctCpGA,OAAA,kBACA,QAAA,GsCyGA,uBdrGE,iBAAA,sEACA,iBAAA,iEACA,iBAAA,uFAAA,iBAAA,kEACA,OAAA,+GACA,kBAAA,ScoGF,wBACE,MAAA,EACA,KAAA,Kd1GA,iBAAA,sEACA,iBAAA,iEACA,iBAAA,uFAAA,iBAAA,kEACA,OAAA,+GACA,kBAAA,S7C6wMJ,wB2DlqME,wBAEE,MAAA,KACA,gBAAA,KACA,QAAA,EtCxHF,OAAA,kBACA,QAAA,GrB8xMF,0CACA,2CAFA,6B2DpsMA,6BAuCI,SAAA,SACA,IAAA,IACA,QAAA,EACA,QAAA,aACA,WAAA,M3DmqMJ,0C2D9sMA,6BA+CI,KAAA,IACA,YAAA,M3DmqMJ,2C2DntMA,6BAoDI,MAAA,IACA,aAAA,M3DmqMJ,6B2DxtMA,6BAyDI,MAAA,KACA,OAAA,KACA,YAAA,MACA,YAAA,EAIA,oCACE,QAAA,QAIF,oCACE,QAAA,QAUN,qBACE,SAAA,SACA,OAAA,KACA,KAAA,IACA,QAAA,GACA,MAAA,IACA,aAAA,EACA,YAAA,KACA,WAAA,OACA,WAAA,KATF,wBAYI,QAAA,aACA,MAAA,KACA,OAAA,KACA,OAAA,IACA,YAAA,OACA,OAAA,QAUA,iBAAA,OACA,iBAAA,cAEA,OAAA,IAAA,MAAA,KACA,cAAA,KA/BJ,6BAmCI,MAAA,KACA,OAAA,KACA,OAAA,EACA,iBAAA,KAOJ,kBACE,SAAA,SACA,MAAA,IACA,OAAA,KACA,KAAA,IACA,QAAA,GACA,YAAA,KACA,eAAA,KACA,MAAA,KACA,WAAA,OACA,YAAA,EAAA,IAAA,IAAA,eAEA,uBACE,YAAA,KAMJ,oCAGE,0C3D+nMA,2CAEA,6BADA,6B2D3nMI,MAAA,KACA,OAAA,KACA,WAAA,MACA,UAAA,KARJ,0C3DwoMA,6B2D5nMI,YAAA,MAZJ,2C3D4oMA,6B2D5nMI,aAAA,MAKJ,kBACE,MAAA,IACA,KAAA,IACA,eAAA,KAIF,qBACE,OAAA,M3D0oMJ,qCADA,sCADA,mBADA,oBAXA,gB4D73ME,iB5Dm4MF,uBADA,wBADA,iBADA,kBADA,wBADA,yBASA,mCADA,oCAqBA,oBADA,qBADA,oBADA,qBAXA,WADA,YAOA,uBADA,wBADA,qBADA,sBADA,cADA,eAOA,aADA,cAGA,kBADA,mBAjBA,WADA,Y4Dl4MI,QAAA,MACA,QAAA,I5Dm6MJ,qCADA,mB4Dh6ME,gB5D65MF,uBADA,iBADA,wBAIA,mCAUA,oBADA,oBANA,WAGA,uBADA,qBADA,cAGA,aACA,kBATA,W4D75MI,MAAA,K5BNJ,c6BVE,QAAA,MACA,aAAA,KACA,YAAA,K7BWF,YACE,MAAA,gBAEF,WACE,MAAA,eAQF,MACE,QAAA,eAEF,MACE,QAAA,gBAEF,WACE,WAAA,OAEF,W8BzBE,KAAA,CAAA,CAAA,EAAA,EACA,MAAA,YACA,YAAA,KACA,iBAAA,YACA,OAAA,E9B8BF,QACE,QAAA,eAOF,OACE,SAAA,M+BjCF,cACE,MAAA,a/D88MF,YADA,YADA,Y+Dt8MA,YClBE,QAAA,ehEs+MF,kBACA,mBACA,yBALA,kBACA,mBACA,yBALA,kBACA,mBACA,yB+Dz8MA,kB/Dq8MA,mBACA,yB+D17ME,QAAA,eAIA,yBAAA,YCjDA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhE4/MV,cgE3/MA,cACU,QAAA,sBDkDV,yBAAA,kBACE,QAAA,iBAIF,yBAAA,mBACE,QAAA,kBAIF,yBAAA,yBACE,QAAA,wBAKF,+CAAA,YCtEA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhE0hNV,cgEzhNA,cACU,QAAA,sBDuEV,+CAAA,kBACE,QAAA,iBAIF,+CAAA,mBACE,QAAA,kBAIF,+CAAA,yBACE,QAAA,wBAKF,gDAAA,YC3FA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhEwjNV,cgEvjNA,cACU,QAAA,sBD4FV,gDAAA,kBACE,QAAA,iBAIF,gDAAA,mBACE,QAAA,kBAIF,gDAAA,yBACE,QAAA,wBAKF,0BAAA,YChHA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhEslNV,cgErlNA,cACU,QAAA,sBDiHV,0BAAA,kBACE,QAAA,iBAIF,0BAAA,mBACE,QAAA,kBAIF,0BAAA,yBACE,QAAA,wBAKF,yBAAA,WC7HA,QAAA,gBDkIA,+CAAA,WClIA,QAAA,gBDuIA,gDAAA,WCvIA,QAAA,gBD4IA,0BAAA,WC5IA,QAAA,gBDuJF,eCvJE,QAAA,eD0JA,aAAA,eClKA,QAAA,gBACA,oBAAU,QAAA,gBACV,iBAAU,QAAA,oBhE2oNV,iBgE1oNA,iBACU,QAAA,sBDkKZ,qBACE,QAAA,eAEA,aAAA,qBACE,QAAA,iBAGJ,sBACE,QAAA,eAEA,aAAA,sBACE,QAAA,kBAGJ,4BACE,QAAA,eAEA,aAAA,4BACE,QAAA,wBAKF,aAAA,cCrLA,QAAA","sourcesContent":["/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: none;\n text-decoration: underline;\n text-decoration: underline dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"../fonts/glyphicons-halflings-regular.eot\");\n src: url(\"../fonts/glyphicons-halflings-regular.eot?#iefix\") format(\"embedded-opentype\"), url(\"../fonts/glyphicons-halflings-regular.woff2\") format(\"woff2\"), url(\"../fonts/glyphicons-halflings-regular.woff\") format(\"woff\"), url(\"../fonts/glyphicons-halflings-regular.ttf\") format(\"truetype\"), url(\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\") format(\"svg\");\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: 400;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: \"\\2014 \\00A0\";\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: \"\";\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: \"\\00A0 \\2014\";\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n.row-no-gutters [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n.col-xs-1,\n.col-sm-1,\n.col-md-1,\n.col-lg-1,\n.col-xs-2,\n.col-sm-2,\n.col-md-2,\n.col-lg-2,\n.col-xs-3,\n.col-sm-3,\n.col-md-3,\n.col-lg-3,\n.col-xs-4,\n.col-sm-4,\n.col-md-4,\n.col-lg-4,\n.col-xs-5,\n.col-sm-5,\n.col-md-5,\n.col-lg-5,\n.col-xs-6,\n.col-sm-6,\n.col-md-6,\n.col-lg-6,\n.col-xs-7,\n.col-sm-7,\n.col-md-7,\n.col-lg-7,\n.col-xs-8,\n.col-sm-8,\n.col-md-8,\n.col-lg-8,\n.col-xs-9,\n.col-sm-9,\n.col-md-9,\n.col-lg-9,\n.col-xs-10,\n.col-sm-10,\n.col-md-10,\n.col-lg-10,\n.col-xs-11,\n.col-sm-11,\n.col-md-11,\n.col-lg-11,\n.col-xs-12,\n.col-sm-12,\n.col-md-12,\n.col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1,\n.col-xs-2,\n.col-xs-3,\n.col-xs-4,\n.col-xs-5,\n.col-xs-6,\n.col-xs-7,\n.col-xs-8,\n.col-xs-9,\n.col-xs-10,\n.col-xs-11,\n.col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1,\n .col-sm-2,\n .col-sm-3,\n .col-sm-4,\n .col-sm-5,\n .col-sm-6,\n .col-sm-7,\n .col-sm-8,\n .col-sm-9,\n .col-sm-10,\n .col-sm-11,\n .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1,\n .col-md-2,\n .col-md-3,\n .col-md-4,\n .col-md-5,\n .col-md-6,\n .col-md-7,\n .col-md-8,\n .col-md-9,\n .col-md-10,\n .col-md-11,\n .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1,\n .col-lg-2,\n .col-lg-3,\n .col-lg-4,\n .col-lg-5,\n .col-lg-6,\n .col-lg-7,\n .col-lg-8,\n .col-lg-9,\n .col-lg-10,\n .col-lg-11,\n .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: 0.01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: 700;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n opacity: 0.65;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n background-image: none;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n background-image: none;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n background-image: none;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n background-image: none;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n background-image: none;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n background-image: none;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: 400;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: 400;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-right: 15px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-right: -15px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: 0.2em 0.6em 0.3em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #eeeeee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: 0.2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 12px;\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: \"\\2039\";\n}\n.carousel-control .icon-next:before {\n content: \"\\203a\";\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable\n\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// 1. Remove the bottom border in Chrome 57- and Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n//\n\nabbr[title] {\n border-bottom: none; // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: none;\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n -moz-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n -webkit-box-shadow: none !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"../fonts/glyphicons-halflings-regular.eot\");\n src: url(\"../fonts/glyphicons-halflings-regular.eot?#iefix\") format(\"embedded-opentype\"), url(\"../fonts/glyphicons-halflings-regular.woff2\") format(\"woff2\"), url(\"../fonts/glyphicons-halflings-regular.woff\") format(\"woff\"), url(\"../fonts/glyphicons-halflings-regular.ttf\") format(\"truetype\"), url(\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\") format(\"svg\");\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: 400;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: \"\\2014 \\00A0\";\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: \"\";\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: \"\\00A0 \\2014\";\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n.row-no-gutters [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n.col-xs-1,\n.col-sm-1,\n.col-md-1,\n.col-lg-1,\n.col-xs-2,\n.col-sm-2,\n.col-md-2,\n.col-lg-2,\n.col-xs-3,\n.col-sm-3,\n.col-md-3,\n.col-lg-3,\n.col-xs-4,\n.col-sm-4,\n.col-md-4,\n.col-lg-4,\n.col-xs-5,\n.col-sm-5,\n.col-md-5,\n.col-lg-5,\n.col-xs-6,\n.col-sm-6,\n.col-md-6,\n.col-lg-6,\n.col-xs-7,\n.col-sm-7,\n.col-md-7,\n.col-lg-7,\n.col-xs-8,\n.col-sm-8,\n.col-md-8,\n.col-lg-8,\n.col-xs-9,\n.col-sm-9,\n.col-md-9,\n.col-lg-9,\n.col-xs-10,\n.col-sm-10,\n.col-md-10,\n.col-lg-10,\n.col-xs-11,\n.col-sm-11,\n.col-md-11,\n.col-lg-11,\n.col-xs-12,\n.col-sm-12,\n.col-md-12,\n.col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1,\n.col-xs-2,\n.col-xs-3,\n.col-xs-4,\n.col-xs-5,\n.col-xs-6,\n.col-xs-7,\n.col-xs-8,\n.col-xs-9,\n.col-xs-10,\n.col-xs-11,\n.col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1,\n .col-sm-2,\n .col-sm-3,\n .col-sm-4,\n .col-sm-5,\n .col-sm-6,\n .col-sm-7,\n .col-sm-8,\n .col-sm-9,\n .col-sm-10,\n .col-sm-11,\n .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1,\n .col-md-2,\n .col-md-3,\n .col-md-4,\n .col-md-5,\n .col-md-6,\n .col-md-7,\n .col-md-8,\n .col-md-9,\n .col-md-10,\n .col-md-11,\n .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1,\n .col-lg-2,\n .col-lg-3,\n .col-lg-4,\n .col-lg-5,\n .col-lg-6,\n .col-lg-7,\n .col-lg-8,\n .col-lg-9,\n .col-lg-10,\n .col-lg-11,\n .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: 0.01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: 700;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n -ms-touch-action: manipulation;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n opacity: 0.65;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n background-image: none;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n background-image: none;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n background-image: none;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n background-image: none;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n background-image: none;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n background-image: none;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: 400;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n -o-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n -o-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n -o-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: 400;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n border-top: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-right: 15px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-right: -15px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: 0.2em 0.6em 0.3em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@-o-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n -webkit-background-size: 40px 40px;\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #eeeeee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: 0.2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: -webkit-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out, -o-transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 12px;\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: -webkit-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out, -o-transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: \"\\2039\";\n}\n.carousel-control .icon-next:before {\n content: \"\\203a\";\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable declaration-no-important, selector-no-qualifying-type\n\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important; // Black prints faster: h5bp.com/s\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n}\n","// stylelint-disable value-list-comma-newline-after, value-list-comma-space-after, indentation, declaration-colon-newline-after, font-family-no-missing-generic-family-keyword\n\n//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"@{icon-font-path}@{icon-font-name}.eot\");\n src: url(\"@{icon-font-path}@{icon-font-name}.eot?#iefix\") format(\"embedded-opentype\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff2\") format(\"woff2\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff\") format(\"woff\"),\n url(\"@{icon-font-path}@{icon-font-name}.ttf\") format(\"truetype\"),\n url(\"@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}\") format(\"svg\");\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\002a\"; } }\n.glyphicon-plus { &:before { content: \"\\002b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// https://getbootstrap.com/docs/3.4/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: https://a11yproject.com/posts/how-to-hide-content\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// stylelint-disable indentation, property-no-vendor-prefix, selector-no-vendor-prefix\n\n// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n word-wrap: break-word;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // WebKit-specific. Other browsers will keep their default outline style.\n // (Initially tried to also force default via `outline: initial`,\n // but that seems to erroneously remove the outline in Firefox altogether.)\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// stylelint-disable media-feature-name-no-vendor-prefix, media-feature-parentheses-space-inside, media-feature-name-no-unknown, indentation, at-rule-name-space-after\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","// stylelint-disable selector-list-comma-newline-after, selector-no-qualifying-type\n\n//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: 400;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n padding: .2em;\n background-color: @state-warning-bg;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @dl-horizontal-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: \"\\2014 \\00A0\"; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: \"\"; }\n &:after {\n content: \"\\00A0 \\2014\"; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n color: @pre-color;\n word-break: break-all;\n word-wrap: break-word;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n padding-right: ceil((@gutter / 2));\n padding-left: floor((@gutter / 2));\n margin-right: auto;\n margin-left: auto;\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-right: floor((@gutter / -2));\n margin-left: ceil((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-right: floor((@grid-gutter-width / 2));\n padding-left: ceil((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","// stylelint-disable selector-max-type, selector-max-compound-selectors, selector-no-qualifying-type\n\n//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n\n // Table cell sizing\n //\n // Reset default table behavior\n\n col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-column;\n float: none;\n }\n\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-cell;\n float: none;\n }\n }\n}\n\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\n\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n min-height: .01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n overflow-x: auto;\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * .75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type, property-no-vendor-prefix, media-feature-name-no-vendor-prefix\n\n//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: 700;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\ninput[type=\"search\"] {\n // Override content-box in Normalize (* isn't specific enough)\n .box-sizing(border-box);\n\n // Search inputs in iOS\n //\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n -webkit-appearance: none;\n appearance: none;\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n\n // Apply same disabled cursor tweak as for inputs\n // Some special care is needed because